多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

.NET 运行时仓库中的 Microsoft.Extensions.Hosting.WindowsServices:Windows 服务托管生命周期深度解析

.NET 运行时仓库中的 Microsoft.Extensions.Hosting.WindowsServices:Windows 服务托管生命周期深度解析 .NET 运行时仓库中的 Microsoft.Extensions.Hosting.WindowsServicesWindows 服务托管生命周期深度解析【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtimeMicrosoft.Extensions.Hosting.WindowsServices 是 .NET 运行时仓库runtime中负责将通用主机Generic Host接入 Windows 服务Windows Service基础设施的库它把IHostLifetime替换为基于ServiceBase的WindowsServiceLifetime使你的 Worker 服务既能以控制台方式运行如开发调试也能无缝注册为 Windows 服务交由服务控制管理器SCM托管。读完本文你将掌握UseWindowsService/AddWindowsService的完整用法、上下文感知context-aware激活原理、服务生命周期事件与宿主停止信号之间的协作机制以及仓库源码与测试对这一行为的具体验证方式。一、包定位与部署方式src/libraries/Microsoft.Extensions.Hosting.WindowsServices/README.md 明确指出该库提供了在 Windows 服务中使用托管基础设施hosting的实现即Microsoft.Extensions.Hosting与 Windows 服务之间的适配层。关于部署README 给出了两条关键事实随 ASP.NET Core 共享框架shared framework分发在 .NET 的 ASP.NET Core 应用例如 Web 应用中无需额外安装包即可直接使用。同时以 out-of-bandOOB方式发布可以显式引用Microsoft.Extensions.Hosting.WindowsServicesNuGet 包到任意项目中不受共享框架版本绑定约束。从项目文件 src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/Microsoft.Extensions.Hosting.WindowsServices.csproj 可以看到该库的实际构造TargetFrameworks$(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.1;netstandard2.0;$(NetFrameworkMinimum)/TargetFrameworks IsPackabletrue/IsPackable PackageDescription.NET hosting infrastructure for Windows Services./PackageDescription它同时面向当前/上一代/最低支持版本的 .NETNetCoreApp*、netstandard2.1、netstandard2.0以及 .NET FrameworkNetFrameworkMinimum并声明为可打包IsPackabletrue这从源码层面印证了随框架内置 OOB 独立包双轨部署的设计。其依赖项也清晰可见Microsoft.Extensions.Hosting提供IHost、IHostBuilder、IHostLifetime等宿主抽象Microsoft.Extensions.Logging.EventLog负责向 Windows 事件日志写入日志System.ServiceProcess.ServiceController提供ServiceBase、ServiceController等 Windows 服务 API在 .NET Framework 目标下则直接引用System.ServiceProcess程序集。二、快速上手把宿主变成 Windows 服务仓库内的 src/libraries/Microsoft.Extensions.Hosting.WindowsServices/src/PACKAGE.md 提供了基于 Visual Studio Worker Service 模板的完整示例IHost host Host.CreateDefaultBuilder(args) .ConfigureServices(services { services.AddHostedServiceWorker(); }) // 配置为 Windows 服务 .UseWindowsService(options { options.ServiceName My Service; }) .Build(); host.Run();这段代码对应的是经典的IHostBuilder编程模型。若使用 .NET 6 引入的HostApplicationBuilder则改用AddWindowsService扩展方法它作用于IServiceCollectionvar builder Host.CreateApplicationBuilder(args); builder.Services.AddHostedServiceWorker(); builder.Services.AddWindowsService(options { options.ServiceName My Service; }); var host builder.Build(); host.Run();两种扩展方法的完整签名在 ref/Microsoft.Extensions.Hosting.WindowsServices.cs 中定义public static IHostBuilder UseWindowsService(this IHostBuilder hostBuilder); public static IHostBuilder UseWindowsService(this IHostBuilder hostBuilder, ActionWindowsServiceLifetimeOptions configure); public static IServiceCollection AddWindowsService(this IServiceCollection services); public static IServiceCollection AddWindowsService(this IServiceCollection services, ActionWindowsServiceLifetimeOptions configure);要点说明options.ServiceName对应WindowsServiceLifetimeOptions.ServiceName属性见 WindowsServiceLifetimeOptions.cs默认值为string.Empty若保持为空WindowsServiceLifetime将以空字符串作为ServiceBase.ServiceName。实际注册 Windows 服务时如sc create命令注册名与ServiceName应保持一致便于 SCM 正确识别。在非 Windows 平台或不以服务身份运行时上述调用不会产生任何副作用详见下一节的上下文感知机制因此可以安全地保留在跨平台代码中。以服务方式发布与安装构建为 Windows 服务后需要先安装再启动典型流程为# 将应用发布为 framework-dependent 或 self-contained 的可执行文件 dotnet publish -c Release -r win-x64 --self-contained false # 以管理员身份创建服务DisplayName 为服务显示名binPath 指向可执行文件 sc create My Service binPath C:\publish\MyApp.exe DisplayName My Service # 启动与停止 sc start My Service sc stop My Service # 删除服务 sc delete My Service也可以使用System.ServiceProcess.ServiceController编程式地查询服务状态测试代码即采用了这一方式见后文。三、上下文感知仅在 Windows 服务中才激活UseWindowsService与AddWindowsService最值得注意的设计是上下文感知context aware。以 WindowsServiceLifetimeHostBuilderExtensions.cs 中的实现为例public static IHostBuilder UseWindowsService(this IHostBuilder hostBuilder, ActionWindowsServiceLifetimeOptions configure) { ArgumentNullException.ThrowIfNull(hostBuilder); if (WindowsServiceHelpers.IsWindowsService()) { hostBuilder.ConfigureServices(services { AddWindowsServiceLifetime(services, configure); }); } return hostBuilder; }只有在WindowsServiceHelpers.IsWindowsService()返回true时才会真正注册 Windows 服务生命周期否则扩展方法就是一个空操作。这意味着同一份可执行文件既可以直接在命令行运行此时仍使用默认的ConsoleLifetime也可以被 SCM 启动此时自动切换为WindowsServiceLifetime开发与生产环境无需维护两套构建产物。那么IsWindowsService()是如何判断的看 WindowsServiceHelpers.csprivate static bool GetIsWindowsService() { if ( #if NETFRAMEWORK Environment.OSVersion.Platform ! PlatformID.Win32NT #elif NET !OperatingSystem.IsWindows() #else !RuntimeInformation.IsOSPlatform(OSPlatform.Windows) #endif ) { return false; } var parent Internal.Win32.GetParentProcess(); if (parent null) { return false; } return string.Equals(services, parent.ProcessName, StringComparison.OrdinalIgnoreCase); }判定逻辑分两步平台检查非 Windows 平台直接返回false针对不同目标框架使用PlatformID.Win32NT、OperatingSystem.IsWindows()或RuntimeInformation.IsOSPlatform。父进程检查通过 Internal/Win32.cs 调用CreateToolhelp32Snapshot枚举系统进程找到当前进程的父进程PROCESSENTRY32.th32ParentProcessID再比较父进程名是否为services不区分大小写——Windows 服务由服务控制管理器services.exe派生因此该启发式规则非常可靠。该值在类型初始化时一次性计算并缓存于静态只读字段_isWindowService。测试 UseWindowsServiceTests.cs 中的DefaultsToOffOutsideOfService用例正好验证了这一点在非服务环境下调用UseWindowsService()后IHostLifetime解析结果仍是ConsoleLifetime。四、WindowsServiceLifetime连接 SCM 与宿主的桥梁当检测到以服务身份运行时注册的核心服务是 WindowsServiceLifetime.cs[SupportedOSPlatform(windows)] public class WindowsServiceLifetime : ServiceBase, IHostLifetime这个类同时继承System.ServiceProcess.ServiceBaseWindows 服务的基类并实现IHostLifetime宿主生命周期抽象因此它既能被 SCM 驱动OnStart/OnStop/OnShutdown又能被宿主驱动WaitForStartAsync/StopAsync。4.1 WaitForStartAsync异步启动的门闩宿主启动时调用WaitForStartAsync。实现使用TaskCompletionSource_delayStart作为门闩并在后台线程中调用Run(this)进入 SCM 服务分发循环public Task WaitForStartAsync(CancellationToken cancellationToken) { cancellationToken.Register(() _delayStart.TrySetCanceled()); ApplicationLifetime.ApplicationStarted.Register(() { /* 记录 Application started... 日志 */ }); ApplicationLifetime.ApplicationStopping.Register(() { /* 记录 Application is shutting down... 日志 */ }); ApplicationLifetime.ApplicationStopped.Register(_delayStop.Set); Thread thread new Thread(Run); thread.IsBackground true; thread.Start(); // 否则会阻塞阻止 IHost.StartAsync 完成。 return _delayStart.Task; }关键点服务分发线程被设置为后台线程IsBackground true避免阻塞IHost.StartAsync真正的启动完成信号来自OnStartprotected override void OnStart(string[] args) { _delayStart.TrySetResult(null); base.OnStart(args); }当 SCM 发出 START 命令、ServiceBase.Run回调OnStart时_delayStart被置为完成状态WaitForStartAsync返回宿主继续执行后续的HostedService.StartAsync等启动流程。若Run(this)在未收到启动信号的情况下直接返回服务被立即停止则通过_delayStart.TrySetException(new InvalidOperationException(Stopped without starting))抛出异常若服务分发过程中出现任何异常也会被捕获并分别传播到_delayStart与_serviceDispatcherStopped。4.2 OnStop / OnShutdown优雅停机当 SCM 发出 STOP服务停止或系统关机SHUTDOWN命令时生命周期需要把服务停止翻译为宿主停止protected override void OnStop() { _serviceStopRequested true; ApplicationLifetime.StopApplication(); // 等待宿主完全关闭后再把服务标记为已停止。 _delayStop.Wait(_hostOptions.ShutdownTimeout); base.OnStop(); }OnStop与OnShutdown逻辑一致调用ApplicationLifetime.StopApplication()触发ApplicationStopping事件宿主随后按逆序停止各IHostedService通过_delayStopManualResetEventSlim阻塞等待该信号量在ApplicationStopped事件中由_delayStop.Set()置位等待时间受HostOptions.ShutdownTimeout约束默认 30 秒超时后仍会返回因为方法返回后服务即被 SCM 标记为 Stopped进程可能随时退出。4.3 StopAsync宿主侧发起的停止StopAsync供IHost.StopAsync调用例如服务自身决定退出public async Task StopAsync(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if (!_serviceStopRequested) { await Task.Run(Stop, cancellationToken).ConfigureAwait(false); } // 底层服务停止后ServiceBase.Run 会返回从而完成 _serviceDispatcherStopped。 await _serviceDispatcherStopped.Task.ConfigureAwait(false); }若尚未收到 SCM 的停止命令则主动调用ServiceBase.Stop()通知 SCM随后等待_serviceDispatcherStopped完成——该任务在Run()方法返回即服务分发循环结束时置位保证进程在真正可退出之前不会提前结束。4.4 生命周期事件顺序测试给出的权威证据测试 WindowsServiceLifetimeTests.cs 中的ServiceSequenceIsCorrect用例通过文件日志完整记录了服务从启动到停止的事件顺序host.Run() WindowsServiceLifetime.OnStart BackgroundService.StartAsync lifetime started WindowsServiceLifetime.OnStop lifetime stopping BackgroundService.StopAsync lifetime stopped host.Run() complete而ServiceCanStopItself用例由服务内部调用host.StopAsync()触发的顺序为host.Start() WindowsServiceLifetime.OnStart BackgroundService.StartAsync lifetime started host.Stop() lifetime stopping BackgroundService.StopAsync lifetime stopped WindowsServiceLifetime.OnStop host.Stop() complete两段日志分别印证了SCM 驱动停止与服务自停止两条路径下IHostApplicationLifetimeApplicationStarted/ApplicationStopping/ApplicationStopped与WindowsServiceLifetimeOnStart/OnStop的严格先后关系先触发应用生命周期事件宿主完成清理后再返回 SCM 停止流程。4.5 异常与取消语义同一测试文件还覆盖了边界情形ExceptionOnStartIsPropagatedOnStart抛出的异常会沿WaitForStartAsync传播给调用方服务最终以ERROR_EXCEPTION_IN_SERVICEWin32 错误码退出ExceptionOnStopIsPropagatedOnStop抛出异常时服务以ERROR_PROCESS_ABORTED退出CancelStopAsync向StopAsync传入已取消的CancellationToken会抛出OperationCanceledException服务以ERROR_PROCESS_ABORTED退出。五、自动事件日志集成UseWindowsService生效后除了替换生命周期还会自动启用事件日志记录详见AddWindowsServiceLifetime私有方法services.AddLogging(logging { logging.AddEventLog(); }); services.AddSingletonIHostLifetime, WindowsServiceLifetime(); services.AddSingletonIConfigureOptionsEventLogSettings, EventLogSettingsSetup(); services.Configure(configure);其中内嵌的EventLogSettingsSetup实现了IConfigureOptionsEventLogSettingspublic void Configure(EventLogSettings settings) { if (string.IsNullOrEmpty(settings.SourceName)) { settings.SourceName _applicationName; } }即若未显式配置事件日志源名称则以宿主环境IHostEnvironment.ApplicationName作为默认的EventLogSettings.SourceName这样服务产生的日志会统一归入以应用名命名的事件日志源便于运维检索。测试ServiceCollectionExtensionMethodSetsEventLogSourceNameToApplicationNameInsideOfService对该行为做了断言构造时传入的ApplicationName最终与EventLogSettings.SourceName相同。在非服务环境下这些注册不会发生也就不会产生事件日志写入测试ServiceCollectionExtensionMethodDefaultsToOffOutsideOfService与ServiceCollectionExtensionMethodCanBeCalledOnDefaultConfiguration分别验证了非服务环境不注册与服务环境注册WindowsServiceLifetime两种分支。六、从源码结构看整体设计从源码结构看该库的模块划分非常清晰全部位于 src/libraries/Microsoft.Extensions.Hosting.WindowsServices/ 下文件职责src/WindowsServiceLifetimeHostBuilderExtensions.cs对外扩展方法UseWindowsService/AddWindowsService与依赖注册src/WindowsServiceLifetime.cs生命周期实现桥接 SCM 与宿主src/WindowsServiceHelpers.cs上下文检测判断当前进程是否由 SCM 托管src/WindowsServiceLifetimeOptions.cs服务名配置src/Internal/Win32.cs基于 Toolhelp API 的父进程枚举tests/UseWindowsServiceTests.cs扩展方法与依赖注入行为测试tests/WindowsServiceLifetimeTests.cs生命周期、异常、事件顺序测试依赖真实 Windows 服务需要特权进程值得注意的设计决策跨平台安全平台检查、父进程检查与SupportedOSPlatform(windows)/SupportedOSPlatformGuard(windows)特性配合确保库在非 Windows 平台被引用时行为正确且可通过平台兼容性分析analyzers校验。低耦合接入一切以 DI 注册为界——宿主只依赖IHostLifetime抽象Windows 服务细节被封装在ServiceBase派生类内部因此替换/扩展生命周期测试中的LoggingWindowsServiceLifetime、ThrowingWindowsServiceLifetime即演示了子类化扩展非常容易。与 Visual Studio 模板兼容PACKAGE.md 明确以 Worker Service 模板为起点UseWindowsService一行即可完成适配。七、适用前提与限制平台限制Windows 服务能力仅在 Windows 上可用跨平台场景请使用 systemdLinux等平台对应的托管方案。测试环境仓库中的集成测试如CanCreateService、ServiceStops依赖RemoteExecutor.IsSupported且需要特权进程PlatformDetection.IsPrivilegedProcess在真实 Windows 服务环境中运行普通 CI 或非管理员环境不会执行这些用例。版本绑定该库随 .NET 运行时仓库同步演进README 亦说明其API 与功能已成熟但偶尔会扩展Contribution Bar 允许新特性、新 API、Bug 修复与性能优化具体 API 以你引用的运行时版本为准。综上Microsoft.Extensions.Hosting.WindowsServices 以极小的 API 面四个扩展方法 一个生命周期类解决了 Windows 服务托管这一高频场景其上下文感知设计让同一份代码无缝兼顾控制台与服务的双模式运行是 .NET 后台服务落地的实用基础设施。【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表