Возвращаем Thread.Abort() в .NET Core. Поставка приложения со своей версией CoreCLR и CoreFX +6


В процессе миграции с .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

  • 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.




К сожалению, не доступен сервер mySQL