New command-line interface

pull/609/head
NextTurn 2020-07-18 00:00:00 +08:00 committed by Next Turn
parent 40b566d330
commit 3ea68e2d20
11 changed files with 681 additions and 540 deletions

View File

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

View File

@ -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);
}
}
}
}

View File

@ -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))

View File

@ -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();

View File

@ -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())
{

View File

@ -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);
}
}

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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)
{
}
}
}

View File

@ -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>