Browse Source

Add "version" and "help", handle unsupported commands (PR #81)

Changes summary:
* Add "version" and "help" commands
* Return error on the unsupported command
* Add CLITestHelper to simplify the testing of the service CLI
* Add unit-tests for the new functionality

Resolves #78 and #46
Related to #80

Signed-off-by: Oleg Nenashev <[email protected]>
Oleg Nenashev 10 years ago
parent
commit
3ae6ffaa47

+ 82 - 5
Main.cs

@@ -10,6 +10,7 @@ using System.Threading;
 using Microsoft.Win32;
 using WMI;
 using ServiceType = WMI.ServiceType;
+using System.Reflection;
 
 namespace winsw
 {
@@ -28,9 +29,20 @@ namespace winsw
         private bool _orderlyShutdown;
         private bool _systemShuttingdown;
 
-        public WrapperService()
+        /// <summary>
+        /// Version of Windows service wrapper
+        /// </summary>
+        /// <remarks>
+        /// The version will be taken from <see cref="AssemblyInfo"/>
+        /// </remarks>
+        public static Version Version
+        {
+            get { return Assembly.GetExecutingAssembly().GetName().Version; }
+        }
+
+        public WrapperService(ServiceDescriptor descriptor)
         {
-            _descriptor = new ServiceDescriptor();
+            _descriptor = descriptor;
             ServiceName = _descriptor.Id;
             CanShutdown = true;
             CanStop = true;
@@ -39,6 +51,10 @@ namespace winsw
             _systemShuttingdown = false;
         }
 
+        public WrapperService() : this (new ServiceDescriptor())
+        {          
+        }
+
         /// <summary>
         /// Process the file copy instructions, so that we can replace files that are always in use while
         /// the service runs.
@@ -511,11 +527,11 @@ namespace winsw
         }
 
         // ReSharper disable once InconsistentNaming
-        public static void Run(string[] _args)
+        public static void Run(string[] _args, ServiceDescriptor descriptor = null)
         {
             if (_args.Length > 0)
             {
-                var d = new ServiceDescriptor();
+                var d = descriptor ?? new ServiceDescriptor();
                 Win32Services svc = new WmiRoot().GetCollection<Win32Services>();
                 Win32Service s = svc.Select(d.Id);
 
@@ -612,6 +628,7 @@ namespace winsw
                             }
                         }
                     }
+                    return;
                 }
                 if (args[0] == "uninstall")
                 {
@@ -627,16 +644,19 @@ namespace winsw
                             return; // it's already uninstalled, so consider it a success
                         throw e;
                     }
+                    return;
                 }
                 if (args[0] == "start")
                 {
                     if (s == null) ThrowNoSuchService();
                     s.StartService();
+                    return;
                 }
                 if (args[0] == "stop")
                 {
                     if (s == null) ThrowNoSuchService();
                     s.StopService();
+                    return;
                 }
                 if (args[0] == "restart")
                 {
@@ -653,6 +673,7 @@ namespace winsw
                     }
 
                     s.StartService();
+                    return;
                 }
                 if (args[0] == "restart!")
                 {
@@ -666,6 +687,7 @@ namespace winsw
                     {
                         throw new Exception("Failed to invoke restart: "+Marshal.GetLastWin32Error());
                     }
+                    return;
                 }
                 if (args[0] == "status")
                 {
@@ -675,6 +697,7 @@ namespace winsw
                         Console.WriteLine("Started");
                     else
                         Console.WriteLine("Stopped");
+                    return;
                 }
                 if (args[0] == "test")
                 {
@@ -682,8 +705,24 @@ namespace winsw
                     wsvc.OnStart(args.ToArray());
                     Thread.Sleep(1000);
                     wsvc.OnStop();
+                    return;
                 }
-                return;
+                if (args[0] == "help" || args[0] == "--help" || args[0] == "-h" 
+                    || args[0] == "-?" || args[0] == "/?")
+                {
+                    printHelp();
+                    return;
+                }
+                if (args[0] == "version")
+                {
+                    printVersion();
+                    return;
+                }
+                
+                Console.WriteLine("Unknown command: " + args[0]);
+                printAvailableCommandsInfo();
+                throw new Exception("Unknown command: " + args[0]);
+
             }
             Run(new WrapperService());
         }
@@ -711,5 +750,43 @@ namespace winsw
                 }
             }
         }
+
+        private static void printHelp()
+        {
+            Console.WriteLine("A wrapper binary that can be used to host executables as Windows services");
+            Console.WriteLine("");
+            Console.WriteLine("Usage: winsw [/redirect file] <command> [<args>]");
+            Console.WriteLine("       Missing arguments trigger the service mode");
+            Console.WriteLine("");
+            printAvailableCommandsInfo();
+            Console.WriteLine("");
+            Console.WriteLine("Extra options:");
+            Console.WriteLine("- '/redirect' - redirect the wrapper's STDOUT and STDERR to the specified file");
+            Console.WriteLine("");
+            printVersion();
+            Console.WriteLine("More info: https://github.com/kohsuke/winsw");
+            Console.WriteLine("Bug tracker: https://github.com/kohsuke/winsw/issues");
+        }
+
+        //TODO: Rework to enum in winsw-2.0
+        private static void printAvailableCommandsInfo()
+        {
+            Console.WriteLine("Available commands:");
+            Console.WriteLine("- 'install'   - install the service to Windows Service Controller");
+            Console.WriteLine("- 'uninstall' - uninstall the service");
+            Console.WriteLine("- 'start'     - start the service (must be installed before)");
+            Console.WriteLine("- 'stop'      - stop the service");
+            Console.WriteLine("- 'restart'   - restart the service");
+            Console.WriteLine("- 'restart!'  - self-restart (can be called from child processes)");
+            Console.WriteLine("- 'status'    - check the current status of the service");
+            Console.WriteLine("- 'test'      - check if the service can be started and then stopped");  
+            Console.WriteLine("- 'version'   - print the version info");
+            Console.WriteLine("- 'help'      - print the help info (aliases: -h,--help,-?,/?)");
+        }
+
+        private static void printVersion()
+        {
+            Console.WriteLine("WinSW " + Version);
+        }
     }
 }

+ 2 - 2
Properties/AssemblyInfo.cs

@@ -28,5 +28,5 @@ using System.Runtime.InteropServices;
 // You can specify all the values or you can default the Build and Revision Numbers 
 // by using the '*' as shown below:
 // [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.1.0.0")]
-[assembly: AssemblyFileVersion("1.1.0.0")]
+[assembly: AssemblyVersion("1.17.0.0")]
+[assembly: AssemblyFileVersion("1.17.0.0")]

+ 48 - 0
Tests/winswTests/MainTest.cs

@@ -0,0 +1,48 @@
+using NUnit.Framework;
+using winsw;
+using winswTests.Util;
+
+namespace winswTests
+{
+    [TestFixture]
+    class MainTest
+    {
+
+        [Test]
+        public void PrintVersion()
+        {
+            string expectedVersion = WrapperService.Version.ToString();
+            string cliOut = CLITestHelper.CLITest(new[] { "version" });
+            StringAssert.Contains(expectedVersion, cliOut, "Expected that version contains " + expectedVersion);
+        }
+
+        [Test]
+        public void PrintHelp()
+        {
+            string expectedVersion = WrapperService.Version.ToString();
+            string cliOut = CLITestHelper.CLITest(new[] { "help" });
+            
+            StringAssert.Contains(expectedVersion, cliOut, "Expected that help contains " + expectedVersion);
+            StringAssert.Contains("start", cliOut, "Expected that help refers start command");
+            StringAssert.Contains("help", cliOut, "Expected that help refers help command");
+            StringAssert.Contains("version", cliOut, "Expected that help refers version command");
+            //TODO: check all commands after the migration of ccommands to enum
+
+            // Extra options
+            StringAssert.Contains("/redirect", cliOut, "Expected that help message refers the redirect message");
+        }
+
+        [Test]
+        public void FailOnUnsupportedCommand()
+        {
+            const string commandName = "nonExistentCommand";
+            string expectedMessage = "Unknown command: " + commandName.ToLower();
+            CLITestResult res = CLITestHelper.CLIErrorTest(new[] {commandName});
+  
+            Assert.True(res.HasException, "Expected an exception due to the wrong command");
+            StringAssert.Contains(expectedMessage, res.Out, "Expected the message about unknown command");
+            // ReSharper disable once PossibleNullReferenceException
+            StringAssert.Contains(expectedMessage, res.Exception.Message, "Expected the message about unknown command");
+        }
+    }
+}

+ 116 - 0
Tests/winswTests/Util/CLITestHelper.cs

@@ -0,0 +1,116 @@
+using System;
+using System.IO;
+using JetBrains.Annotations;
+using winsw;
+
+namespace winswTests.Util
+{
+    /// <summary>
+    /// Helper for WinSW CLI testing
+    /// </summary>
+    public static class CLITestHelper
+    {
+        private const string SeedXml = "<service>"
+                                    + "<id>service.exe</id>"
+                                    + "<name>Service</name>"
+                                    + "<description>The service.</description>"
+                                    + "<executable>node.exe</executable>"
+                                    + "<arguments>My Arguments</arguments>"
+                                    + "<logmode>rotate</logmode>"
+                                    + "<workingdirectory>"
+                                    + @"C:\winsw\workdir"
+                                    + "</workingdirectory>"
+                                    + @"<logpath>C:\winsw\logs</logpath>"
+                                    + "</service>";
+        private static readonly ServiceDescriptor DefaultServiceDescriptor = ServiceDescriptor.FromXML(SeedXml);
+
+        /// <summary>
+        /// Runs a simle test, which returns the output CLI
+        /// </summary>
+        /// <param name="args">CLI arguments to be passed</param>
+        /// <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>
+        [NotNull]
+        public static string CLITest(String[] args, ServiceDescriptor descriptor = null)
+        {
+            using (StringWriter sw = new StringWriter())
+            {
+                TextWriter tmp = Console.Out;
+                Console.SetOut(sw);
+                WrapperService.Run(args, descriptor ?? DefaultServiceDescriptor);
+                Console.SetOut(tmp);
+                Console.Write(sw.ToString());
+                return sw.ToString();
+            }
+        }
+
+        /// <summary>
+        /// Runs a simle test, which returns the output CLI
+        /// </summary>
+        /// <param name="args">CLI arguments to be passed</param>
+        /// <param name="descriptor">Optional Service descriptor (will be used for initializationpurposes)</param>
+        /// <returns>Test results</returns>
+        [NotNull]
+        public static CLITestResult CLIErrorTest(String[] args, ServiceDescriptor descriptor = null)
+        {
+            StringWriter swOut, swErr;
+            Exception testEx = null;
+            TextWriter tmpOut = Console.Out;
+            TextWriter tmpErr = Console.Error;
+
+            using (swOut = new StringWriter())
+            using (swErr = new StringWriter())
+                try
+                {              
+                    Console.SetOut(swOut);
+                    Console.SetError(swErr);
+                    WrapperService.Run(args, descriptor ?? DefaultServiceDescriptor);
+                }
+                catch (Exception ex)
+                {
+                    testEx = ex;
+                }
+                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);
+                    }
+                }
+                
+            return new CLITestResult(swOut.ToString(), swErr.ToString(), testEx);
+        }
+    }
+
+    /// <summary>
+    /// Aggregated test report
+    /// </summary>
+    public class CLITestResult
+    {
+        [NotNull]
+        public String Out { get; private set; }
+        
+        [NotNull]
+        public String Err { get; private set; }
+        
+        [CanBeNull]
+        public Exception Exception { get; private set; }
+
+        public bool HasException { get { return Exception != null; } }
+
+        public CLITestResult(String output, String err, Exception exception = null)
+        {
+            Out = output;
+            Err = err;
+            Exception = exception;
+        }
+    }
+}

+ 4 - 0
Tests/winswTests/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="JetBrains.Annotations" version="8.0.5.0" targetFramework="net20" />
+</packages>

+ 10 - 0
Tests/winswTests/winswTests.csproj

@@ -36,17 +36,24 @@
     <WarningLevel>4</WarningLevel>
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="JetBrains.Annotations, Version=8.0.5.0, Culture=neutral, PublicKeyToken=1010a0d8d6380325, processorArchitecture=MSIL">
+      <SpecificVersion>False</SpecificVersion>
+      <HintPath>..\..\packages\JetBrains.Annotations.8.0.5.0\lib\net20\JetBrains.Annotations.dll</HintPath>
+    </Reference>
     <Reference Include="nunit.framework, Version=2.6.2.12296, Culture=neutral, PublicKeyToken=96d09a1eb7f44a77, processorArchitecture=MSIL">
       <SpecificVersion>False</SpecificVersion>
       <HintPath>..\Lib\nunit.framework.dll</HintPath>
     </Reference>
     <Reference Include="System" />
     <Reference Include="System.Data" />
+    <Reference Include="System.ServiceProcess" />
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="MainTest.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="ServiceDescriptorTests.cs" />
+    <Compile Include="Util\CLITestHelper.cs" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\winsw.csproj">
@@ -54,6 +61,9 @@
       <Name>winsw</Name>
     </ProjectReference>
   </ItemGroup>
+  <ItemGroup>
+    <None Include="packages.config" />
+  </ItemGroup>
   <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 
        Other similar extension points exist, see Microsoft.Common.targets.

+ 4 - 0
packages/repositories.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<repositories>
+  <repository path="..\Tests\winswTests\packages.config" />
+</repositories>