diff --git a/README.md b/README.md
index c33506d..de29c56 100644
--- a/README.md
+++ b/README.md
@@ -49,6 +49,8 @@ Your renamed *WinSW.exe* binary also accepts the following commands:
* `Started` to indicate the service is 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
User documentation:
diff --git a/src/Core/ServiceWrapper/Main.cs b/src/Core/ServiceWrapper/Main.cs
index 8003e2c..e58fa5b 100644
--- a/src/Core/ServiceWrapper/Main.cs
+++ b/src/Core/ServiceWrapper/Main.cs
@@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
+using System.Security.Principal;
using System.ServiceProcess;
using System.Text;
using System.Text.RegularExpressions;
@@ -589,6 +591,26 @@ namespace winsw
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())
{
case "install":
@@ -647,6 +669,12 @@ namespace winsw
void Install()
{
+ if (!elevated)
+ {
+ Elevate();
+ return;
+ }
+
Log.Info("Installing the service with id '" + descriptor.Id + "'");
// Check if the service exists
@@ -738,6 +766,12 @@ namespace winsw
void Uninstall()
{
+ if (!elevated)
+ {
+ Elevate();
+ return;
+ }
+
Log.Info("Uninstalling the service with id '" + descriptor.Id + "'");
if (s is null)
{
@@ -777,6 +811,12 @@ namespace winsw
void Start()
{
+ if (!elevated)
+ {
+ Elevate();
+ return;
+ }
+
Log.Info("Starting the service with id '" + descriptor.Id + "'");
if (s is null)
ThrowNoSuchService();
@@ -800,6 +840,12 @@ namespace winsw
void Stop()
{
+ if (!elevated)
+ {
+ Elevate();
+ return;
+ }
+
Log.Info("Stopping the service with id '" + descriptor.Id + "'");
if (s is null)
ThrowNoSuchService();
@@ -823,6 +869,12 @@ namespace winsw
void Restart()
{
+ if (!elevated)
+ {
+ Elevate();
+ return;
+ }
+
Log.Info("Restarting the service with id '" + descriptor.Id + "'");
if (s is null)
ThrowNoSuchService();
@@ -841,6 +893,11 @@ namespace winsw
void RestartSelf()
{
+ if (!elevated)
+ {
+ throw new UnauthorizedAccessException("Access is denied.");
+ }
+
Log.Info("Restarting the service with id '" + descriptor.Id + "'");
// run restart from another process group. see README.md for why this is useful.
@@ -865,6 +922,12 @@ namespace winsw
void Test()
{
+ if (!elevated)
+ {
+ Elevate();
+ return;
+ }
+
WrapperService wsvc = new WrapperService(descriptor);
wsvc.OnStart(args.ToArray());
Thread.Sleep(1000);
@@ -873,12 +936,52 @@ namespace winsw
void TestWait()
{
+ if (!elevated)
+ {
+ Elevate();
+ return;
+ }
+
WrapperService wsvc = new WrapperService(descriptor);
wsvc.OnStart(args.ToArray());
Console.WriteLine("Press any key to stop the service...");
Console.Read();
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)
@@ -941,6 +1044,40 @@ namespace winsw
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()
{
StringBuilder buf = new StringBuilder();
diff --git a/src/Core/ServiceWrapper/winsw.csproj b/src/Core/ServiceWrapper/winsw.csproj
index e90deb5..071de8f 100644
--- a/src/Core/ServiceWrapper/winsw.csproj
+++ b/src/Core/ServiceWrapper/winsw.csproj
@@ -5,6 +5,7 @@
net20;net40;net461;netcoreapp3.1
latest
enable
+ true
Windows Service Wrapper
Allows arbitrary process to run as a Windows service by wrapping it.
diff --git a/src/Core/WinSWCore/Native/Advapi32.cs b/src/Core/WinSWCore/Native/Advapi32.cs
index 6604f33..6c87fdd 100755
--- a/src/Core/WinSWCore/Native/Advapi32.cs
+++ b/src/Core/WinSWCore/Native/Advapi32.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
+using System.Security.Principal;
using System.Text;
// ReSharper disable InconsistentNaming
@@ -321,6 +322,20 @@ namespace winsw.Native
[DllImport(Advapi32LibraryName, SetLastError = false)]
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
@@ -603,4 +618,14 @@ namespace winsw.Native
{
public string lpDescription;
}
+
+ public enum TOKEN_INFORMATION_CLASS
+ {
+ TokenElevation = 20,
+ }
+
+ public struct TOKEN_ELEVATION
+ {
+ public uint TokenIsElevated;
+ }
}
diff --git a/src/Core/WinSWCore/Native/Errors.cs b/src/Core/WinSWCore/Native/Errors.cs
new file mode 100644
index 0000000..2dfe908
--- /dev/null
+++ b/src/Core/WinSWCore/Native/Errors.cs
@@ -0,0 +1,7 @@
+namespace winsw.Native
+{
+ public static class Errors
+ {
+ public const int ERROR_CANCELLED = 1223;
+ }
+}
diff --git a/src/Core/WinSWCore/Native/Kernel32.cs b/src/Core/WinSWCore/Native/Kernel32.cs
index 32528c1..bd93b91 100755
--- a/src/Core/WinSWCore/Native/Kernel32.cs
+++ b/src/Core/WinSWCore/Native/Kernel32.cs
@@ -28,6 +28,12 @@ namespace winsw.Native
string? lpCurrentDirectory,
in STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
+
+ [DllImport(Kernel32LibraryName)]
+ public static extern IntPtr GetCurrentProcess();
+
+ [DllImport(Kernel32LibraryName)]
+ public static extern bool CloseHandle(IntPtr hObject);
}
[StructLayout(LayoutKind.Sequential)]
diff --git a/src/Core/WinSWCore/Util/SigIntHelper.cs b/src/Core/WinSWCore/Util/SigIntHelper.cs
index b8ede86..558cad1 100644
--- a/src/Core/WinSWCore/Util/SigIntHelper.cs
+++ b/src/Core/WinSWCore/Util/SigIntHelper.cs
@@ -9,13 +9,15 @@ namespace winsw.Util
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(SigIntHelper));
+ public const int ATTACH_PARENT_PROCESS = -1;
+
private const string Kernel32LibraryName = "kernel32.dll";
[DllImport(Kernel32LibraryName, SetLastError = true)]
- private static extern bool AttachConsole(uint dwProcessId);
+ public static extern bool AttachConsole(int dwProcessId);
[DllImport(Kernel32LibraryName, SetLastError = true)]
- private static extern bool FreeConsole();
+ public static extern bool FreeConsole();
[DllImport(Kernel32LibraryName)]
private static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate? HandlerRoutine, bool Add);
@@ -44,7 +46,7 @@ namespace winsw.Util
/// True if the process shut down successfully to the SIGINT, false if it did not.
public static bool SendSIGINTToProcess(Process process, TimeSpan shutdownTimeout)
{
- if (AttachConsole((uint)process.Id))
+ if (AttachConsole(process.Id))
{
// Disable Ctrl-C handling for our program
_ = SetConsoleCtrlHandler(null, true);