Prompt for elevation (#457)

* Prompt for elevation

* Add error messages for interop errors

* Assume process is elevated on Windows XP

Co-authored-by: Oleg Nenashev <o.v.nenashev@gmail.com>
pull/516/head v2.8.0
Next Turn 2020-04-19 15:04:50 +08:00 committed by GitHub
parent a1d335bd60
commit b8888c9a29
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 183 additions and 3 deletions

View File

@ -49,6 +49,8 @@ Your renamed *WinSW.exe* binary also accepts the following commands:
* `Started` to indicate the service is currently running * `Started` to indicate the service is currently running
* `Stopped` to indicate that the service is installed but not currently running. * `Stopped` to indicate that the service is installed but not currently running.
Most commands require Administrator privileges to execute. Since v2.8, WinSW will prompt for UAC in non-elevated sessions.
## Documentation ## Documentation
User documentation: User documentation:

View File

@ -1,11 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.AccessControl; using System.Security.AccessControl;
using System.Security.Principal;
using System.ServiceProcess; using System.ServiceProcess;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
@ -589,6 +591,26 @@ namespace winsw
args = args.GetRange(2, args.Count - 2); args = args.GetRange(2, args.Count - 2);
} }
bool elevated;
if (args[0] == "/elevated")
{
elevated = true;
_ = SigIntHelper.FreeConsole();
_ = SigIntHelper.AttachConsole(SigIntHelper.ATTACH_PARENT_PROCESS);
args = args.GetRange(1, args.Count - 1);
}
else if (Environment.OSVersion.Version.Major == 5)
{
// Windows XP
elevated = true;
}
else
{
elevated = IsProcessElevated();
}
switch (args[0].ToLower()) switch (args[0].ToLower())
{ {
case "install": case "install":
@ -647,6 +669,12 @@ namespace winsw
void Install() void Install()
{ {
if (!elevated)
{
Elevate();
return;
}
Log.Info("Installing the service with id '" + descriptor.Id + "'"); Log.Info("Installing the service with id '" + descriptor.Id + "'");
// Check if the service exists // Check if the service exists
@ -738,6 +766,12 @@ namespace winsw
void Uninstall() void Uninstall()
{ {
if (!elevated)
{
Elevate();
return;
}
Log.Info("Uninstalling the service with id '" + descriptor.Id + "'"); Log.Info("Uninstalling the service with id '" + descriptor.Id + "'");
if (s is null) if (s is null)
{ {
@ -777,6 +811,12 @@ namespace winsw
void Start() void Start()
{ {
if (!elevated)
{
Elevate();
return;
}
Log.Info("Starting the service with id '" + descriptor.Id + "'"); Log.Info("Starting the service with id '" + descriptor.Id + "'");
if (s is null) if (s is null)
ThrowNoSuchService(); ThrowNoSuchService();
@ -800,6 +840,12 @@ namespace winsw
void Stop() void Stop()
{ {
if (!elevated)
{
Elevate();
return;
}
Log.Info("Stopping the service with id '" + descriptor.Id + "'"); Log.Info("Stopping the service with id '" + descriptor.Id + "'");
if (s is null) if (s is null)
ThrowNoSuchService(); ThrowNoSuchService();
@ -823,6 +869,12 @@ namespace winsw
void Restart() void Restart()
{ {
if (!elevated)
{
Elevate();
return;
}
Log.Info("Restarting the service with id '" + descriptor.Id + "'"); Log.Info("Restarting the service with id '" + descriptor.Id + "'");
if (s is null) if (s is null)
ThrowNoSuchService(); ThrowNoSuchService();
@ -841,6 +893,11 @@ namespace winsw
void RestartSelf() void RestartSelf()
{ {
if (!elevated)
{
throw new UnauthorizedAccessException("Access is denied.");
}
Log.Info("Restarting the service with id '" + descriptor.Id + "'"); Log.Info("Restarting the service with id '" + descriptor.Id + "'");
// run restart from another process group. see README.md for why this is useful. // run restart from another process group. see README.md for why this is useful.
@ -865,6 +922,12 @@ namespace winsw
void Test() void Test()
{ {
if (!elevated)
{
Elevate();
return;
}
WrapperService wsvc = new WrapperService(descriptor); WrapperService wsvc = new WrapperService(descriptor);
wsvc.OnStart(args.ToArray()); wsvc.OnStart(args.ToArray());
Thread.Sleep(1000); Thread.Sleep(1000);
@ -873,12 +936,52 @@ namespace winsw
void TestWait() void TestWait()
{ {
if (!elevated)
{
Elevate();
return;
}
WrapperService wsvc = new WrapperService(descriptor); WrapperService wsvc = new WrapperService(descriptor);
wsvc.OnStart(args.ToArray()); wsvc.OnStart(args.ToArray());
Console.WriteLine("Press any key to stop the service..."); Console.WriteLine("Press any key to stop the service...");
Console.Read(); Console.Read();
wsvc.OnStop(); wsvc.OnStop();
} }
// [DoesNotReturn]
void Elevate()
{
using Process current = Process.GetCurrentProcess();
ProcessStartInfo startInfo = new ProcessStartInfo
{
UseShellExecute = true,
Verb = "runas",
FileName = current.MainModule.FileName,
#if NETCOREAPP
Arguments = "/elevated " + string.Join(' ', args),
#elif !NET20
Arguments = "/elevated " + string.Join(" ", args),
#else
Arguments = "/elevated " + string.Join(" ", args.ToArray()),
#endif
WindowStyle = ProcessWindowStyle.Hidden,
};
try
{
using Process elevated = Process.Start(startInfo);
elevated.WaitForExit();
Environment.Exit(elevated.ExitCode);
}
catch (Win32Exception e) when (e.NativeErrorCode == Errors.ERROR_CANCELLED)
{
Log.Fatal(e.Message);
Environment.Exit(e.ErrorCode);
}
}
} }
private static void InitLoggers(ServiceDescriptor d, bool enableCLILogging) private static void InitLoggers(ServiceDescriptor d, bool enableCLILogging)
@ -941,6 +1044,40 @@ namespace winsw
appenders.ToArray()); appenders.ToArray());
} }
private static unsafe bool IsProcessElevated()
{
IntPtr process = Kernel32.GetCurrentProcess();
if (!Advapi32.OpenProcessToken(process, TokenAccessLevels.Read, out IntPtr token))
{
ThrowWin32Exception("Failed to open process token.");
}
try
{
if (!Advapi32.GetTokenInformation(
token,
TOKEN_INFORMATION_CLASS.TokenElevation,
out TOKEN_ELEVATION elevation,
sizeof(TOKEN_ELEVATION),
out _))
{
ThrowWin32Exception("Failed to get token information");
}
return elevation.TokenIsElevated != 0;
}
finally
{
_ = Kernel32.CloseHandle(token);
}
static void ThrowWin32Exception(string message)
{
Win32Exception inner = new Win32Exception();
throw new Win32Exception(inner.NativeErrorCode, message + ' ' + inner.Message);
}
}
private static string ReadPassword() private static string ReadPassword()
{ {
StringBuilder buf = new StringBuilder(); StringBuilder buf = new StringBuilder();

View File

@ -5,6 +5,7 @@
<TargetFrameworks>net20;net40;net461;netcoreapp3.1</TargetFrameworks> <TargetFrameworks>net20;net40;net461;netcoreapp3.1</TargetFrameworks>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<AssemblyTitle>Windows Service Wrapper</AssemblyTitle> <AssemblyTitle>Windows Service Wrapper</AssemblyTitle>
<Description>Allows arbitrary process to run as a Windows service by wrapping it.</Description> <Description>Allows arbitrary process to run as a Windows service by wrapping it.</Description>

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.AccessControl; using System.Security.AccessControl;
using System.Security.Principal;
using System.Text; using System.Text;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
@ -321,6 +322,20 @@ namespace winsw.Native
[DllImport(Advapi32LibraryName, SetLastError = false)] [DllImport(Advapi32LibraryName, SetLastError = false)]
internal static extern uint LsaNtStatusToWinError(uint status); internal static extern uint LsaNtStatusToWinError(uint status);
[DllImport(Advapi32LibraryName, SetLastError = true)]
public static extern bool OpenProcessToken(
IntPtr ProcessHandle,
TokenAccessLevels DesiredAccess,
out IntPtr TokenHandle);
[DllImport(Advapi32LibraryName, SetLastError = true)]
public static extern bool GetTokenInformation(
IntPtr TokenHandle,
TOKEN_INFORMATION_CLASS TokenInformationClass,
out TOKEN_ELEVATION TokenInformation,
int TokenInformationLength,
out int ReturnLength);
} }
// http://msdn.microsoft.com/en-us/library/windows/desktop/bb545671(v=vs.85).aspx // http://msdn.microsoft.com/en-us/library/windows/desktop/bb545671(v=vs.85).aspx
@ -603,4 +618,14 @@ namespace winsw.Native
{ {
public string lpDescription; public string lpDescription;
} }
public enum TOKEN_INFORMATION_CLASS
{
TokenElevation = 20,
}
public struct TOKEN_ELEVATION
{
public uint TokenIsElevated;
}
} }

View File

@ -0,0 +1,7 @@
namespace winsw.Native
{
public static class Errors
{
public const int ERROR_CANCELLED = 1223;
}
}

View File

@ -28,6 +28,12 @@ namespace winsw.Native
string? lpCurrentDirectory, string? lpCurrentDirectory,
in STARTUPINFO lpStartupInfo, in STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation); out PROCESS_INFORMATION lpProcessInformation);
[DllImport(Kernel32LibraryName)]
public static extern IntPtr GetCurrentProcess();
[DllImport(Kernel32LibraryName)]
public static extern bool CloseHandle(IntPtr hObject);
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]

View File

@ -9,13 +9,15 @@ namespace winsw.Util
{ {
private static readonly ILog Logger = LogManager.GetLogger(typeof(SigIntHelper)); private static readonly ILog Logger = LogManager.GetLogger(typeof(SigIntHelper));
public const int ATTACH_PARENT_PROCESS = -1;
private const string Kernel32LibraryName = "kernel32.dll"; private const string Kernel32LibraryName = "kernel32.dll";
[DllImport(Kernel32LibraryName, SetLastError = true)] [DllImport(Kernel32LibraryName, SetLastError = true)]
private static extern bool AttachConsole(uint dwProcessId); public static extern bool AttachConsole(int dwProcessId);
[DllImport(Kernel32LibraryName, SetLastError = true)] [DllImport(Kernel32LibraryName, SetLastError = true)]
private static extern bool FreeConsole(); public static extern bool FreeConsole();
[DllImport(Kernel32LibraryName)] [DllImport(Kernel32LibraryName)]
private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate? HandlerRoutine, bool Add); private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate? HandlerRoutine, bool Add);
@ -44,7 +46,7 @@ namespace winsw.Util
/// <returns>True if the process shut down successfully to the SIGINT, false if it did not.</returns> /// <returns>True if the process shut down successfully to the SIGINT, false if it did not.</returns>
public static bool SendSIGINTToProcess(Process process, TimeSpan shutdownTimeout) public static bool SendSIGINTToProcess(Process process, TimeSpan shutdownTimeout)
{ {
if (AttachConsole((uint)process.Id)) if (AttachConsole(process.Id))
{ {
// Disable Ctrl-C handling for our program // Disable Ctrl-C handling for our program
_ = SetConsoleCtrlHandler(null, true); _ = SetConsoleCtrlHandler(null, true);