From 57b6d2ad484698cdeadbd7494c920331775af2f5 Mon Sep 17 00:00:00 2001 From: Nicholas Carpenter Date: Sat, 31 Jan 2015 21:38:05 -0500 Subject: [PATCH 1/7] Add ability to set startup type to XML file --- Main.cs | 2 +- README.markdown | 3 ++ ServiceDescriptor.cs | 26 +++++++++ Tests/winswTests/ServiceDescriptorTests.cs | 63 ++++++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/Main.cs b/Main.cs index 99bddda..2c56a7d 100644 --- a/Main.cs +++ b/Main.cs @@ -584,7 +584,7 @@ namespace winsw "\"" + d.ExecutablePath + "\"", ServiceType.OwnProcess, ErrorControl.UserNotified, - StartMode.Automatic, + d.StartMode, d.Interactive, username, password, diff --git a/README.markdown b/README.markdown index ee36f76..b52c4b2 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. diff --git a/ServiceDescriptor.cs b/ServiceDescriptor.cs index 0949bd5..baa3e7b 100755 --- a/ServiceDescriptor.cs +++ b/ServiceDescriptor.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using System.IO; using System.Reflection; using System.Xml; +using WMI; namespace winsw { @@ -393,6 +394,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/Tests/winswTests/ServiceDescriptorTests.cs b/Tests/winswTests/ServiceDescriptorTests.cs index 9c0e5ab..7b7ebb1 100644 --- a/Tests/winswTests/ServiceDescriptorTests.cs +++ b/Tests/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() { From 76de18b74496b5434a7e2d435747c22438c7856b Mon Sep 17 00:00:00 2001 From: Nicholas Carpenter Date: Sat, 31 Jan 2015 22:51:44 -0500 Subject: [PATCH 2/7] Fix needing to call install command from same directory. --- ServiceDescriptor.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ServiceDescriptor.cs b/ServiceDescriptor.cs index 0949bd5..826e096 100755 --- a/ServiceDescriptor.cs +++ b/ServiceDescriptor.cs @@ -36,8 +36,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); } } From 91ed4b3184053fdb1a758049e5a83f61c03447e2 Mon Sep 17 00:00:00 2001 From: Nicholas Carpenter Date: Sat, 31 Jan 2015 23:19:50 -0500 Subject: [PATCH 3/7] Fix null exception and throw a FileNotFoundException if xml file is never found --- ServiceDescriptor.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ServiceDescriptor.cs b/ServiceDescriptor.cs index 0949bd5..8f9453e 100755 --- a/ServiceDescriptor.cs +++ b/ServiceDescriptor.cs @@ -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"); From 804691e95fc097cb111b041f1a9be86587a71ebd Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Sun, 1 Feb 2015 15:10:00 +0300 Subject: [PATCH 4/7] Better diagnosis in install and uninstall commands * install - Fail the installation if the service exists * uninstall - Print warning if the service does not exist Resolves #55 Signed-off-by: Oleg Nenashev --- Main.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Main.cs b/Main.cs index 99bddda..5131fa7 100644 --- a/Main.cs +++ b/Main.cs @@ -545,6 +545,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") @@ -616,7 +624,10 @@ namespace winsw 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(); From c5fd4a7a05b1b4866d32f9d1716f53a24376ea01 Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Sun, 1 Feb 2015 17:31:13 +0300 Subject: [PATCH 5/7] Get rid of extra configurations, fix output destinations * Win32 is the only supported configurationdue to Win32 API dependencies * Debug and Release now output the data to different folders Fixes #84 Signed-off-by: Oleg Nenashev --- winsw.csproj | 4 +++- winsw.sln | 24 ++++-------------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/winsw.csproj b/winsw.csproj index 343c31c..215bbd8 100644 --- a/winsw.csproj +++ b/winsw.csproj @@ -43,7 +43,7 @@ full false bin\Debug\ - DEBUG;TRACE + TRACE;DEBUG prompt 4 true @@ -56,6 +56,8 @@ prompt 4 true + + diff --git a/winsw.sln b/winsw.sln index efd71d1..7e4aa45 100644 --- a/winsw.sln +++ b/winsw.sln @@ -9,34 +9,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "winswTests", "Tests\winswTe 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 From 3ae6ffaa476e0074f485efdc88707a26e8ee8acd Mon Sep 17 00:00:00 2001 From: Oleg Nenashev Date: Tue, 3 Feb 2015 00:05:56 +0300 Subject: [PATCH 6/7] 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 --- Main.cs | 87 +++++++++++++++++-- Properties/AssemblyInfo.cs | 4 +- Tests/winswTests/MainTest.cs | 48 ++++++++++ Tests/winswTests/Util/CLITestHelper.cs | 116 +++++++++++++++++++++++++ Tests/winswTests/packages.config | 4 + Tests/winswTests/winswTests.csproj | 10 +++ packages/repositories.config | 4 + 7 files changed, 266 insertions(+), 7 deletions(-) create mode 100644 Tests/winswTests/MainTest.cs create mode 100644 Tests/winswTests/Util/CLITestHelper.cs create mode 100644 Tests/winswTests/packages.config create mode 100644 packages/repositories.config diff --git a/Main.cs b/Main.cs index 99bddda..b800a91 100644 --- a/Main.cs +++ b/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() + /// + /// 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; @@ -39,6 +51,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. @@ -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(); 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] []"); + 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/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs index 0a13fa9..1531349 100644 --- a/Properties/AssemblyInfo.cs +++ b/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/Tests/winswTests/MainTest.cs b/Tests/winswTests/MainTest.cs new file mode 100644 index 0000000..6bf8402 --- /dev/null +++ b/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"); + } + } +} diff --git a/Tests/winswTests/Util/CLITestHelper.cs b/Tests/winswTests/Util/CLITestHelper.cs new file mode 100644 index 0000000..193c6b7 --- /dev/null +++ b/Tests/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/Tests/winswTests/packages.config b/Tests/winswTests/packages.config new file mode 100644 index 0000000..e38473a --- /dev/null +++ b/Tests/winswTests/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Tests/winswTests/winswTests.csproj b/Tests/winswTests/winswTests.csproj index 9e37360..5992f0f 100644 --- a/Tests/winswTests/winswTests.csproj +++ b/Tests/winswTests/winswTests.csproj @@ -36,17 +36,24 @@ 4 + + False + ..\..\packages\JetBrains.Annotations.8.0.5.0\lib\net20\JetBrains.Annotations.dll + False ..\Lib\nunit.framework.dll + + + @@ -54,6 +61,9 @@ winsw + + +