From a6ba41681d84d84d95eb7a377c369d709e32225b Mon Sep 17 00:00:00 2001 From: Next Turn <45985406+nxtn@users.noreply.github.com> Date: Sun, 29 Jan 2023 00:27:45 +0800 Subject: [PATCH] Upgrade to .NET 7 (#1001) --- eng/build.yml | 13 +- src/WinSW.Core/AssemblyInfo.cs | 4 +- src/WinSW.Core/WinSW.Core.csproj | 10 +- src/WinSW.Core/WrapperService.cs | 1 - src/WinSW.Plugins/WinSW.Plugins.csproj | 2 +- src/WinSW.Tasks/Trim.cs | 11 +- src/WinSW.Tasks/WinSW.Tasks.csproj | 2 +- src/WinSW.Tests/CommandLineTests.cs | 2 +- src/WinSW.Tests/WinSW.Tests.csproj | 14 +- src/WinSW/AssemblyInfo.cs | 4 +- src/WinSW/CommandExtensions.cs | 41 +++++ src/WinSW/Program.cs | 222 +++++++++++++------------ src/WinSW/WinSW.csproj | 29 ++-- 13 files changed, 206 insertions(+), 149 deletions(-) create mode 100644 src/WinSW/CommandExtensions.cs diff --git a/eng/build.yml b/eng/build.yml index a7417d3..0727edf 100644 --- a/eng/build.yml +++ b/eng/build.yml @@ -25,11 +25,6 @@ strategy: Release: BuildConfiguration: Release steps: -- task: UseDotNet@2 - displayName: Install .NET SDK - inputs: - packageType: sdk - version: 6.x - task: DotNetCoreCLI@2 displayName: Build inputs: @@ -37,9 +32,9 @@ steps: projects: src\WinSW.sln arguments: -c $(BuildConfiguration) -p:Version=$(BuildVersion) - script: | - dotnet publish src\WinSW\WinSW.csproj -c $(BuildConfiguration) -f net6.0-windows -r win-x64 -p:Version=$(BuildVersion) - dotnet publish src\WinSW\WinSW.csproj -c $(BuildConfiguration) -f net6.0-windows -r win-x86 -p:Version=$(BuildVersion) - dotnet publish src\WinSW\WinSW.csproj -c $(BuildConfiguration) -f net6.0-windows -r win-arm64 -p:Version=$(BuildVersion) + dotnet publish src\WinSW\WinSW.csproj -c $(BuildConfiguration) -f net7.0-windows -r win-x64 --sc -p:Version=$(BuildVersion) + dotnet publish src\WinSW\WinSW.csproj -c $(BuildConfiguration) -f net7.0-windows -r win-x86 --sc -p:Version=$(BuildVersion) + dotnet publish src\WinSW\WinSW.csproj -c $(BuildConfiguration) -f net7.0-windows -r win-arm64 --sc -p:Version=$(BuildVersion) displayName: Build - task: DotNetCoreCLI@2 displayName: Test @@ -73,7 +68,7 @@ steps: - publish: artifacts\publish\WinSW-arm64.exe artifact: WinSW-arm64.exe_$(BuildConfiguration) - displayName: Publish .NET arm64 .exe + displayName: Publish .NET Arm64 .exe - publish: $(Build.ArtifactStagingDirectory)\WinSW.$(BuildVersion).nupkg artifact: WinSW.nupkg_$(BuildConfiguration) diff --git a/src/WinSW.Core/AssemblyInfo.cs b/src/WinSW.Core/AssemblyInfo.cs index 3b0a125..7bd9500 100644 --- a/src/WinSW.Core/AssemblyInfo.cs +++ b/src/WinSW.Core/AssemblyInfo.cs @@ -1,4 +1,6 @@ -using System.Runtime.CompilerServices; +using System.Reflection; +using System.Runtime.CompilerServices; +[assembly: AssemblyMetadata("IsTrimmable", "True")] [assembly: InternalsVisibleTo("WinSW")] [assembly: InternalsVisibleTo("WinSW.Tests")] diff --git a/src/WinSW.Core/WinSW.Core.csproj b/src/WinSW.Core/WinSW.Core.csproj index e2caa00..768022c 100644 --- a/src/WinSW.Core/WinSW.Core.csproj +++ b/src/WinSW.Core/WinSW.Core.csproj @@ -1,8 +1,8 @@  - net461;net6.0-windows - preview + net461;net7.0-windows + latest enable true @@ -15,13 +15,13 @@ - + - + - + diff --git a/src/WinSW.Core/WrapperService.cs b/src/WinSW.Core/WrapperService.cs index 7a97721..33bf83a 100644 --- a/src/WinSW.Core/WrapperService.cs +++ b/src/WinSW.Core/WrapperService.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; diff --git a/src/WinSW.Plugins/WinSW.Plugins.csproj b/src/WinSW.Plugins/WinSW.Plugins.csproj index 3fdb8be..9971e51 100644 --- a/src/WinSW.Plugins/WinSW.Plugins.csproj +++ b/src/WinSW.Plugins/WinSW.Plugins.csproj @@ -1,7 +1,7 @@  - net461;net6.0-windows + net461;net7.0-windows latest enable diff --git a/src/WinSW.Tasks/Trim.cs b/src/WinSW.Tasks/Trim.cs index 1895189..62883ab 100644 --- a/src/WinSW.Tasks/Trim.cs +++ b/src/WinSW.Tasks/Trim.cs @@ -17,13 +17,18 @@ namespace WinSW.Tasks { using var module = ModuleDefinition.ReadModule(this.Path, new() { ReadWrite = true, ReadSymbols = true }); + foreach (var t in module.CustomAttributeTypes()) + { + this.WalkType(t); + } + this.WalkType(module.EntryPoint.DeclaringType); var types = module.Types; for (int i = types.Count - 1; i >= 0; i--) { var type = types[i]; - if (type.FullName.Contains("WinSW.Plugins")) + if (type.FullName.StartsWith("WinSW.Plugins")) { this.WalkType(type); } @@ -64,6 +69,10 @@ namespace WinSW.Tasks this.WalkType(genericArg); } } + else if (typeRef is IModifierType modifierType) + { + this.WalkType(modifierType.ModifierType); + } return; } diff --git a/src/WinSW.Tasks/WinSW.Tasks.csproj b/src/WinSW.Tasks/WinSW.Tasks.csproj index 874eb83..d19e91d 100644 --- a/src/WinSW.Tasks/WinSW.Tasks.csproj +++ b/src/WinSW.Tasks/WinSW.Tasks.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/WinSW.Tests/CommandLineTests.cs b/src/WinSW.Tests/CommandLineTests.cs index fa7eff3..3c711b2 100644 --- a/src/WinSW.Tests/CommandLineTests.cs +++ b/src/WinSW.Tests/CommandLineTests.cs @@ -77,7 +77,7 @@ namespace WinSW.Tests var result = Helper.ErrorTest(new[] { commandName }); - Assert.Equal($"Unrecognized command or argument '{commandName}'\r\n\r\n", result.Error); + Assert.Equal($"Unrecognized command or argument '{commandName}'.\r\n\r\n", result.Error); } /// diff --git a/src/WinSW.Tests/WinSW.Tests.csproj b/src/WinSW.Tests/WinSW.Tests.csproj index a3a88fc..6288ec4 100644 --- a/src/WinSW.Tests/WinSW.Tests.csproj +++ b/src/WinSW.Tests/WinSW.Tests.csproj @@ -1,7 +1,7 @@  - net471;net6.0-windows + net471;net7.0-windows latest @@ -12,15 +12,15 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + @@ -39,11 +39,11 @@ - - <_FilesToCopy Include="$(ArtifactsBinDir)WinSW\$(Configuration)\net6.0-windows\WinSW.runtimeconfig*.json" /> + + <_FilesToCopy Include="$(ArtifactsBinDir)WinSW\$(Configuration)\net7.0-windows\WinSW.runtimeconfig*.json" /> - + <_FilesToCopy Include="$(ArtifactsBinDir)WinSW\$(Configuration)\net461\System.ValueTuple.dll" /> diff --git a/src/WinSW/AssemblyInfo.cs b/src/WinSW/AssemblyInfo.cs index 790f54e..3916c1f 100644 --- a/src/WinSW/AssemblyInfo.cs +++ b/src/WinSW/AssemblyInfo.cs @@ -1,3 +1,5 @@ -using System.Runtime.CompilerServices; +using System.Reflection; +using System.Runtime.CompilerServices; +[assembly: AssemblyMetadata("IsTrimmable", "True")] [assembly: InternalsVisibleTo("WinSW.Tests")] diff --git a/src/WinSW/CommandExtensions.cs b/src/WinSW/CommandExtensions.cs new file mode 100644 index 0000000..f5537fa --- /dev/null +++ b/src/WinSW/CommandExtensions.cs @@ -0,0 +1,41 @@ +using System; +using System.CommandLine; +using System.CommandLine.Invocation; + +namespace WinSW +{ + internal static class CommandExtensions + { + internal static void SetHandler(this Command command, Action handle, Argument symbol) + { + command.SetHandler(context => + { + var value = context.ParseResult.GetValueForArgument(symbol); + handle(value!, context); + }); + } + + internal static void SetHandler(this Command command, Action handle, Argument symbol1, Option symbol2, Option symbol3) + { + command.SetHandler(context => + { + var value1 = context.ParseResult.GetValueForArgument(symbol1); + var value2 = context.ParseResult.GetValueForOption(symbol2); + var value3 = context.ParseResult.GetValueForOption(symbol3); + handle(value1!, value2!, value3!, context); + }); + } + + internal static void SetHandler(this Command command, Action handle, Argument symbol1, Option symbol2, Option symbol3, Option symbol4) + { + command.SetHandler(context => + { + var value1 = context.ParseResult.GetValueForArgument(symbol1); + var value2 = context.ParseResult.GetValueForOption(symbol2); + var value3 = context.ParseResult.GetValueForOption(symbol3); + var value4 = context.ParseResult.GetValueForOption(symbol4); + handle(value1!, value2!, value3!, value4!, context); + }); + } + } +} diff --git a/src/WinSW/Program.cs b/src/WinSW/Program.cs index f085eb0..ecc203f 100644 --- a/src/WinSW/Program.cs +++ b/src/WinSW/Program.cs @@ -13,7 +13,6 @@ using System.Reflection; using System.Security.AccessControl; using System.Security.Principal; using System.ServiceProcess; -using System.Threading; using log4net; using log4net.Appender; using log4net.Config; @@ -107,36 +106,14 @@ namespace WinSW elevated = IsProcessElevated(); } - var root = new RootCommand("A wrapper binary that can be used to host executables as Windows services. https://github.com/winsw/winsw") + var serviceConfig = new Argument("path-to-config") { - Handler = CommandHandler.Create((string? pathToConfig) => - { - XmlServiceConfig config = null!; - try - { - config = LoadConfigAndInitLoggers(pathToConfig, false); - } - catch (FileNotFoundException) - { - Throw.Command.Exception("The specified command or file was not found."); - } - - Log.Debug("Starting WinSW in service mode."); - - AutoRefresh(config); - - using var service = new WrapperService(config); - try - { - ServiceBase.Run(service); - } - catch - { - // handled in OnStart - } - }), + Arity = ArgumentArity.ZeroOrOne, + IsHidden = true, }; + var root = new RootCommand("A wrapper binary that can be used to host executables as Windows services. https://github.com/winsw/winsw"); + using (var identity = WindowsIdentity.GetCurrent()) { var principal = new WindowsPrincipal(identity); @@ -145,31 +122,31 @@ namespace WinSW principal.IsInRole(new SecurityIdentifier(WellKnownSidType.LocalServiceSid, null)) || principal.IsInRole(new SecurityIdentifier(WellKnownSidType.NetworkServiceSid, null))) { - root.Add(new Argument("path-to-config") - { - Arity = ArgumentArity.ZeroOrOne, - IsHidden = true, - }); + root.Add(serviceConfig); } } + root.SetHandler(Run, serviceConfig); + var config = new Argument("path-to-config", "The path to the configuration file.") { Arity = ArgumentArity.ZeroOrOne, }; - var noElevate = new Option("--no-elevate", "Doesn't automatically trigger a UAC prompt."); + var noElevate = new Option("--no-elevate", "Doesn't automatically trigger a UAC prompt."); { + var username = new Option(new[] { "--username", "--user" }, "Specifies the user name of the service account."); + var password = new Option(new[] { "--password", "--pass" }, "Specifies the password of the service account."); + var install = new Command("install", "Installs the service.") { - Handler = CommandHandler.Create(Install), + config, + noElevate, + username, + password, }; - - install.Add(config); - install.Add(noElevate); - install.Add(new Option(new[] { "--username", "--user" }, "Specifies the user name of the service account.")); - install.Add(new Option(new[] { "--password", "--pass" }, "Specifies the password of the service account.")); + install.SetHandler(Install, config, noElevate, username, password); root.Add(install); } @@ -177,51 +154,54 @@ namespace WinSW { var uninstall = new Command("uninstall", "Uninstalls the service.") { - Handler = CommandHandler.Create(Uninstall), + config, + noElevate, }; - - uninstall.Add(config); - uninstall.Add(noElevate); + uninstall.SetHandler(Uninstall, config, noElevate); root.Add(uninstall); } { + var noWait = new Option("--no-wait", "Doesn't wait for the service to actually start."); + var start = new Command("start", "Starts the service.") { - Handler = CommandHandler.Create(Start), + config, + noElevate, + noWait, }; - - start.Add(config); - start.Add(noElevate); - start.Add(new Option("--no-wait", "Doesn't wait for the service to actually start.")); + start.SetHandler(Start, config, noElevate, noWait); root.Add(start); } { + var noWait = new Option("--no-wait", "Doesn't wait for the service to actually stop."); + var force = new Option("--force", "Stops the service even if it has started dependent services."); + var stop = new Command("stop", "Stops the service.") { - Handler = CommandHandler.Create(Stop), + config, + noElevate, + noWait, + force, }; - - stop.Add(config); - stop.Add(noElevate); - stop.Add(new Option("--no-wait", "Doesn't wait for the service to actually stop.")); - stop.Add(new Option("--force", "Stops the service even if it has started dependent services.")); + stop.SetHandler(Stop, config, noElevate, noWait, force); root.Add(stop); } { + var force = new Option("--force", "Restarts the service even if it has started dependent services."); + var restart = new Command("restart", "Stops and then starts the service.") { - Handler = CommandHandler.Create(Restart), + config, + noElevate, + force, }; - - restart.Add(config); - restart.Add(noElevate); - restart.Add(new Option("--force", "Restarts the service even if it has started dependent services.")); + restart.SetHandler(Restart, config, noElevate, force); root.Add(restart); } @@ -229,10 +209,9 @@ namespace WinSW { var restartSelf = new Command("restart!", "self-restart (can be called from child processes)") { - Handler = CommandHandler.Create(RestartSelf), + config, }; - - restartSelf.Add(config); + restartSelf.SetHandler(RestartSelf, config); root.Add(restartSelf); } @@ -240,10 +219,9 @@ namespace WinSW { var status = new Command("status", "Checks the status of the service.") { - Handler = CommandHandler.Create(Status), + config, }; - - status.Add(config); + status.SetHandler(Status, config); root.Add(status); } @@ -251,44 +229,43 @@ namespace WinSW { var refresh = new Command("refresh", "Refreshes the service properties without reinstallation.") { - Handler = CommandHandler.Create(Refresh), + config, + noElevate, }; - - refresh.Add(config); - refresh.Add(noElevate); + refresh.SetHandler(Refresh, config, noElevate); root.Add(refresh); } { - var customize = new Command("customize", "Customizes the wrapper executable.") + var output = new Option(new[] { "--output", "-o" }) { - Handler = CommandHandler.Create(Customize), + IsRequired = true, }; - customize.Add(new Option(new[] { "--output", "-o" }) - { - Required = true, - }); - var manufacturer = new Option("--manufacturer") { - Required = true, + IsRequired = true, }; - manufacturer.Argument.AddValidator(argument => + manufacturer.AddValidator(result => { const int minLength = 12; const int maxLength = 15; - string token = argument.Tokens.Single().Value; + string token = result.Tokens.Single().Value; int length = token.Length; - return + result.ErrorMessage = length < minLength ? $"The length of argument '{token}' must be greater than or equal to {minLength}." : length > maxLength ? $"The length of argument '{token}' must be less than or equal to {maxLength}." : null; }); - customize.Add(manufacturer); + var customize = new Command("customize", "Customizes the wrapper executable.") + { + output, + manufacturer, + }; + customize.SetHandler(Customize, output, manufacturer); root.Add(customize); } @@ -299,14 +276,14 @@ namespace WinSW root.Add(dev); { + var all = new Option(new[] { "--all", "-a" }); + var ps = new Command("ps", "Draws the process tree associated with the service.") { - Handler = CommandHandler.Create(DevPs), + config, + all, }; - - ps.Add(config); - - ps.Add(new Option(new[] { "--all", "-a" })); + ps.SetHandler(DevPs, config, all); dev.Add(ps); } @@ -314,20 +291,17 @@ namespace WinSW { var kill = new Command("kill", "Terminates the service if it has stopped responding.") { - Handler = CommandHandler.Create(DevKill), + config, + noElevate, }; - - kill.Add(config); - kill.Add(noElevate); + kill.SetHandler(DevKill, config, noElevate); dev.Add(kill); } { - var list = new Command("list", "Lists services managed by the current executable.") - { - Handler = CommandHandler.Create(DevList), - }; + var list = new Command("list", "Lists services managed by the current executable."); + list.SetHandler(DevList); dev.Add(list); } @@ -346,16 +320,13 @@ namespace WinSW static void OnException(Exception exception, InvocationContext context) { - Debug.Assert(exception is TargetInvocationException); - Debug.Assert(exception.InnerException != null); - exception = exception.InnerException!; switch (exception) { case InvalidDataException e: { string message = "The configuration file could not be loaded. " + e.Message; Log.Fatal(message, e); - context.ResultCode = -1; + context.ExitCode = -1; break; } @@ -363,7 +334,7 @@ namespace WinSW { Debug.Assert(e.CancellationToken == context.GetCancellationToken()); Log.Fatal(e.Message); - context.ResultCode = -1; + context.ExitCode = -1; break; } @@ -371,7 +342,7 @@ namespace WinSW { string message = e.Message; Log.Fatal(message); - context.ResultCode = e.InnerException is Win32Exception inner ? inner.NativeErrorCode : -1; + context.ExitCode = e.InnerException is Win32Exception inner ? inner.NativeErrorCode : -1; break; } @@ -379,7 +350,7 @@ namespace WinSW { string message = e.Message; Log.Fatal(message); - context.ResultCode = inner.NativeErrorCode; + context.ExitCode = inner.NativeErrorCode; break; } @@ -387,19 +358,46 @@ namespace WinSW { string message = e.Message; Log.Fatal(message, e); - context.ResultCode = e.NativeErrorCode; + context.ExitCode = e.NativeErrorCode; break; } default: { Log.Fatal("Unhandled exception", exception); - context.ResultCode = -1; + context.ExitCode = -1; break; } } } + static void Run(string? pathToConfig) + { + XmlServiceConfig config = null!; + try + { + config = LoadConfigAndInitLoggers(pathToConfig, false); + } + catch (FileNotFoundException) + { + Throw.Command.Exception("The specified command or file was not found."); + } + + Log.Debug("Starting WinSW in service mode."); + + AutoRefresh(config); + + using var service = new WrapperService(config); + try + { + ServiceBase.Run(service); + } + catch + { + // handled in OnStart + } + } + void Install(string? pathToConfig, bool noElevate, string? username, string? password) { var config = LoadConfigAndInitLoggers(pathToConfig, true); @@ -557,7 +555,7 @@ namespace WinSW } } - void Start(string? pathToConfig, bool noElevate, bool noWait, CancellationToken ct) + void Start(string? pathToConfig, bool noElevate, bool noWait, InvocationContext context) { var config = LoadConfigAndInitLoggers(pathToConfig, true); @@ -580,6 +578,7 @@ namespace WinSW { try { + var ct = context.GetCancellationToken(); svc.WaitForStatus(ServiceControllerStatus.Running, ServiceControllerStatus.StartPending, ct); } catch (TimeoutException) @@ -602,7 +601,7 @@ namespace WinSW } } - void Stop(string? pathToConfig, bool noElevate, bool noWait, bool force, CancellationToken ct) + void Stop(string? pathToConfig, bool noElevate, bool noWait, bool force, InvocationContext context) { var config = LoadConfigAndInitLoggers(pathToConfig, true); @@ -633,6 +632,7 @@ namespace WinSW { try { + var ct = context.GetCancellationToken(); svc.WaitForStatus(ServiceControllerStatus.Stopped, ServiceControllerStatus.StopPending, ct); } catch (TimeoutException) @@ -655,7 +655,7 @@ namespace WinSW } } - void Restart(string? pathToConfig, bool noElevate, bool force, CancellationToken ct) + void Restart(string? pathToConfig, bool noElevate, bool force, InvocationContext context) { var config = LoadConfigAndInitLoggers(pathToConfig, true); @@ -688,6 +688,7 @@ namespace WinSW try { + var ct = context.GetCancellationToken(); svc.WaitForStatus(ServiceControllerStatus.Stopped, ServiceControllerStatus.StopPending, ct); } catch (TimeoutException) @@ -710,6 +711,7 @@ namespace WinSW try { + var ct = context.GetCancellationToken(); svc.WaitForStatus(ServiceControllerStatus.Running, ServiceControllerStatus.StartPending, ct); } catch (TimeoutException) @@ -763,7 +765,7 @@ namespace WinSW _ = HandleApis.CloseHandle(processInfo.ThreadHandle); } - static int Status(string? pathToConfig) + static void Status(string? pathToConfig, InvocationContext context) { var config = LoadConfigAndInitLoggers(pathToConfig, true); @@ -781,7 +783,7 @@ namespace WinSW _ => "Inactive (stopped)" }); - return svc.Status switch + context.ExitCode = svc.Status switch { ServiceControllerStatus.Stopped => 0, _ => 1 @@ -791,7 +793,7 @@ namespace WinSW when (e.InnerException is Win32Exception inner && inner.NativeErrorCode == Errors.ERROR_SERVICE_DOES_NOT_EXIST) { Console.WriteLine("NonExistent"); - return Errors.ERROR_SERVICE_DOES_NOT_EXIST; + context.ExitCode = Errors.ERROR_SERVICE_DOES_NOT_EXIST; } } diff --git a/src/WinSW/WinSW.csproj b/src/WinSW/WinSW.csproj index 0c3b021..19bdada 100644 --- a/src/WinSW/WinSW.csproj +++ b/src/WinSW/WinSW.csproj @@ -2,11 +2,10 @@ Exe - net461;net6.0-windows - preview + net461;net7.0-windows + latest enable true - true Windows Service Wrapper Allows arbitrary process to run as a Windows service by wrapping it. @@ -15,19 +14,27 @@ Copyright (c) 2008-2020 Kohsuke Kawaguchi, Sun Microsystems, Inc., CloudBees, Inc., Oleg Nenashev and other contributors - + + true + partial + false + false + <_AggressiveAttributeTrimming>true + + + true - + 3.0.41 - + - + @@ -37,11 +44,11 @@ - + - + @@ -49,7 +56,7 @@ - + "$(OutDir)$(TargetFileName)" @@ -77,7 +84,7 @@ - +