Една характеристика на .net core 2.2, която не ми хрумна веднага, са „кукичките за стартиране“. Казано по-просто, това е начин за глобално регистриране на метод в сборка, който ще се изпълнява при всяко стартиране на .net основно приложение. Това отключва цял набор от сценарии, от инжектиране на профайлър до настройване на статичен контекст в дадена среда.

Как работи? Първо, трябва да създадете нов .net ядро ​​и да добавите клас StartupHook. Уверете се, че е извън пространството на имената. Този клас трябва да дефинира статичен метод за инициализация. Това е методът, който ще се извиква при всяко стартиране на .net основно приложение.

Следната кука, например, ще покаже „Hello world!“ когато се стартира приложение:

За да регистрирате куката, трябва да декларирате променлива на средата DOTNET_STARTUP_HOOKS, сочеща към сборката. След като направите това, ще видите показаното съобщение:

Наистина е важно да разберете, че куката се изпълнява вътре в процеса на кандидатстване. А глобално регистрирана кука, която работи във всички основни процеси на .net, звучи като добра настройка за шега за нищо неподозиращите колеги.

Обърнатата конзола

Каква шега? Нека започнем с нещо просто: какво ще кажете за замяна на потока на конзолата, за да обърнете показания текст? Първо пишем персонализиран TextWriter:

След това го присвояваме на конзолата в куката за стартиране:

След като куката за стартиране бъде регистрирана, всички .net core 2.2 приложения ще извеждат обърнат текст в конзолата:

И най-добрата част е, че работи дори за самия изпълним файл „dotnet.exe“!

Можете лесно да си представите объркването, което би произлязло от това.

Заменящ масив. Празен

Има ли нещо друго, което можем да направим? Не успях да заменя стойността на string.Empty (вероятно защото е декларирана като присъща), но вместо това можем да заменим стойността на Array.Empty<T>. Например за Array.Empty<string>:

Това звучи доста безобидно, докато не разгледате случая с методи с аргумент params. Например този метод:

От гледна точка на C# можете да го извикате без никакъв параметър (PrintArgs()). Но от гледна точка на IL параметърът args е просто обикновен масив. Магията се прави от компилатора, който автоматично вмъква празен масив, ефективно пренаписвайки извикването към PrintArgs(Array.Empty<string>()). Следователно, с регистрирана кука за стартиране, методът, извикан без никакъв параметър, всъщност ще покаже „Hello world!“.

Асинхронна държавна машина

Това вече са добри начини да объркате колеги, но исках да отида още по-далеч. Тогава си помислих да заменя TaskScheduler по подразбиране. Какво бихме могли да направим с него? Какво ще кажете за... пренаписване на произволни стойности в асинхронната държавна машина? Когато даден метод използва async/await, той се преобразува в държавна машина, която съхранява между другото локалните променливи, използвани от метода (за възстановяване на контекста, когато продължаването на await започне да се изпълнява). Ако успеем да извлечем тази държавна машина, следователно можем да променим стойността на локалните между всеки await!

Започваме с деклариране на нашия персонализиран планировчик на задачи (и го именуваме ThreadPoolTaskScheduler, в случай че някой се сети да инспектира стека за извикване) и го използваме, за да презапишем TaskScheduler.Default.

Имайте предвид, че винаги задаваме s_asyncDebuggingEnabled на true, за да избегнем различно поведение, когато е прикрепен дебъгерът, което би усложнило нашия код. Планировчикът на задачи извиква празен метод MutateState, след което използва пула от нишки, за да планира изпълнението на задачата. Сега трябва да приложим този метод.

Как да възстановите държавната машина? Първата стъпка е да извлечете ContinuationWrapper. Това е структура, която обвива действието на задачата, когато s_asyncDebuggingEnabled е зададено на true. В зависимост от вида на задачата, можем да я намерим или в действие на задача, или в състояние:

Оттам извличаме стойността на полето _continuation и проверяваме дали то е екземпляр на AsyncStateMachineBox. Ако е, тогава можем да намерим държавната машина в полето StateMachine:

Как изглежда асинхронната държавна машина?

Винаги има две публични полета: <>1__state и <>t__builder. <>1__state се използва за съхраняване на текущата стъпка на изпълнение в асинхронния метод. Можем да го използваме например за пренавиване на изпълнението на метода. <>t__builder съдържа съоръженията, използвани за изчакване на други методи (вложени повиквания). Има много неща, които можем да направим с него, но ще се съсредоточим върху местните.

Местните се съхраняват в частните ниви. В този случай <>u__1 и <j>5__1. Това са тези, с които искаме да играем:

Това, което правим тук, е да създадем нова държавна машина, след което да копираме стойността на старите полета в новите. Ако полето е лично и е int, ние го заместваме с произволна стойност.

Сега нека направим проста програма за тестване на куката:

И ще видите, че... не работи. Защо? Тъй като TPL е доста добре оптимизиран. На много места кодът проверява текущия планировчик и напълно го заобикаля, ако е по подразбиране за директно планиране на продължението в пула от нишки. Например в YieldAwaiter (използвано от Task.Yield).

Как можем да заобиколим това? Абсолютно се нуждаем от нашия персонализиран планировчик на задачи да бъде този по подразбиране, в противен случай той няма да се използва при извикване на Task.Run. Но ако планировчикът на задачи по подразбиране е присвоен на задача, тогава няма да бъдем извикани обратно и няма да можем да променим състоянието. Ако проверим кода на YieldAwaiter по-горе, можем да видим, че той прави просто референтно сравнение. Така че можем да презапишем планировчика на задачата с нов екземпляр на нашия персонализиран планировчик, за да заблудим тези проверки:

Готови ли сме? Ако се върнем към нашия пример, можем да започнем отстраняването на грешки стъпка по стъпка:

аз съм на 42, всичко е наред. Още една стъпка и…

Сега отидете и се насладете на смаяните погледи на вашите колеги!

Обърнете внимание, че това няма да работи, когато използвате ConfigureAwait(false), защото директно поставя продължението в пула от нишки и дори няма да провери текущия планировчик на задачи (защо би го направило?). Един от начините да заобиколите това може да бъде да замените конструктора на задачи с персонализиран такъв, но шегата вече стигна достатъчно далеч, както е 🙂

Разбира се, всички тези трикове могат да имат непредвидими ефекти върху целевите приложения, така че не забравяйте да наблюдавате внимателно шегата и спрете веднага щом може да стане опасна.