// Copyright Epic Games, Inc. All Rights Reserved.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Logging;
namespace HordeAgent.Utility
{
static class Shutdown
{
public const uint TOKEN_QUERY = 0x0008;
public const uint TOKEN_ADJUST_PRIVILEGES = 0x0020;
[DllImport("advapi32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool OpenProcessToken(IntPtr processHandle, uint desiredAccess, out IntPtr tokenHandle);
[StructLayout(LayoutKind.Sequential)]
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Native struct")]
public struct LUID
{
public uint LowPart;
public int HighPart;
}
[DllImport("advapi32.dll")]
static extern bool LookupPrivilegeValue(string? lpSystemName, string lpName, ref LUID lpLuid);
[StructLayout(LayoutKind.Sequential, Pack = 4)]
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Native struct")]
public struct LUID_AND_ATTRIBUTES
{
public LUID Luid;
public uint Attributes;
}
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Native struct")]
struct TOKEN_PRIVILEGES
{
public int PrivilegeCount;
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)]
public LUID_AND_ATTRIBUTES[] Privileges;
}
// Use this signature if you do not want the previous state
[DllImport("advapi32.dll", SetLastError = true)]
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "Native struct")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool AdjustTokenPrivileges(IntPtr TokenHandle,
[MarshalAs(UnmanagedType.Bool)] bool DisableAllPrivileges,
ref TOKEN_PRIVILEGES NewState,
uint Zero,
IntPtr Null1,
IntPtr Null2);
const uint SHTDN_REASON_MAJOR_APPLICATION = 0x00040000;
const uint SHTDN_REASON_MINOR_MAINTENANCE = 0x00000001;
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern bool InitiateSystemShutdownEx(string? lpMachineName, string? lpMessage, uint dwTimeout, bool bForceAppsClosed, bool bRebootAfterShutdown, uint dwReason);
const string SE_SHUTDOWN_NAME = "SeShutdownPrivilege";
const int SE_PRIVILEGE_ENABLED = 0x00000002;
///
/// Initiate a shutdown operation
///
/// Whether to restart after the shutdown
/// Logger for the operation
///
public static bool InitiateShutdown(bool restartAfterShutdown, ILogger logger)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
if (restartAfterShutdown)
{
logger.LogInformation("Triggering restart");
}
else
{
logger.LogInformation("Triggering shutdown");
}
IntPtr tokenHandle;
if (!OpenProcessToken(Process.GetCurrentProcess().Handle, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, out tokenHandle))
{
logger.LogError("OpenProcessToken() failed (code 0x{Code:x8})", Marshal.GetLastWin32Error());
return false;
}
// Get the LUID for the shutdown privilege.
LUID luid = new LUID();
if (!LookupPrivilegeValue(null, SE_SHUTDOWN_NAME, ref luid))
{
logger.LogError("LookupPrivilegeValue() failed (code 0x{Code:x8})", Marshal.GetLastWin32Error());
return false;
}
TOKEN_PRIVILEGES privileges = new TOKEN_PRIVILEGES();
privileges.PrivilegeCount = 1;
privileges.Privileges = new LUID_AND_ATTRIBUTES[1];
privileges.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
privileges.Privileges[0].Luid = luid;
if (!AdjustTokenPrivileges(tokenHandle, false, ref privileges, 0, IntPtr.Zero, IntPtr.Zero))
{
logger.LogError("AdjustTokenPrivileges() failed (code 0x{Code:x8})", Marshal.GetLastWin32Error());
return false;
}
uint dialogTimeout = 0; // The length of time that the shutdown dialog box should be displayed, in seconds
if (!InitiateSystemShutdownEx(null, "HordeAgent has initiated shutdown", dialogTimeout, true, restartAfterShutdown, SHTDN_REASON_MAJOR_APPLICATION | SHTDN_REASON_MINOR_MAINTENANCE))
{
logger.LogError("Shutdown failed (0x{Code:x8})", Marshal.GetLastWin32Error());
return false;
}
return true;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
string shutdownArgs;
if (restartAfterShutdown)
{
shutdownArgs = "sudo shutdown -r +0 \"Horde Agent is restarting\"";
}
else
{
shutdownArgs = "sudo shutdown +0 \"Horde Agent is shutting down\"";
}
using (Process shutdownProcess = new Process())
{
DataReceivedEventHandler handler = new DataReceivedEventHandler((_, args) =>
{
if (!String.IsNullOrEmpty(args.Data))
{
logger.LogInformation("{Output}", args.Data);
}
});
shutdownProcess.StartInfo.FileName = "/bin/sh";
shutdownProcess.StartInfo.ArgumentList.Add("-c");
shutdownProcess.StartInfo.ArgumentList.Add(shutdownArgs);
shutdownProcess.StartInfo.UseShellExecute = false;
shutdownProcess.StartInfo.CreateNoWindow = true;
shutdownProcess.StartInfo.RedirectStandardOutput = true;
shutdownProcess.StartInfo.RedirectStandardError = true;
shutdownProcess.ErrorDataReceived += handler;
shutdownProcess.OutputDataReceived += handler;
logger.LogInformation("Running {Command} {Arguments}", shutdownProcess.StartInfo.FileName, shutdownProcess.StartInfo.Arguments);
shutdownProcess.Start();
shutdownProcess.BeginOutputReadLine();
shutdownProcess.BeginErrorReadLine();
shutdownProcess.WaitForExit();
int exitCode = shutdownProcess.ExitCode;
if (exitCode != 0)
{
logger.LogError("Shutdown failed ({ExitCode})", exitCode);
return false;
}
logger.LogInformation("Exit code {ExitCode}", exitCode);
}
return true;
}
else
{
logger.LogError("Shutdown is not implemented on this platform");
return false;
}
}
///
/// Attempt to initiate a shutdown and return if it failed
///
/// Whether to restart
/// Logger for output
/// Cancellation token for the operation
public static async Task ExecuteAsync(bool restart, ILogger logger, CancellationToken cancellationToken)
{
logger.LogInformation("Initiating shutdown (restart={Restart})", restart);
if (Shutdown.InitiateShutdown(restart, logger))
{
for (int idx = 10; idx > 0; idx--)
{
logger.LogInformation("Waiting for shutdown ({Count})", idx);
try
{
await Task.Delay(TimeSpan.FromMinutes(2.0), cancellationToken);
logger.LogInformation("Shutdown aborted.");
}
catch (OperationCanceledException)
{
logger.LogInformation("Agent is shutting down.");
return;
}
}
}
}
}
}