В процессе миграции с .NET Framework на .NET Core могут всплыть некоторые неприятные моменты. Например, если ваше приложение использует домены — логику придется переписывать. Аналогичная ситуация с Thread.Abort(): Microsoft настолько не любит эту практику (и справедливо), что сначала они объявили этот метод deprecated, а затем полностью выпилили его из фреймворка и теперь он вероломно выбрасывает PlatformNotSupportedException.
Но что делать, если ваше приложение использует Thread.Abort(), а вы очень хотите перевести его на .NET Core, ничего не переписывая? Ну, мы-то прекрасно знаем, что платформа очень даже поддерживает этот функционал, так что могу вас обрадовать: выход есть, нужно всего лишь собрать свою собственную версию CLR.
Disclaimer: Это сугубо практическая статья с минимумом теории, призванная только продемонстрировать новые варианты взаимодействия разработчика и .NET среды. Никогда не делайте так в продакшене. Но если очень хочется...
Сделать это стало возможным благодаря двум вещам: стремлению Microsoft к кроссплатформенности .NET Core и проделанной разработчиками работе по переносу исходников фреймворка в открытый доступ. Давайте используем это в своих интересах.
Теоретический минимум:
Прежде чем переходить к нашей основной цели — возвращению Thread.Abort() — давайте для разминки поменяем что-нибудь фундаментальное в CoreFX чтобы проверить работоспособность всех инструментов. Например, по следам моей предыдущей статьи про dynamic, полностью запретим его использование в приложении. Зачем? Потому что мы можем.
Прежде всего установим все необходимое для сборки:
.NET desktop development
Desktop development with C++
.NET Core cross-platform development
Клонируем corefx:
Теперь запретим dynamic. Для этого откроем (здесь и далее я буду указывать относительные от корня репозитория пути)
И в конец функции CallSite<T>.Create вставляем незамысловатый код:
Возвращаемся в corefx и выполняем build.cmd. После окончания сборки, создаем в Visual Studio новый .NET Core проект со следующим содержимым:
Компилируем наш проект. Теперь идем в
и находим там пакет выглядящий примерно так: Microsoft.Private.CoreFx.NETCoreApp.5.0.0-dev.19465.1.nupkg. Открываем .csproj нашего проекта и вставляем туда следующие строки:
Версия должна быть такая же, как в названии собранного пакета. В моем случае 5.0.0-dev.19465.1.
Переходим в настройки nuget для нашего проекта и добавляем туда два новых пути:
И снимаем галочки у всех остальных.
Переходим в папку с проектом и выполняем
Готово! Осталось только запустить:
Работает! Библиотеки берутся не из GAC, dynamic не работает.
Теперь перейдем ко второй части, возвращению Thread.Abort(). Здесь нас ждет неприятный сюрприз: имплементация Thread лежит в CoreCLR, который не является частью CoreFX и предустанавливается на машину отдельно. Сперва создадим демонстрационный проект:
Выкачиваем coreclr. Находим файл
И заменяем Abort() на
Теперь нам нужно вернуть атрибуты и с++ имплементацию. Я собрал её по кусочкам из различных открытых репозиториев .NET Framework и для удобства оформил все изменения в виде пулл-реквеста.
Note: при сборке возникали проблемы с ресурсами в «новых» атрибутах, которые я
«исправил» заглушками. Учитывайте это, если захотите использовать этот код где-либо, кроме домашних экспериментов
После интеграции этих изменений в код, запускаем build.cmd из coreclr. Сборка на поздних этапах может начать сыпать ошибками, но это не страшно, нам главное чтобы смог собраться CoreCLR. Он будут лежать в:
Выполняем
Файлы из Windows_NT.x64.Debug скидываем в папку с опубликованным приложением.
Как только библиотеки собрались, переходим в
и клонируем папку 3.0.0. Назовем её, например, 5.0.1. Скопируем туда все, что лежит в Windows_NT.x64.Debug, кроме папок. Теперь наша версия CoreCLR будет доступна через runtimeconfig.
Собираем наш проект. Добавляем в .csproj:
Повторяем манипуляции с nuget из предыдущей части статьи. Публикуем
В runtimeconfig.json впишем следующую конфигурацию:
Запускаем!
Магия произошла. Теперь в наших .NET Core приложениях снова работает Thread.Abort(). Но, разумеется, только на Windows.
Но что делать, если ваше приложение использует Thread.Abort(), а вы очень хотите перевести его на .NET Core, ничего не переписывая? Ну, мы-то прекрасно знаем, что платформа очень даже поддерживает этот функционал, так что могу вас обрадовать: выход есть, нужно всего лишь собрать свою собственную версию CLR.
Disclaimer: Это сугубо практическая статья с минимумом теории, призванная только продемонстрировать новые варианты взаимодействия разработчика и .NET среды. Никогда не делайте так в продакшене. Но если очень хочется...
Сделать это стало возможным благодаря двум вещам: стремлению Microsoft к кроссплатформенности .NET Core и проделанной разработчиками работе по переносу исходников фреймворка в открытый доступ. Давайте используем это в своих интересах.
Теоретический минимум:
- dotnet publish возволяет нам публиковать standalone приложение: фреймворк будет поставляться вместе с ним, а не искаться где-то в GAC
- Версию CoreCLR, на которой будет запускаться приложение, при некоторых условиях можно задать при помощи runtimeconfig.json
- Мы можем собрать свой собственный CoreFX: github.com/dotnet/corefx
- Мы можем собрать свой собственный CoreCLR: github.com/dotnet/coreclr
Кастомизируем CoreFX
Прежде чем переходить к нашей основной цели — возвращению Thread.Abort() — давайте для разминки поменяем что-нибудь фундаментальное в CoreFX чтобы проверить работоспособность всех инструментов. Например, по следам моей предыдущей статьи про dynamic, полностью запретим его использование в приложении. Зачем? Потому что мы можем.
Prerequisites
Прежде всего установим все необходимое для сборки:
- CMake
- Visual Studio 2019 Preview
- Latest .NET Core SDK (.NET Core 3.0 Preview)
Visual Studio 2019 — Workloads
.NET desktop development
- All Required Components
- .NET Framework 4.7.2 Development Tools
Desktop development with C++
- All Required Components
- VC++ 2019 v142 Toolset (x86, x64)
- Windows 8.1 SDK and UCRT SDK
- VC++ 2017 v141 Toolset (x86, x64)
.NET Core cross-platform development
- All Required Components
Visual Studio 2019 — Individual components
- C# and Visual Basic Roslyn Compilers
- Static Analysis Tools
- .NET Portable Library Targeting Pack
- Windows 10 SDK or Windows 8.1 SDK
- Visual Studio C++ Core Features
- VC++ 2019 v142 Toolset (x86, x64)
- VC++ 2017 v141 Toolset (x86, x64)
- MSBuild
- .NET Framework 4.7.2 Targeting Pack
- Windows Universal CRT SDK
Клонируем corefx:
git clone https://github.com/dotnet/corefx.git
Теперь запретим dynamic. Для этого откроем (здесь и далее я буду указывать относительные от корня репозитория пути)
corefx\src\System.Linq.Expressions\src\System\Runtime\CompilerServices\CallSite.cs
И в конец функции CallSite<T>.Create вставляем незамысловатый код:
throw new PlatformNotSupportedException("No way");
Возвращаемся в corefx и выполняем build.cmd. После окончания сборки, создаем в Visual Studio новый .NET Core проект со следующим содержимым:
public int Test { get; set; }
public static void Main(string[] args)
{
try
{
dynamic a = new Program();
a.Test = 120;
}
catch (Exception e)
{
Console.WriteLine(e);
}
//Узнаем, откуда берутся сборки
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
Console.WriteLine(asm.Location);
}
}
Компилируем наш проект. Теперь идем в
corefx\artifacts\packages\Debug\NonShipping
и находим там пакет выглядящий примерно так: Microsoft.Private.CoreFx.NETCoreApp.5.0.0-dev.19465.1.nupkg. Открываем .csproj нашего проекта и вставляем туда следующие строки:
<PropertyGroup>
<PackageConflictPreferredPackages>Microsoft.Private.CoreFx.NETCoreApp;runtime.$(RuntimeIdentifiers).Microsoft.Private.CoreFx.NETCoreApp;$(PackageConflictPreferredPackages)</PackageConflictPreferredPackages>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Private.CoreFx.NETCoreApp" Version="5.0.0-dev.19465.1" />
</ItemGroup>
Версия должна быть такая же, как в названии собранного пакета. В моем случае 5.0.0-dev.19465.1.
Переходим в настройки nuget для нашего проекта и добавляем туда два новых пути:
corefx\artifacts\packages\Debug\NonShipping
corefx\artifacts\packages\Debug\Shipping
И снимаем галочки у всех остальных.
Переходим в папку с проектом и выполняем
dotnet publish --runtime win-x64 --self-contained
Готово! Осталось только запустить:
Работает! Библиотеки берутся не из GAC, dynamic не работает.
Make CoreCLR Great Again
Теперь перейдем ко второй части, возвращению Thread.Abort(). Здесь нас ждет неприятный сюрприз: имплементация Thread лежит в CoreCLR, который не является частью CoreFX и предустанавливается на машину отдельно. Сперва создадим демонстрационный проект:
var runtimeInformation = RuntimeInformation.FrameworkDescription;
Console.WriteLine(runtimeInformation);
var thr = new Thread(() =>
{
try
{
while (true)
{
Console.WriteLine(".");
Thread.Sleep(500);
}
}
catch (ThreadAbortException)
{
Console.WriteLine("Thread aborted!");
}
});
foreach (var asm in AppDomain.CurrentDomain.GetAssemblies())
{
Console.WriteLine(asm.Location);
}
thr.Start();
Thread.Sleep(2000);
thr.Abort();
Выкачиваем coreclr. Находим файл
coreclr\src\System.Private.CoreLib\shared\System\Threading\Thread.cs
И заменяем Abort() на
[SecuritySafeCritical]
[SecurityPermissionAttribute(SecurityAction.Demand, ControlThread = true)]
public void Abort()
{
AbortInternal();
}
[System.Security.SecurityCritical] // auto-generated
[ResourceExposure(ResourceScope.None)]
[MethodImplAttribute(MethodImplOptions.InternalCall)]
private extern void AbortInternal();
Теперь нам нужно вернуть атрибуты и с++ имплементацию. Я собрал её по кусочкам из различных открытых репозиториев .NET Framework и для удобства оформил все изменения в виде пулл-реквеста.
Note: при сборке возникали проблемы с ресурсами в «новых» атрибутах, которые я
«исправил» заглушками. Учитывайте это, если захотите использовать этот код где-либо, кроме домашних экспериментов
После интеграции этих изменений в код, запускаем build.cmd из coreclr. Сборка на поздних этапах может начать сыпать ошибками, но это не страшно, нам главное чтобы смог собраться CoreCLR. Он будут лежать в:
coreclr\bin\Product\Windows_NT.x64.Debug
Простой путь
Выполняем
dotnet publish --runtime win-x64 --self-contained
Файлы из Windows_NT.x64.Debug скидываем в папку с опубликованным приложением.
Сложный путь
Как только библиотеки собрались, переходим в
C:\Program Files\dotnet\shared\Microsoft.NETCore.App
и клонируем папку 3.0.0. Назовем её, например, 5.0.1. Скопируем туда все, что лежит в Windows_NT.x64.Debug, кроме папок. Теперь наша версия CoreCLR будет доступна через runtimeconfig.
Собираем наш проект. Добавляем в .csproj:
<PropertyGroup>
<DisableImplicitFrameworkReferences>true</DisableImplicitFrameworkReferences> <PackageConflictPreferredPackages>Microsoft.Private.CoreFx.NETCoreApp;runtime.$(RuntimeIdentifiers).Microsoft.Private.CoreFx.NETCoreApp;$(PackageConflictPreferredPackages)</PackageConflictPreferredPackages>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Private.CoreFx.NETCoreApp" Version="5.0.0-dev.19465.1" />
</ItemGroup>
Повторяем манипуляции с nuget из предыдущей части статьи. Публикуем
dotnet publish --runtime win-x64 --self-contained
В runtimeconfig.json впишем следующую конфигурацию:
{
"runtimeOptions": {
"tfm": "netcoreapp3.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "5.0.1"
}
}
}
Результат
Запускаем!
Магия произошла. Теперь в наших .NET Core приложениях снова работает Thread.Abort(). Но, разумеется, только на Windows.
AxisPod
А чего это MS не любит эту практику, прерывать выполнение потока, а если он в файл в это время пишет или формирует ответ сервера клиенту? Все не любят эту практику, а за использование надо руки отрывать и более не подпускать к компьютеру. Данная функция просто убивает контроль над завершением потока. Ну не надо изначально так делать и при портировании пусть переписывают код на нормальные модели прерывания работы потоков.
iluxa1810
Имхо, все таки иногда можно грохнуть поток, если есть полная уверенность, что завершение ничему не навредит.
Например, есть метод из сторонней либы, который на вход принимает массив и выполняет расчеты. Доступа к исходникам нету. Мы запускаем в отдельном потоке выполнение и может так случится, что она не отработает за необходимое время. Что нам делать, так как? Получается, что единственный вариант — это прекратить работу убив поток. Или есть варианты по лучше?
AxisPod
И какова уверенность, что он не навредит? Утечки памяти и ресурсов это не вред? Кто знает, может там неуправляемый код есть.
iluxa1810
А есть еще какие-то варианты, если исходный код либы не доступен => туда нельзя добавить проверку токенов отмены?
Вроде, отдельный домен решает эту проблему красиво и его не жалко прибить?
Lelushak Автор
При выгрузке домена, под капотом происходит Abort всех потоков, находящихся в нём. Правда память в итоге очистится, так что утечки не страшны, но не уверен насчёт неуправляемых ресурсов, нужно почитать про это
В текущих условиях мне приходит в голову только вынос кода в отдельный процесс. Это относительно несложно делать на лету при помощи ExpressionTrees и пайпов.
iluxa1810
А отдельный домен- это разве не полностью изолированная область => пофиг что там поломается при убийстве потоков?
Правда, утечки неуправляемых ресурсов все же возможны…
Lelushak Автор
Отредактировал комментарий уже после вашего ответа :(
Вот что пишут:
iluxa1810
А есть пример, как на лету создать процесс через ExpressionTrees?
Lelushak Автор
Честно говоря, я сам не пробовал. Идея в том, что через ряд ухищрений их можно сериализовать и отправить в заранее заготовленный экзешник, в котором и исполнять в виде отдельного процесса.
Но в такой схеме гораздо проще использовать рефлексию: отправить в экзешник путь до либы, класс, метод и сериализованные параметры. Остается только на каждый запуск генерировать уникальное имя процесса и управлять его временем жизни. Результат возвращать так же в сериализованном виде