Возвращаем Thread.Abort() в .NET Core. Поставка приложения со своей версией CoreCLR и CoreFX +6
DIY или Сделай сам, .NET, C#, Ненормальное программирование
Рекомендация: подборка платных и бесплатных курсов дизайна интерьера - https://katalog-kursov.ru/
В процессе миграции с .NET Framework на .NET Core могут всплыть некоторые неприятные моменты. Например, если ваше приложение использует домены — логику придется переписывать. Аналогичная ситуация с Thread.Abort(): Microsoft настолько не любит эту практику (и справедливо), что сначала они объявили этот метод deprecated, а затем полностью выпилили его из фреймворка и теперь он вероломно выбрасывает PlatformNotSupportedException.
Но что делать, если ваше приложение использует 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
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.
К сожалению, не доступен сервер mySQL