1
0
Эх сурвалжийг харах

New command-line interface

NextTurn 5 жил өмнө
parent
commit
3ea68e2d20

+ 1 - 0
src/WinSW.Core/AssemblyInfo.cs

@@ -1,3 +1,4 @@
 using System.Runtime.CompilerServices;
 
 [assembly: InternalsVisibleTo("WinSW")]
+[assembly: InternalsVisibleTo("WinSW.Tests")]

+ 103 - 0
src/WinSW.Core/Native/Credentials.cs

@@ -0,0 +1,103 @@
+using System;
+using System.ComponentModel;
+using System.Runtime.InteropServices;
+using static WinSW.Native.CredentialApis;
+
+namespace WinSW.Native
+{
+    internal static class Credentials
+    {
+        internal static void PropmtForCredentialsDialog(ref string? userName, ref string? password, string caption, string message)
+        {
+            userName ??= string.Empty;
+            password ??= string.Empty;
+
+            int inBufferSize = 0;
+            _ = CredPackAuthenticationBuffer(
+                0,
+                userName,
+                password,
+                IntPtr.Zero,
+                ref inBufferSize);
+
+            IntPtr inBuffer = Marshal.AllocCoTaskMem(inBufferSize);
+            try
+            {
+                if (!CredPackAuthenticationBuffer(
+                    0,
+                    userName,
+                    password,
+                    inBuffer,
+                    ref inBufferSize))
+                {
+                    Throw.Command.Win32Exception("Failed to pack auth buffer.");
+                }
+
+                CREDUI_INFO info = new CREDUI_INFO
+                {
+                    Size = Marshal.SizeOf(typeof(CREDUI_INFO)),
+                    CaptionText = caption,
+                    MessageText = message,
+                };
+                uint authPackage = 0;
+                bool save = false;
+                int error = CredUIPromptForWindowsCredentials(
+                    info,
+                    0,
+                    ref authPackage,
+                    inBuffer,
+                    inBufferSize,
+                    out IntPtr outBuffer,
+                    out uint outBufferSize,
+                    ref save,
+                    CREDUIWIN_GENERIC);
+
+                if (error != Errors.ERROR_SUCCESS)
+                {
+                    throw new Win32Exception(error);
+                }
+
+                try
+                {
+                    int userNameLength = 0;
+                    int passwordLength = 0;
+                    _ = CredUnPackAuthenticationBuffer(
+                        0,
+                        outBuffer,
+                        outBufferSize,
+                        null,
+                        ref userNameLength,
+                        default,
+                        default,
+                        null,
+                        ref passwordLength);
+
+                    userName = userNameLength == 0 ? null : new string('\0', userNameLength - 1);
+                    password = passwordLength == 0 ? null : new string('\0', passwordLength - 1);
+
+                    if (!CredUnPackAuthenticationBuffer(
+                        0,
+                        outBuffer,
+                        outBufferSize,
+                        userName,
+                        ref userNameLength,
+                        default,
+                        default,
+                        password,
+                        ref passwordLength))
+                    {
+                        Throw.Command.Win32Exception("Failed to unpack auth buffer.");
+                    }
+                }
+                finally
+                {
+                    Marshal.FreeCoTaskMem(outBuffer);
+                }
+            }
+            finally
+            {
+                Marshal.FreeCoTaskMem(inBuffer);
+            }
+        }
+    }
+}

+ 27 - 21
src/WinSW.Core/Native/Service.cs

@@ -76,24 +76,6 @@ namespace WinSW.Native
             string? username,
             string? password)
         {
-            int arrayLength = 1;
-            for (int i = 0; i < dependencies.Length; i++)
-            {
-                arrayLength += dependencies[i].Length + 1;
-            }
-
-            StringBuilder? array = null;
-            if (dependencies.Length != 0)
-            {
-                array = new StringBuilder(arrayLength);
-                for (int i = 0; i < dependencies.Length; i++)
-                {
-                    _ = array.Append(dependencies[i]).Append('\0');
-                }
-
-                _ = array.Append('\0');
-            }
-
             IntPtr handle = ServiceApis.CreateService(
                 this.handle,
                 serviceName,
@@ -105,7 +87,7 @@ namespace WinSW.Native
                 executablePath,
                 default,
                 default,
-                array,
+                Service.GetNativeDependencies(dependencies),
                 username,
                 password);
             if (handle == IntPtr.Zero)
@@ -171,6 +153,29 @@ namespace WinSW.Native
             }
         }
 
+        internal static StringBuilder? GetNativeDependencies(string[] dependencies)
+        {
+            int arrayLength = 1;
+            for (int i = 0; i < dependencies.Length; i++)
+            {
+                arrayLength += dependencies[i].Length + 1;
+            }
+
+            StringBuilder? array = null;
+            if (dependencies.Length != 0)
+            {
+                array = new StringBuilder(arrayLength);
+                for (int i = 0; i < dependencies.Length; i++)
+                {
+                    _ = array.Append(dependencies[i]).Append('\0');
+                }
+
+                _ = array.Append('\0');
+            }
+
+            return array;
+        }
+
         /// <exception cref="CommandException" />
         internal void SetStatus(IntPtr statusHandle, ServiceControllerStatus state)
         {
@@ -192,7 +197,8 @@ namespace WinSW.Native
         /// <exception cref="CommandException" />
         internal void ChangeConfig(
             string displayName,
-            ServiceStartMode startMode)
+            ServiceStartMode startMode,
+            string[] dependencies)
         {
             if (!ChangeServiceConfig(
                 this.handle,
@@ -202,7 +208,7 @@ namespace WinSW.Native
                 null,
                 null,
                 IntPtr.Zero,
-                null,
+                GetNativeDependencies(dependencies),
                 null,
                 null,
                 displayName))

+ 49 - 21
src/WinSW.Core/ServiceDescriptor.cs

@@ -21,6 +21,8 @@ namespace WinSW
 
         private readonly Dictionary<string, string> environmentVariables;
 
+        internal static ServiceDescriptor? TestDescriptor;
+
         public static DefaultWinSWSettings Defaults { get; } = new DefaultWinSWSettings();
 
         /// <summary>
@@ -42,38 +44,59 @@ namespace WinSW
 
         public ServiceDescriptor()
         {
-            // find co-located configuration xml. We search up to the ancestor directories to simplify debugging,
-            // as well as trimming off ".vshost" suffix (which is used during debugging)
-            // Get the first parent to go into the recursive loop
-            string p = this.ExecutablePath;
-            string baseName = Path.GetFileNameWithoutExtension(p);
-            if (baseName.EndsWith(".vshost"))
+            string path = this.ExecutablePath;
+            string baseName = Path.GetFileNameWithoutExtension(path);
+            string baseDir = Path.GetDirectoryName(path)!;
+
+            if (!File.Exists(Path.Combine(baseDir, baseName + ".xml")))
             {
-                baseName = baseName.Substring(0, baseName.Length - 7);
+                throw new FileNotFoundException("Unable to locate " + baseName + ".xml file within executable directory");
             }
 
-            DirectoryInfo d = new DirectoryInfo(Path.GetDirectoryName(p));
-            while (true)
+            this.BaseName = baseName;
+            this.BasePath = Path.Combine(baseDir, baseName);
+
+            try
             {
-                if (File.Exists(Path.Combine(d.FullName, baseName + ".xml")))
-                {
-                    break;
-                }
+                this.dom.Load(this.BasePath + ".xml");
+            }
+            catch (XmlException e)
+            {
+                throw new InvalidDataException(e.Message, e);
+            }
 
-                if (d.Parent is null)
-                {
-                    throw new FileNotFoundException("Unable to locate " + baseName + ".xml file within executable directory or any parents");
-                }
+            // register the base directory as environment variable so that future expansions can refer to this.
+            Environment.SetEnvironmentVariable("BASE", baseDir);
 
-                d = d.Parent;
+            // ditto for ID
+            Environment.SetEnvironmentVariable("SERVICE_ID", this.Id);
+
+            // New name
+            Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameExecutablePath, this.ExecutablePath);
+
+            // Also inject system environment variables
+            Environment.SetEnvironmentVariable(WinSWSystem.EnvVarNameServiceId, this.Id);
+
+            this.environmentVariables = this.LoadEnvironmentVariables();
+        }
+
+        /// <exception cref="FileNotFoundException" />
+        public ServiceDescriptor(string path)
+        {
+            if (!File.Exists(path))
+            {
+                throw new FileNotFoundException(null, path);
             }
 
+            string baseName = Path.GetFileNameWithoutExtension(path);
+            string baseDir = Path.GetDirectoryName(Path.GetFullPath(path))!;
+
             this.BaseName = baseName;
-            this.BasePath = Path.Combine(d.FullName, this.BaseName);
+            this.BasePath = Path.Combine(baseDir, baseName);
 
             try
             {
-                this.dom.Load(this.BasePath + ".xml");
+                this.dom.Load(path);
             }
             catch (XmlException e)
             {
@@ -81,7 +104,7 @@ namespace WinSW
             }
 
             // register the base directory as environment variable so that future expansions can refer to this.
-            Environment.SetEnvironmentVariable("BASE", d.FullName);
+            Environment.SetEnvironmentVariable("BASE", baseDir);
 
             // ditto for ID
             Environment.SetEnvironmentVariable("SERVICE_ID", this.Id);
@@ -107,6 +130,11 @@ namespace WinSW
             this.environmentVariables = this.LoadEnvironmentVariables();
         }
 
+        internal static ServiceDescriptor Create(string? path)
+        {
+            return path != null ? new ServiceDescriptor(path) : TestDescriptor ?? new ServiceDescriptor();
+        }
+
         public static ServiceDescriptor FromXml(string xml)
         {
             var dom = new XmlDocument();

+ 2 - 2
src/WinSW.Tests/Attributes/ElevatedFactAttribute.cs

@@ -4,9 +4,9 @@ using Xunit;
 namespace WinSW.Tests
 {
     [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
-    internal sealed class ElevatedFactAttribute : FactAttribute
+    public sealed class ElevatedFactAttribute : FactAttribute
     {
-        internal ElevatedFactAttribute()
+        public ElevatedFactAttribute()
         {
             if (!Program.IsProcessElevated())
             {

+ 9 - 33
src/WinSW.Tests/MainTest.cs

@@ -12,10 +12,10 @@ namespace WinSW.Tests
         {
             try
             {
-                _ = CLITestHelper.CLITest(new[] { "install" });
+                _ = CommandLineTestHelper.Test(new[] { "install" });
 
-                using ServiceController controller = new ServiceController(CLITestHelper.Id);
-                Assert.Equal(CLITestHelper.Name, controller.DisplayName);
+                using ServiceController controller = new ServiceController(CommandLineTestHelper.Id);
+                Assert.Equal(CommandLineTestHelper.Name, controller.DisplayName);
                 Assert.False(controller.CanStop);
                 Assert.False(controller.CanShutdown);
                 Assert.False(controller.CanPauseAndContinue);
@@ -24,42 +24,18 @@ namespace WinSW.Tests
             }
             finally
             {
-                _ = CLITestHelper.CLITest(new[] { "uninstall" });
+                _ = CommandLineTestHelper.Test(new[] { "uninstall" });
             }
         }
 
         [Fact]
-        public void PrintVersion()
+        public void FailOnUnknownCommand()
         {
-            string expectedVersion = WrapperService.Version.ToString();
-            string cliOut = CLITestHelper.CLITest(new[] { "version" });
-            Assert.Contains(expectedVersion, cliOut);
-        }
-
-        [Fact]
-        public void PrintHelp()
-        {
-            string expectedVersion = WrapperService.Version.ToString();
-            string cliOut = CLITestHelper.CLITest(new[] { "help" });
-
-            Assert.Contains(expectedVersion, cliOut);
-            Assert.Contains("start", cliOut);
-            Assert.Contains("help", cliOut);
-            Assert.Contains("version", cliOut);
+            const string commandName = "unknown";
 
-            // TODO: check all commands after the migration of ccommands to enum
-        }
-
-        [Fact]
-        public void FailOnUnsupportedCommand()
-        {
-            const string commandName = "nonExistentCommand";
-            string expectedMessage = "Unknown command: " + commandName;
-            CLITestResult result = CLITestHelper.CLIErrorTest(new[] { commandName });
+            CommandLineTestResult result = CommandLineTestHelper.ErrorTest(new[] { commandName });
 
-            Assert.True(result.HasException);
-            Assert.Contains(expectedMessage, result.Out);
-            Assert.Contains(expectedMessage, result.Exception.Message);
+            Assert.Equal($"Unrecognized command or argument '{commandName}'\r\n\r\n", result.Error);
         }
 
         /// <summary>
@@ -68,7 +44,7 @@ namespace WinSW.Tests
         [Fact]
         public void ShouldNotPrintLogsForStatusCommand()
         {
-            string cliOut = CLITestHelper.CLITest(new[] { "status" });
+            string cliOut = CommandLineTestHelper.Test(new[] { "status" });
             Assert.Equal("NonExistent" + Environment.NewLine, cliOut);
         }
     }

+ 27 - 31
src/WinSW.Tests/Util/CLITestHelper.cs → src/WinSW.Tests/Util/CommandLineTestHelper.cs

@@ -7,7 +7,7 @@ namespace WinSW.Tests.Util
     /// <summary>
     /// Helper for WinSW CLI testing
     /// </summary>
-    public static class CLITestHelper
+    public static class CommandLineTestHelper
     {
         public const string Id = "WinSW.Tests";
         public const string Name = "WinSW Test Service";
@@ -33,28 +33,29 @@ $@"<service>
         /// <param name="descriptor">Optional Service descriptor (will be used for initializationpurposes)</param>
         /// <returns>STDOUT if there's no exceptions</returns>
         /// <exception cref="Exception">Command failure</exception>
-        public static string CLITest(string[] arguments, ServiceDescriptor descriptor = null)
+        public static string Test(string[] arguments, ServiceDescriptor descriptor = null)
         {
             TextWriter tmpOut = Console.Out;
-            TextWriter tmpErr = Console.Error;
+            TextWriter tmpError = Console.Error;
 
             using StringWriter swOut = new StringWriter();
-            using StringWriter swErr = new StringWriter();
+            using StringWriter swError = new StringWriter();
 
             Console.SetOut(swOut);
-            Console.SetError(swErr);
+            Console.SetError(swError);
+            ServiceDescriptor.TestDescriptor = descriptor ?? DefaultServiceDescriptor;
             try
             {
-                Program.Run(arguments, descriptor ?? DefaultServiceDescriptor);
+                _ = Program.Run(arguments);
             }
             finally
             {
                 Console.SetOut(tmpOut);
-                Console.SetError(tmpErr);
+                Console.SetError(tmpError);
+                ServiceDescriptor.TestDescriptor = null;
             }
 
-            Assert.Equal(0, swErr.GetStringBuilder().Length);
-            Console.Write(swOut.ToString());
+            Assert.Equal(string.Empty, swError.ToString());
             return swOut.ToString();
         }
 
@@ -64,49 +65,44 @@ $@"<service>
         /// <param name="arguments">CLI arguments to be passed</param>
         /// <param name="descriptor">Optional Service descriptor (will be used for initializationpurposes)</param>
         /// <returns>Test results</returns>
-        public static CLITestResult CLIErrorTest(string[] arguments, ServiceDescriptor descriptor = null)
+        public static CommandLineTestResult ErrorTest(string[] arguments, ServiceDescriptor descriptor = null)
         {
-            Exception testEx = null;
+            Exception exception = null;
+
             TextWriter tmpOut = Console.Out;
-            TextWriter tmpErr = Console.Error;
+            TextWriter tmpError = Console.Error;
 
             using StringWriter swOut = new StringWriter();
-            using StringWriter swErr = new StringWriter();
+            using StringWriter swError = new StringWriter();
 
             Console.SetOut(swOut);
-            Console.SetError(swErr);
+            Console.SetError(swError);
+            ServiceDescriptor.TestDescriptor = descriptor ?? DefaultServiceDescriptor;
+            Program.TestExceptionHandler = (e, _) => exception = e;
             try
             {
-                Program.Run(arguments, descriptor ?? DefaultServiceDescriptor);
+                _ = Program.Run(arguments);
             }
-            catch (Exception ex)
+            catch (Exception e)
             {
-                testEx = ex;
+                exception = e;
             }
             finally
             {
                 Console.SetOut(tmpOut);
-                Console.SetError(tmpErr);
-            }
-
-            Console.WriteLine("\n>>> Output: ");
-            Console.Write(swOut.ToString());
-            Console.WriteLine("\n>>> Error: ");
-            Console.Write(swErr.ToString());
-            if (testEx != null)
-            {
-                Console.WriteLine("\n>>> Exception: ");
-                Console.WriteLine(testEx);
+                Console.SetError(tmpError);
+                ServiceDescriptor.TestDescriptor = null;
+                Program.TestExceptionHandler = null;
             }
 
-            return new CLITestResult(swOut.ToString(), swErr.ToString(), testEx);
+            return new CommandLineTestResult(swOut.ToString(), swError.ToString(), exception);
         }
     }
 
     /// <summary>
     /// Aggregated test report
     /// </summary>
-    public class CLITestResult
+    public class CommandLineTestResult
     {
         public string Out { get; }
 
@@ -116,7 +112,7 @@ $@"<service>
 
         public bool HasException => this.Exception != null;
 
-        public CLITestResult(string output, string error, Exception exception = null)
+        public CommandLineTestResult(string output, string error, Exception exception = null)
         {
             this.Out = output;
             this.Error = error;

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 411 - 344
src/WinSW/Program.cs


+ 17 - 8
src/WinSW/ServiceControllerExtension.cs

@@ -6,17 +6,26 @@ namespace WinSW
 {
     internal static class ServiceControllerExtension
     {
-        internal static bool TryWaitForStatus(/*this*/ ServiceController serviceController, ServiceControllerStatus desiredStatus, TimeSpan timeout)
+        /// <exception cref="TimeoutException" />
+        internal static void WaitForStatus(this ServiceController serviceController, ServiceControllerStatus desiredStatus, ServiceControllerStatus pendingStatus)
         {
-            try
+            TimeSpan timeout = TimeSpan.FromSeconds(1);
+            for (; ; )
             {
-                serviceController.WaitForStatus(desiredStatus, timeout);
-                return true;
-            }
-            catch (TimeoutException)
-            {
-                return false;
+                try
+                {
+                    serviceController.WaitForStatus(desiredStatus, timeout);
+                    break;
+                }
+                catch (TimeoutException) when (serviceController.Status == desiredStatus || serviceController.Status == pendingStatus)
+                {
+                }
             }
         }
+
+        internal static bool HasAnyStartedDependentService(this ServiceController serviceController)
+        {
+            return Array.Exists(serviceController.DependentServices, service => service.Status != ServiceControllerStatus.Stopped);
+        }
     }
 }

+ 0 - 17
src/WinSW/UserException.cs

@@ -1,17 +0,0 @@
-using System;
-
-namespace WinSW
-{
-    internal sealed class UserException : Exception
-    {
-        internal UserException(string message)
-            : base(message)
-        {
-        }
-
-        internal UserException(string? message, Exception inner)
-            : base(message, inner)
-        {
-        }
-    }
-}

+ 5 - 0
src/WinSW/WinSW.csproj

@@ -19,6 +19,10 @@
     <ILMergeVersion>3.0.40</ILMergeVersion>
   </PropertyGroup>
 
+  <ItemGroup>
+    <PackageReference Include="System.CommandLine" Version="2.0.0-beta1.20303.1" />
+  </ItemGroup>
+
   <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
     <PackageReference Include="System.ServiceProcess.ServiceController" Version="4.7.0" />
   </ItemGroup>
@@ -59,6 +63,7 @@
       <InputAssemblies>$(InputAssemblies) "$(OutDir)WinSW.Core.dll"</InputAssemblies>
       <InputAssemblies>$(InputAssemblies) "$(OutDir)WinSW.Plugins.dll"</InputAssemblies>
       <InputAssemblies>$(InputAssemblies) "$(OutDir)log4net.dll"</InputAssemblies>
+      <InputAssemblies>$(InputAssemblies) "$(OutDir)System.CommandLine.dll"</InputAssemblies>
       <OutputAssembly>"$(ArtifactsDir)WinSW.$(TargetFrameworkSuffix).exe"</OutputAssembly>
     </PropertyGroup>
 

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно