diff --git a/README.markdown b/README.markdown index ee36f76..91bee02 100644 --- a/README.markdown +++ b/README.markdown @@ -171,6 +171,9 @@ Long human-readable description of the service. This gets displayed in Windows s ### executable This element specifies the executable to be launched. It can be either absolute path, or you can just specify the executable name and let it be searched from `PATH` (although note that the services often run in a different user account and therefore it might have different `PATH` than your shell does.) +### startmode - Optional Element +This element specifies the start mode of the Windows service. It can be one of the following values: Boot, System, Automatic, or Manual. See [MSDN](https://msdn.microsoft.com/en-us/library/aa384896%28v=vs.85%29.aspx) for details. The default is Automatic. + ### depend Specify IDs of other services that this service depends on. When service X depends on service Y, X can only run if Y is running. @@ -305,6 +308,17 @@ Optionally specify the order of service shutdown. If true, the parent process is Developer info ---------------------- +### Project status + +* WinSW 1.x - Maintenance only + * [winsw-1.17] fixes the most of active issues + * [winsw-1.17-beta.2] is available for the evaluation + * New versions may be released on-demand + * All new fixes will be ported to WinSW-2.x +* WinSW 2.x - Active development, no stable releases available + * [winsw-2.0] - Current development branch + * API stability is not guaranteed till the first release, the project structure is in flux + ### Build Environment * IDE: [Visual Studio Community 2013][MVS2013] (free for open-source projects) @@ -313,3 +327,6 @@ Developer info * The certificate is in .gitignore list. Please do not add it to the repository [MVS2013]: http://www.visualstudio.com/en-us/news/vs2013-community-vs.aspx +[winsw-1.17]: https://github.com/kohsuke/winsw/milestones/winsw-1.17 +[winsw-1.17-beta.2]: https://github.com/kohsuke/winsw/releases/tag/1.17-beta.2 +[WinSW-2.0]: https://github.com/kohsuke/winsw/milestones/winsw-2.0 diff --git a/packages/repositories.config b/packages/repositories.config new file mode 100644 index 0000000..939c169 --- /dev/null +++ b/packages/repositories.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/Core/ServiceWrapper/Main.cs b/src/Core/ServiceWrapper/Main.cs index 4eef94e..bf29bbf 100644 --- a/src/Core/ServiceWrapper/Main.cs +++ b/src/Core/ServiceWrapper/Main.cs @@ -16,6 +16,7 @@ using log4net.Repository.Hierarchy; using Microsoft.Win32; using WMI; using ServiceType = WMI.ServiceType; +using System.Reflection; namespace winsw { @@ -36,9 +37,20 @@ namespace winsw private bool _orderlyShutdown; private bool _systemShuttingdown; - public WrapperService() + /// + /// Version of Windows service wrapper + /// + /// + /// The version will be taken from + /// + public static Version Version { - _descriptor = new ServiceDescriptor(); + get { return Assembly.GetExecutingAssembly().GetName().Version; } + } + + public WrapperService(ServiceDescriptor descriptor) + { + _descriptor = descriptor; ServiceName = _descriptor.Id; CanShutdown = true; CanStop = true; @@ -47,6 +59,10 @@ namespace winsw _systemShuttingdown = false; } + public WrapperService() : this (new ServiceDescriptor()) + { + } + /// /// Process the file copy instructions, so that we can replace files that are always in use while /// the service runs. @@ -517,17 +533,17 @@ namespace winsw } // ReSharper disable once InconsistentNaming - public static void Run(string[] _args) + public static void Run(string[] _args, ServiceDescriptor descriptor = null) { bool isCLIMode = _args.Length > 0; - var d = new ServiceDescriptor(); + var d = descriptor ?? new ServiceDescriptor(); // Configure the wrapper-internal logging // STDIN and STDOUT of the child process will be handled independently InitLoggers(d, isCLIMode); if (isCLIMode) // CLI mode - { + { Log.Debug("Starting ServiceWrapper in CLI mode"); // Get service info for the future use @@ -560,6 +576,14 @@ namespace winsw args[0] = args[0].ToLower(); if (args[0] == "install") { + // Check if the service exists + if (s != null) + { + Console.WriteLine("Service with id '" + d.Id + "' already exists"); + Console.WriteLine("To install the service, delete the existing one or change service Id in the configuration file"); + throw new Exception("Installation failure: Service with id '" + d.Id + "' already exists"); + } + string username=null, password=null; bool setallowlogonasaserviceright = false; if (args.Count > 1 && args[1] == "/p") @@ -599,7 +623,7 @@ namespace winsw "\"" + d.ExecutablePath + "\"", ServiceType.OwnProcess, ErrorControl.UserNotified, - StartMode.Automatic, + d.StartMode, d.Interactive, username, password, @@ -627,11 +651,15 @@ namespace winsw } } } + return; } if (args[0] == "uninstall") { if (s == null) + { + Console.WriteLine("Warning! The service with id '" + d.Id + "' does not exist. Nothing to uninstall"); return; // there's no such service, so consider it already uninstalled + } try { s.Delete(); @@ -642,16 +670,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") { @@ -668,6 +699,7 @@ namespace winsw } s.StartService(); + return; } if (args[0] == "restart!") { @@ -681,6 +713,7 @@ namespace winsw { throw new Exception("Failed to invoke restart: "+Marshal.GetLastWin32Error()); } + return; } if (args[0] == "status") { @@ -691,6 +724,7 @@ namespace winsw Console.WriteLine("Started"); else Console.WriteLine("Stopped"); + return; } if (args[0] == "test") { @@ -698,8 +732,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()); } @@ -764,5 +814,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] []"); + 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); + } } } diff --git a/src/Core/ServiceWrapper/Properties/AssemblyInfo.cs b/src/Core/ServiceWrapper/Properties/AssemblyInfo.cs index 0a13fa9..1531349 100644 --- a/src/Core/ServiceWrapper/Properties/AssemblyInfo.cs +++ b/src/Core/ServiceWrapper/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")] diff --git a/src/Core/ServiceWrapper/ServiceDescriptor.cs b/src/Core/ServiceWrapper/ServiceDescriptor.cs index 0949bd5..1263087 100755 --- a/src/Core/ServiceWrapper/ServiceDescriptor.cs +++ b/src/Core/ServiceWrapper/ServiceDescriptor.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.IO; using System.Reflection; using System.Xml; +using WMI; namespace winsw { @@ -36,8 +37,7 @@ namespace winsw // this returns the executable name as given by the calling process, so // it needs to be absolutized. string p = Environment.GetCommandLineArgs()[0]; - return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, p); - + return Path.GetFullPath(p); } } @@ -45,18 +45,24 @@ namespace winsw { // 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 = ExecutablePath; string baseName = Path.GetFileNameWithoutExtension(p); if (baseName.EndsWith(".vshost")) baseName = baseName.Substring(0, baseName.Length - 7); + DirectoryInfo d = new DirectoryInfo(Path.GetDirectoryName(p)); while (true) { - p = Path.GetDirectoryName(p); - if (File.Exists(Path.Combine(p, baseName + ".xml"))) + if (File.Exists(Path.Combine(d.FullName, baseName + ".xml"))) break; + + if (d.Parent == null) + throw new FileNotFoundException("Unable to locate "+baseName+".xml file within executable directory or any parents"); + + d = d.Parent; } BaseName = baseName; - BasePath = Path.Combine(p, BaseName); + BasePath = Path.Combine(d.FullName, BaseName); dom.Load(BasePath + ".xml"); @@ -393,6 +399,31 @@ namespace winsw } } + /// + /// Start mode of the Service + /// + public StartMode StartMode + { + get + { + var p = SingleElement("startmode", true); + if (p == null) return StartMode.Automatic; // default value + try + { + return (StartMode)Enum.Parse(typeof(StartMode), p, true); + } + catch + { + Console.WriteLine("Start mode in XML must be one of the following:"); + foreach (string sm in Enum.GetNames(typeof(StartMode))) + { + Console.WriteLine(sm); + } + throw; + } + } + } + /// /// True if the service should when finished on shutdown. /// This doesn't work on some OSes. See http://msdn.microsoft.com/en-us/library/ms679277%28VS.85%29.aspx diff --git a/src/Core/ServiceWrapper/winsw.csproj b/src/Core/ServiceWrapper/winsw.csproj index b4fda6c..0844ff4 100644 --- a/src/Core/ServiceWrapper/winsw.csproj +++ b/src/Core/ServiceWrapper/winsw.csproj @@ -45,7 +45,7 @@ full false bin\Debug\ - DEBUG;TRACE + TRACE;DEBUG prompt 4 true @@ -58,6 +58,8 @@ prompt 4 true + + diff --git a/src/Test/winswTests/MainTest.cs b/src/Test/winswTests/MainTest.cs new file mode 100644 index 0000000..6bf8402 --- /dev/null +++ b/src/Test/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"); + } + } +} diff --git a/src/Test/winswTests/ServiceDescriptorTests.cs b/src/Test/winswTests/ServiceDescriptorTests.cs index 9c0e5ab..7b7ebb1 100644 --- a/src/Test/winswTests/ServiceDescriptorTests.cs +++ b/src/Test/winswTests/ServiceDescriptorTests.cs @@ -5,6 +5,9 @@ using winsw; namespace winswTests { + using System; + using WMI; + [TestFixture] public class ServiceDescriptorTests { @@ -41,6 +44,66 @@ namespace winswTests _extendedServiceDescriptor = ServiceDescriptor.FromXML(seedXml); } + [Test] + public void DefaultStartMode() + { + Assert.That(_extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Automatic)); + } + + [Test] + [ExpectedException(typeof(System.ArgumentException))] + public void IncorrectStartMode() + { + const string SeedXml = "" + + "service.exe" + + "Service" + + "The service." + + "node.exe" + + "My Arguments" + + "rotate" + + "rotate" + + "" + + "" + Domain + "" + + "" + Username + "" + + "" + Password + "" + + "" + AllowServiceAccountLogonRight + "" + + "" + + "" + + ExpectedWorkingDirectory + + "" + + @"C:\logs" + + ""; + + _extendedServiceDescriptor = ServiceDescriptor.FromXML(SeedXml); + Assert.That(_extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Manual)); + } + + [Test] + public void ChangedStartMode() + { + const string SeedXml = "" + + "service.exe" + + "Service" + + "The service." + + "node.exe" + + "My Arguments" + + "manual" + + "rotate" + + "" + + "" + Domain + "" + + "" + Username + "" + + "" + Password + "" + + "" + AllowServiceAccountLogonRight + "" + + "" + + "" + + ExpectedWorkingDirectory + + "" + + @"C:\logs" + + ""; + + _extendedServiceDescriptor = ServiceDescriptor.FromXML(SeedXml); + Assert.That(_extendedServiceDescriptor.StartMode, Is.EqualTo(StartMode.Manual)); + } [Test] public void VerifyWorkingDirectory() { diff --git a/src/Test/winswTests/Util/CLITestHelper.cs b/src/Test/winswTests/Util/CLITestHelper.cs new file mode 100644 index 0000000..193c6b7 --- /dev/null +++ b/src/Test/winswTests/Util/CLITestHelper.cs @@ -0,0 +1,116 @@ +using System; +using System.IO; +using JetBrains.Annotations; +using winsw; + +namespace winswTests.Util +{ + /// + /// Helper for WinSW CLI testing + /// + public static class CLITestHelper + { + private const string SeedXml = "" + + "service.exe" + + "Service" + + "The service." + + "node.exe" + + "My Arguments" + + "rotate" + + "" + + @"C:\winsw\workdir" + + "" + + @"C:\winsw\logs" + + ""; + private static readonly ServiceDescriptor DefaultServiceDescriptor = ServiceDescriptor.FromXML(SeedXml); + + /// + /// Runs a simle test, which returns the output CLI + /// + /// CLI arguments to be passed + /// Optional Service descriptor (will be used for initializationpurposes) + /// STDOUT if there's no exceptions + /// Command failure + [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(); + } + } + + /// + /// Runs a simle test, which returns the output CLI + /// + /// CLI arguments to be passed + /// Optional Service descriptor (will be used for initializationpurposes) + /// Test results + [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); + } + } + + /// + /// Aggregated test report + /// + 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; + } + } +} diff --git a/src/Test/winswTests/packages.config b/src/Test/winswTests/packages.config index c29756e..cba033f 100644 --- a/src/Test/winswTests/packages.config +++ b/src/Test/winswTests/packages.config @@ -1,4 +1,5 @@  + \ No newline at end of file diff --git a/src/Test/winswTests/winswTests.csproj b/src/Test/winswTests/winswTests.csproj index 507e9d0..83bddf7 100644 --- a/src/Test/winswTests/winswTests.csproj +++ b/src/Test/winswTests/winswTests.csproj @@ -37,18 +37,25 @@ prompt 4 - + + + False + ..\..\packages\JetBrains.Annotations.8.0.5.0\lib\net20\JetBrains.Annotations.dll + False ..\..\packages\NUnit.2.6.4\lib\nunit.framework.dll + + + diff --git a/src/winsw.sln b/src/winsw.sln index c608641..15d09bd 100644 --- a/src/winsw.sln +++ b/src/winsw.sln @@ -16,34 +16,18 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{6BDF40 EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Debug|Mixed Platforms = Debug|Mixed Platforms Debug|Win32 = Debug|Win32 - Release|Any CPU = Release|Any CPU - Release|Mixed Platforms = Release|Mixed Platforms Release|Win32 = Release|Win32 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Mixed Platforms.ActiveCfg = Release|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Mixed Platforms.Build.0 = Release|Any CPU {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Win32.ActiveCfg = Debug|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Any CPU.Build.0 = Release|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Debug|Win32.Build.0 = Debug|Any CPU {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Win32.ActiveCfg = Release|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Any CPU.Build.0 = Debug|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {0DE77F55-ADE5-43C1-999A-0BC81153B039}.Release|Win32.Build.0 = Release|Any CPU {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Win32.ActiveCfg = Debug|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Any CPU.ActiveCfg = Release|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Any CPU.Build.0 = Release|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {93843402-842B-44B4-B303-AEE829BE0B43}.Debug|Win32.Build.0 = Debug|Any CPU {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Win32.ActiveCfg = Release|Any CPU + {93843402-842B-44B4-B303-AEE829BE0B43}.Release|Win32.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE