mirror of https://github.com/winsw/winsw
New command-line interface
parent
40b566d330
commit
3ea68e2d20
|
@ -1,3 +1,4 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("WinSW")]
|
||||
[assembly: InternalsVisibleTo("WinSW.Tests")]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
|
|
|
@ -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,34 +44,17 @@ 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);
|
||||
}
|
||||
|
||||
DirectoryInfo d = new DirectoryInfo(Path.GetDirectoryName(p));
|
||||
while (true)
|
||||
{
|
||||
if (File.Exists(Path.Combine(d.FullName, baseName + ".xml")))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (d.Parent is null)
|
||||
{
|
||||
throw new FileNotFoundException("Unable to locate " + baseName + ".xml file within executable directory or any parents");
|
||||
}
|
||||
|
||||
d = d.Parent;
|
||||
throw new FileNotFoundException("Unable to locate " + baseName + ".xml file within executable directory");
|
||||
}
|
||||
|
||||
this.BaseName = baseName;
|
||||
this.BasePath = Path.Combine(d.FullName, this.BaseName);
|
||||
this.BasePath = Path.Combine(baseDir, baseName);
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -81,7 +66,45 @@ 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);
|
||||
|
||||
// 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(baseDir, baseName);
|
||||
|
||||
try
|
||||
{
|
||||
this.dom.Load(path);
|
||||
}
|
||||
catch (XmlException e)
|
||||
{
|
||||
throw new InvalidDataException(e.Message, e);
|
||||
}
|
||||
|
||||
// register the base directory as environment variable so that future expansions can refer to this.
|
||||
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();
|
||||
|
|
|
@ -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())
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
const string commandName = "unknown";
|
||||
|
||||
[Fact]
|
||||
public void PrintHelp()
|
||||
{
|
||||
string expectedVersion = WrapperService.Version.ToString();
|
||||
string cliOut = CLITestHelper.CLITest(new[] { "help" });
|
||||
CommandLineTestResult result = CommandLineTestHelper.ErrorTest(new[] { commandName });
|
||||
|
||||
Assert.Contains(expectedVersion, cliOut);
|
||||
Assert.Contains("start", cliOut);
|
||||
Assert.Contains("help", cliOut);
|
||||
Assert.Contains("version", cliOut);
|
||||
|
||||
// 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 });
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.SetError(tmpError);
|
||||
ServiceDescriptor.TestDescriptor = null;
|
||||
Program.TestExceptionHandler = null;
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
File diff suppressed because it is too large
Load Diff
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
|
||||
|
|
Loading…
Reference in New Issue