Автор объясняет, как он использовал скрипты Roslyn и режим сокетов Slack для проверки и отладки запущенного приложения и взаимодействия с ним.
Проблема
Я использую Mykeels.CSharpRepl уже несколько лет для отладки своих .NET-приложений. Он предоставляет мне REPL на C# с полным доступом к интерактивному выполнению кода приложения подобно окну Immediate в Visual Studio, за исключением того, что он не привязан к точкам останова отладчика или IDE.
Для моих приложений это фактически создаёт ещё одну точку входа. Я могу просто запустить:
./myapplication.exe repl
и начать интерактивное выполнение кода приложения.
При этом, когда оно уже запущено на сервере, я могу подключиться к машине по SSH и запустить REPL, но это запускает другой процесс. Он имеет доступ к тем же сборкам и конфигурации, но это не запущенное приложение.
У запущенного процесса есть состояние, которого нет у нового процесса. Оно включает кэши в оперативной памяти, синглтон-сервисы, активные фоновые обработчики, открытые соединения и тот же контекст выполнения. Именно он привёл к ошибке, которую я пытаюсь исследовать.
Мне нужен был REPL внутри самого работающего приложения.
Идея
Несколько недель назад при создании симулятора инцидентов я решил использовать Slack в качестве основного интерфейса. Вместо того, чтобы предоставлять HTTP-конечные точки или создавать собственную панель мониторинга, приложение полностью взаимодействовало через Slack, используя режим сокетов.
Это означало, что у работающего процесса уже был постоянный двусторонний канал связи.
И тут меня осенило.
Если моё приложение может получать сообщения через Slack…
…почему эти сообщения не могут быть написаны на C#?
Что ещё важнее, почему они не могут выполняться внутри собственного процесса приложения?
В этот момент Slack перестал быть интересной частью задачи.
Он стал не более чем транспортным уровнем для Mykeels.CSharpRepl.
Цель заключалась в том, чтобы предоставить работающему .NET-приложению интерактивную консоль, доступную из Slack.
Архитектура
Если Slack должен был стать транспортным уровнем для работающего REPL, необходимо было решить три проблемы:
REPL не мог блокировать основной поток приложения.
Каждой сессии Slack требовался собственный независимый контекст выполнения.
REPL требовался доступ к сервисам и конфигурации приложения.
Реализация, которая это обеспечила, доступна в данном pull request.
Решение первой задачи было довольно простым.
REPL фактически представляет собой бесконечный цикл, ожидающий ввода пользователя, что, очевидно, нежелательно для работы в основном потоке приложения.
Вместо этого я размещаю REPL Slack внутри BackgroundService. Если соединение со Slack по какой-либо причине обрывается, сервис просто переподключается через небольшую задержку, не влияя на остальную часть приложения.
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace MyApplication; // Runs ReplHost.RunSlack for the lifetime of the host. Restarts on failure instead of // letting an unhandled exception here bring down the whole ASP.NET app. public sealed class SlackReplBackgroundService( IConfiguration configuration, ILogger<SlackReplBackgroundService> logger ) : BackgroundService { private static readonly TimeSpan RestartDelay = TimeSpan.FromSeconds(5); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { try { logger.Info("Starting Slack REPL host"); await ReplHost.RunSlack(configuration, stoppingToken); } catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; } catch (Exception ex) { logger.LogError(ex, "Slack REPL host crashed, restarting in {RestartDelay}", RestartDelay); try { await Task.Delay(RestartDelay, stoppingToken); } catch (OperationCanceledException) { break; } } } } }
В файле ReplHost.cs у нас есть:
sing Microsoft.Extensions.Configuration; using Mykeels.CSharpRepl; namespace Under4Games; public static class ReplHost { private static string[] Namespaces = [ "System", "System.Collections.Generic", "System.Linq", "MyApplication" ]; public static async Task RunSlack(IConfiguration configuration, CancellationToken cancellationToken = default) { const string ApplicationName = "MyApplication.Slack"; string RequireConfiguration(string key) => configuration.GetValue<string>(key) ?? throw new Exception($"Configuration key {key} is required"); await SlackReplHost.Run( new SlackReplOptions { BotToken = RequireConfiguration("Slack:BotToken"), AppToken = RequireConfiguration("Slack:AppToken"), // Fails closed if left unset — see SlackReplOptions.AllowedUserIds. AllowedChannelIds = RequireConfiguration("Slack:AllowedChannelIds") .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .ToHashSet(), SlashCommand = "/myapplication-repl", }, commands: [ "using static MyApplication.ScriptGlobals;" ], config: new CSharpRepl.Services.Configuration( usings: Namespaces, applicationName: ApplicationName ), onLoad: (rosylnServices) => { rosylnServices .EvaluateAsync( "using static MyApplication.ScriptGlobals;", cancellationToken: CancellationToken.None ) .ConfigureAwait(false); string cwd = AppContext.BaseDirectory; string logFilePath = Path.Combine(cwd, $"{ApplicationName}.log"); Console.WriteLine($"Logs: {logFilePath}"); }, cancellationToken: cancellationToken ); } }
В файле Program.cs я могу запустить REPL-хост следующим образом:
var host = Host.CreateDefaultBuilder(args) .ConfigureServices((context, services) => { services.AddHostedService<SlackReplBackgroundService>(); }) .Build(); await host.RunAsync();
На этом этапе приложение запускает обработчик событий Slack наряду с остальными стандартными сервисами.
Как это работает
Здесь стоит отметить несколько моментов.
Режим сокета означает отсутствие общедоступной HTTP-конечной точки для открытия или туннелирования.
Каждый поток Slack получает свою собственную сессию Roslyn, поэтому несколько разработчиков могут использовать REPL одновременно, не мешая друг другу.
Я предварительно загружаю несколько пространств имён и вспомогательных методов, чтобы сессия воспринималась как расширение приложения, а не как пустой скрипт C#.
Для получения более подробной информации о работе REPL, пожалуйста, обратитесь к файлу README библиотеки Mykeels.CSharpRepl.
Безопасность и авторизация
Выполнение произвольного кода C# внутри запущенного процесса должно вызывать опасения у каждого инженера.
Для смягчения этой проблемы система спроектирована таким образом, чтобы закрывать REPL при сбое и разрешать его использование только определённым пользователям и каналам. Я добавил несколько функций безопасности в класс SlackReplOptions:
AllowedChannelIds: Разрешить использование REPL только в определённых каналах.AllowedUserIds: Разрешить использование REPL только определённым пользователям.RestrictRepliesToSessionOwner: Разрешить отправку сообщений в поток своей сессии только пользователю, выполнившему команду слэша. По умолчанию — true.IsAuthorized: Функция обратного вызова, которую можно использовать для добавления дополнительных проверок в процесс авторизации.
Куда можно двигаться дальше
Как только приложение сможет проверять себя во время выполнения, останется лишь небольшой шаг до того, чтобы позволить ему адаптироваться во время выполнения.
Представьте себе помощника во время выполнения, который:
отслеживает повторяющиеся сбои,
захватывает контекст выполнения,
предлагает временное исправление,
запрашивает одобрение человека,
применяет исправление без перезапуска процесса,
генерирует эквивалентный запрос на слияние в GitHub
и удаляет исправление во время выполнения после того, как оно будет включено в обычный релиз.
Эта идея переросла в то, что я называю Runtime Reliability Manager (RRM).
Придёт ли в итоге Mykeels.CSharpRepl к этому, я пока не знаю.
Но превращение Slack в консоль для работы в реальном времени заставило меня понять, что работающим приложениям не обязательно быть пассивными наблюдателями собственных сбоев.