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 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>net461;net6.0-windows</TargetFrameworks>
-    <LangVersion>preview</LangVersion>
+    <TargetFrameworks>net461;net7.0-windows</TargetFrameworks>
+    <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
@@ -15,13 +15,13 @@
     </PackageReference>
   </ItemGroup>
 
-  <ItemGroup Condition="'$(TargetFramework)' == 'net6.0-windows'">
+  <ItemGroup Condition="'$(TargetFramework)' == 'net7.0-windows'">
     <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
     <PackageReference Include="System.Security.AccessControl" Version="6.0.0" />
-    <PackageReference Include="System.ServiceProcess.ServiceController" Version="5.0.0" />
+    <PackageReference Include="System.ServiceProcess.ServiceController" Version="7.0.0" />
   </ItemGroup>
 
-  <ItemGroup Condition="'$(TargetFramework)' != 'net6.0-windows'">
+  <ItemGroup Condition="'$(TargetFramework)' != 'net7.0-windows'">
     <PackageReference Include="System.Memory" Version="4.5.5" />
     <PackageReference Include="System.ValueTuple" Version="4.5.0" />
     <Reference Include="System.ServiceProcess" />
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 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>net461;net6.0-windows</TargetFrameworks>
+    <TargetFrameworks>net461;net7.0-windows</TargetFrameworks>
     <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
   </PropertyGroup>
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 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.2.0" />
+    <PackageReference Include="Microsoft.Build.Utilities.Core" Version="17.4.0" />
     <PackageReference Include="Mono.Cecil" Version="0.11.4" />
   </ItemGroup>
 
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);
         }
 
         /// <summary>
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 @@
 <Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
-    <TargetFrameworks>net471;net6.0-windows</TargetFrameworks>
+    <TargetFrameworks>net471;net7.0-windows</TargetFrameworks>
     <LangVersion>latest</LangVersion>
   </PropertyGroup>
 
@@ -12,15 +12,15 @@
     </PackageReference>
     <PackageReference Include="Microsoft.Diagnostics.Runtime" Version="2.0.226801" />
     <PackageReference Include="Microsoft.Diagnostics.Runtime.Utilities" Version="2.0.0-rc.20303.3" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.0" />
-    <PackageReference Include="xunit" Version="2.4.1" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
+    <PackageReference Include="xunit" Version="2.4.2" />
     <PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
       <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
       <PrivateAssets>all</PrivateAssets>
     </PackageReference>
   </ItemGroup>
 
-  <ItemGroup Condition="'$(TargetFramework)' != 'net6.0-windows'">
+  <ItemGroup Condition="'$(TargetFramework)' != 'net7.0-windows'">
     <PackageReference Include="System.Reflection.Metadata" Version="5.0.0" />
     <Reference Include="System.ServiceProcess" />
   </ItemGroup>
@@ -39,11 +39,11 @@
 
   <Target Name="Copy" BeforeTargets="AfterBuild">
 
-    <ItemGroup Condition="'$(TargetFramework)' == 'net6.0-windows'">
-      <_FilesToCopy Include="$(ArtifactsBinDir)WinSW\$(Configuration)\net6.0-windows\WinSW.runtimeconfig*.json" />
+    <ItemGroup Condition="'$(TargetFramework)' == 'net7.0-windows'">
+      <_FilesToCopy Include="$(ArtifactsBinDir)WinSW\$(Configuration)\net7.0-windows\WinSW.runtimeconfig*.json" />
     </ItemGroup>
 
-    <ItemGroup Condition="'$(TargetFramework)' != 'net6.0-windows'">
+    <ItemGroup Condition="'$(TargetFramework)' != 'net7.0-windows'">
       <_FilesToCopy Include="$(ArtifactsBinDir)WinSW\$(Configuration)\net461\System.ValueTuple.dll" />
     </ItemGroup>
 
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<T>(this Command command, Action<T, InvocationContext> handle, Argument<T> symbol)
+        {
+            command.SetHandler(context =>
+            {
+                var value = context.ParseResult.GetValueForArgument(symbol);
+                handle(value!, context);
+            });
+        }
+
+        internal static void SetHandler<T1, T2, T3>(this Command command, Action<T1, T2, T3, InvocationContext> handle, Argument<T1> symbol1, Option<T2> symbol2, Option<T3> 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<T1, T2, T3, T4>(this Command command, Action<T1, T2, T3, T4, InvocationContext> handle, Argument<T1> symbol1, Option<T2> symbol2, Option<T3> symbol3, Option<T4> 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<string?>("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<string?>("path-to-config")
-                    {
-                        Arity = ArgumentArity.ZeroOrOne,
-                        IsHidden = true,
-                    });
+                    root.Add(serviceConfig);
                 }
             }
 
+            root.SetHandler(Run, serviceConfig);
+
             var config = new Argument<string?>("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<bool>("--no-elevate", "Doesn't automatically trigger a UAC prompt.");
 
             {
+                var username = new Option<string?>(new[] { "--username", "--user" }, "Specifies the user name of the service account.");
+                var password = new Option<string?>(new[] { "--password", "--pass" }, "Specifies the password of the service account.");
+
                 var install = new Command("install", "Installs the service.")
                 {
-                    Handler = CommandHandler.Create<string?, bool, string?, string?>(Install),
+                    config,
+                    noElevate,
+                    username,
+                    password,
                 };
-
-                install.Add(config);
-                install.Add(noElevate);
-                install.Add(new Option<string?>(new[] { "--username", "--user" }, "Specifies the user name of the service account."));
-                install.Add(new Option<string?>(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<string?, bool>(Uninstall),
+                    config,
+                    noElevate,
                 };
-
-                uninstall.Add(config);
-                uninstall.Add(noElevate);
+                uninstall.SetHandler(Uninstall, config, noElevate);
 
                 root.Add(uninstall);
             }
 
             {
+                var noWait = new Option<bool>("--no-wait", "Doesn't wait for the service to actually start.");
+
                 var start = new Command("start", "Starts the service.")
                 {
-                    Handler = CommandHandler.Create<string?, bool, bool, CancellationToken>(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<bool>("--no-wait", "Doesn't wait for the service to actually stop.");
+                var force = new Option<bool>("--force", "Stops the service even if it has started dependent services.");
+
                 var stop = new Command("stop", "Stops the service.")
                 {
-                    Handler = CommandHandler.Create<string?, bool, bool, bool, CancellationToken>(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<bool>("--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<string?, bool, bool, CancellationToken>(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<string?>(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<string?>(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<string?, bool>(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<string>(new[] { "--output", "-o" })
                 {
-                    Handler = CommandHandler.Create<string, string>(Customize),
+                    IsRequired = true,
                 };
 
-                customize.Add(new Option<string>(new[] { "--output", "-o" })
-                {
-                    Required = true,
-                });
-
                 var manufacturer = new Option<string>("--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<bool>(new[] { "--all", "-a" });
+
                     var ps = new Command("ps", "Draws the process tree associated with the service.")
                     {
-                        Handler = CommandHandler.Create<string?, bool>(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<string?, bool>(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 @@
 
   <PropertyGroup>
     <OutputType>Exe</OutputType>
-    <TargetFrameworks>net461;net6.0-windows</TargetFrameworks>
-    <LangVersion>preview</LangVersion>
+    <TargetFrameworks>net461;net7.0-windows</TargetFrameworks>
+    <LangVersion>latest</LangVersion>
     <Nullable>enable</Nullable>
     <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
-    <PublishTrimmed>true</PublishTrimmed>
 
     <AssemblyTitle>Windows Service Wrapper</AssemblyTitle>
     <Description>Allows arbitrary process to run as a Windows service by wrapping it.</Description>
@@ -15,19 +14,27 @@
     <Copyright>Copyright (c) 2008-2020 Kohsuke Kawaguchi, Sun Microsystems, Inc., CloudBees, Inc., Oleg Nenashev and other contributors</Copyright>
   </PropertyGroup>
 
-  <PropertyGroup Condition="'$(TargetFramework)' == 'net6.0-windows' AND '$(RuntimeIdentifier)' != ''">
+  <PropertyGroup>
+    <PublishTrimmed>true</PublishTrimmed>
+    <TrimMode>partial</TrimMode>
+    <DebuggerSupport>false</DebuggerSupport>
+    <NullabilityInfoContextSupport>false</NullabilityInfoContextSupport>
+    <_AggressiveAttributeTrimming>true</_AggressiveAttributeTrimming>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(TargetFramework)' == 'net7.0-windows' AND '$(RuntimeIdentifier)' != ''">
     <PublishSingleFile>true</PublishSingleFile>
   </PropertyGroup>
 
-  <PropertyGroup Condition="'$(TargetFramework)' != 'net6.0-windows'">
+  <PropertyGroup Condition="'$(TargetFramework)' != 'net7.0-windows'">
     <ILMergeVersion>3.0.41</ILMergeVersion>
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20303.1" />
+    <PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
   </ItemGroup>
 
-  <ItemGroup Condition="'$(TargetFramework)' != 'net6.0-windows'">
+  <ItemGroup Condition="'$(TargetFramework)' != 'net7.0-windows'">
     <PackageReference Include="ilmerge" Version="$(ILMergeVersion)" />
     <Reference Include="System.ServiceProcess" />
   </ItemGroup>
@@ -37,11 +44,11 @@
     <ProjectReference Include="..\WinSW.Plugins\WinSW.Plugins.csproj" />
   </ItemGroup>
 
-  <ItemGroup Condition="'$(TargetFramework)' != 'net6.0-windows'">
+  <ItemGroup Condition="'$(TargetFramework)' != 'net7.0-windows'">
     <ProjectReference Include="..\WinSW.Tasks\WinSW.Tasks.csproj" ReferenceOutputAssembly="false" />
   </ItemGroup>
 
-  <Target Name="PublishCoreExe" AfterTargets="Publish" Condition="'$(TargetFramework)' == 'net6.0-windows'">
+  <Target Name="PublishCoreExe" AfterTargets="Publish" Condition="'$(TargetFramework)' == 'net7.0-windows'">
 
     <MakeDir Directories="$(ArtifactsPublishDir)" />
     <Copy SourceFiles="$(PublishDir)$(TargetName).exe" DestinationFiles="$(ArtifactsPublishDir)WinSW-$(PlatformTarget).exe" />
@@ -49,7 +56,7 @@
   </Target>
 
   <!-- Merge plugins and other DLLs into the executable -->
-  <Target Name="Merge" BeforeTargets="AfterBuild" Condition="'$(TargetFramework)' != 'net6.0-windows'">
+  <Target Name="Merge" BeforeTargets="AfterBuild" Condition="'$(TargetFramework)' != 'net7.0-windows'">
 
     <PropertyGroup>
       <InputAssemblies>"$(OutDir)$(TargetFileName)"</InputAssemblies>
@@ -77,7 +84,7 @@
   </Target>
 
   <UsingTask TaskName="WinSW.Tasks.Trim" AssemblyFile="$(ArtifactsBinDir)WinSW.Tasks\$(Configuration)\net461\WinSW.Tasks.dll" />
-  <Target Name="Trim" AfterTargets="Merge" Condition="'$(TargetFramework)' != 'net6.0-windows'">
+  <Target Name="Trim" AfterTargets="Merge" Condition="'$(TargetFramework)' != 'net7.0-windows'">
     <Trim Path="$(ArtifactsPublishDir)WinSW-$(TargetFramework).exe" />
   </Target>