From 413ef4c32ff0ab858a7b8da5f98d280801da279a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 12 Jun 2020 18:06:15 -0400 Subject: [PATCH] Implement minidumps on Windows --- .../Models/DreamDaemon.cs | 5 +++ .../Rights/DreamDaemonRights.cs | 5 +++ src/Tgstation.Server.Api/Routes.cs | 5 +++ .../Session/DeadSessionController.cs | 3 ++ .../Components/Session/SessionController.cs | 3 ++ .../Components/Watchdog/IWatchdog.cs | 7 ++++ .../Components/Watchdog/WatchdogBase.cs | 23 +++++++++++++ .../Controllers/DreamDaemonController.cs | 32 +++++++++++++++++++ .../Extensions/DateTimeOffsetExtensions.cs | 19 +++++++++++ src/Tgstation.Server.Host/NativeMethods.cs | 24 +++++++++++++- .../System/IProcessBase.cs | 9 ++++++ .../System/IProcessFeatures.cs | 11 ++++++- .../System/PosixProcessFeatures.cs | 6 ++++ src/Tgstation.Server.Host/System/Process.cs | 10 ++++++ .../System/WindowsProcessFeatures.cs | 24 ++++++++++++++ 15 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 src/Tgstation.Server.Host/Extensions/DateTimeOffsetExtensions.cs diff --git a/src/Tgstation.Server.Api/Models/DreamDaemon.cs b/src/Tgstation.Server.Api/Models/DreamDaemon.cs index 38e29363e0..8f2545af4f 100644 --- a/src/Tgstation.Server.Api/Models/DreamDaemon.cs +++ b/src/Tgstation.Server.Api/Models/DreamDaemon.cs @@ -48,5 +48,10 @@ namespace Tgstation.Server.Api.Models /// If the server is undergoing a soft shutdown /// public bool? SoftShutdown { get; set; } + + /// + /// If a dump of the active DreamDaemon executable should be created. + /// + public bool? CreateDump { get; set; } } } diff --git a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs index ed341547cd..fd4c53f641 100644 --- a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs +++ b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs @@ -77,5 +77,10 @@ namespace Tgstation.Server.Api.Rights /// User can change /// SetHeartbeatInterval = 4096, + + /// + /// User can create DreamDaemon process dumps. + /// + CreateDump = 8192, } } diff --git a/src/Tgstation.Server.Api/Routes.cs b/src/Tgstation.Server.Api/Routes.cs index 6a2da6c8f0..2bb3f4e603 100644 --- a/src/Tgstation.Server.Api/Routes.cs +++ b/src/Tgstation.Server.Api/Routes.cs @@ -43,6 +43,11 @@ namespace Tgstation.Server.Api /// public const string DreamDaemon = Root + nameof(Models.DreamDaemon); + /// + /// For accessing DD diagnostics + /// + public const string Diagnostics = DreamDaemon + "/Diagnostics"; + /// /// The controller /// diff --git a/src/Tgstation.Server.Host/Components/Session/DeadSessionController.cs b/src/Tgstation.Server.Host/Components/Session/DeadSessionController.cs index 2f5ff7f5c4..a425b5aac3 100644 --- a/src/Tgstation.Server.Host/Components/Session/DeadSessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/DeadSessionController.cs @@ -123,5 +123,8 @@ namespace Tgstation.Server.Host.Components.Session /// public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task CreateDump(string outputFile, CancellationToken cancellationToken) => throw new NotSupportedException(); } } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index dca20a3a14..47cbac6bd8 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -696,5 +696,8 @@ namespace Tgstation.Server.Host.Components.Session new TopicParameters( new ChatUpdate(newChannels)), cancellationToken); + + /// + public Task CreateDump(string outputFile, CancellationToken cancellationToken) => process.CreateDump(outputFile, cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index 6c40b1bd97..bc2fdbd8a7 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -80,5 +80,12 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the operation /// A representing the running operation Task ResetRebootState(CancellationToken cancellationToken); + + /// + /// Attempt to create a process dump for DreamDaemon. + /// + /// The for the operation. + /// A representing the running operation. + Task CreateDump(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 6f2b0341d1..af48e18841 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -18,6 +18,7 @@ using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.Components.Watchdog @@ -114,6 +115,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IRestartRegistration restartRegistration; + /// + /// The pointing to the Diagnostics directory. + /// + readonly IIOManager diagnosticsIOManager; + /// /// used for . /// @@ -919,5 +925,22 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public abstract Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken); + + /// + public async Task CreateDump(CancellationToken cancellationToken) + { + var session = GetActiveController(); + + const string DumpDirectory = "ProcessDumps"; + await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken).ConfigureAwait(false); + + var dumpFileName = diagnosticsIOManager.ResolvePath( + diagnosticsIOManager.ConcatPath( + DumpDirectory, + $"DreamDaemon-{DateTimeOffset.Now.ToFileStamp()}.dmp")); + + Logger.LogInformation("Dumping session to {0}...", dumpFileName); + await session.CreateDump(dumpFileName, cancellationToken).ConfigureAwait(false); + } } } diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 707f1d9309..5ce0fdfb2c 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -290,5 +290,37 @@ namespace Tgstation.Server.Host.Controllers await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false); return Accepted(job.ToApi()); } + + /// + /// Creates a to generate a DreamDaemon process dump. + /// + /// The for the operation + /// A resulting in the of the request + /// Dump started successfully. + [HttpPost(Routes.Diagnostics)] + [TgsAuthorize(DreamDaemonRights.CreateDump)] + [ProducesResponseType(typeof(Api.Models.Job), 202)] + public async Task CreateDump(CancellationToken cancellationToken) + { + var job = new Models.Job + { + Instance = Instance, + CancelRightsType = RightsType.DreamDaemon, + CancelRight = (ulong)DreamDaemonRights.CreateDump, + StartedBy = AuthenticationContext.User, + Description = "Create DreamDaemon Process Dump" + }; + + var watchdog = instanceManager.GetInstance(Instance).Watchdog; + + if (!watchdog.Running) + return Conflict(new ErrorMessage(ErrorCode.WatchdogNotRunning)); + + await jobManager.RegisterOperation( + job, + (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.CreateDump(ct), cancellationToken) + .ConfigureAwait(false); + return Accepted(job.ToApi()); + } } } diff --git a/src/Tgstation.Server.Host/Extensions/DateTimeOffsetExtensions.cs b/src/Tgstation.Server.Host/Extensions/DateTimeOffsetExtensions.cs new file mode 100644 index 0000000000..f3f36d3b23 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/DateTimeOffsetExtensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for the . + /// + static class DateTimeOffsetExtensions + { + /// + /// Convert a given into a that can be used to stamp file creation times. + /// + /// The to convert. + /// as a file stamp . + public static string ToFileStamp(this DateTimeOffset dateTimeOffset) + => dateTimeOffset.ToString("yyyyMMddhhmmss", CultureInfo.InvariantCulture); + } +} diff --git a/src/Tgstation.Server.Host/NativeMethods.cs b/src/Tgstation.Server.Host/NativeMethods.cs index afc7211071..bdace24b63 100644 --- a/src/Tgstation.Server.Host/NativeMethods.cs +++ b/src/Tgstation.Server.Host/NativeMethods.cs @@ -5,7 +5,7 @@ using System.Text; namespace Tgstation.Server.Host { /// - /// Native Windows methods used by the code + /// Native Windows methods used by the code. /// #pragma warning disable SA1600 #pragma warning disable SA1602 @@ -32,6 +32,15 @@ namespace Tgstation.Server.Host SuspendResume = 0x0002, } + /// + /// See https://docs.microsoft.com/en-us/windows/win32/api/minidumpapiset/ne-minidumpapiset-minidump_type + /// + [Flags] + public enum MiniDumpType : uint + { + Normal = 0x00000000 + } + /// /// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getwindowthreadprocessid /// @@ -102,5 +111,18 @@ namespace Tgstation.Server.Host /// [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern uint ResumeThread(IntPtr hThread); + + /// + /// See https://docs.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump + /// + [DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool MiniDumpWriteDump( + IntPtr hProcess, + uint processId, + SafeHandle hFile, + MiniDumpType dumpType, + IntPtr expParam, + IntPtr userStreamParam, + IntPtr callbackParam); } } diff --git a/src/Tgstation.Server.Host/System/IProcessBase.cs b/src/Tgstation.Server.Host/System/IProcessBase.cs index a9013d567a..c861f8ae5f 100644 --- a/src/Tgstation.Server.Host/System/IProcessBase.cs +++ b/src/Tgstation.Server.Host/System/IProcessBase.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace Tgstation.Server.Host.System @@ -27,5 +28,13 @@ namespace Tgstation.Server.Host.System /// Resumes the process. /// void Resume(); + + /// + /// Create a dump file of the process. + /// + /// The full path to the output file. + /// The for the operation. + /// A representing the running operation. + Task CreateDump(string outputFile, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/System/IProcessFeatures.cs b/src/Tgstation.Server.Host/System/IProcessFeatures.cs index c56d03f5d0..b45d0831e0 100644 --- a/src/Tgstation.Server.Host/System/IProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/IProcessFeatures.cs @@ -17,7 +17,7 @@ namespace Tgstation.Server.Host.System Task GetExecutingUsername(global::System.Diagnostics.Process process, CancellationToken cancellationToken); /// - /// Suspend a given . + /// Suspend a given . /// /// The to suspend. void SuspendProcess(global::System.Diagnostics.Process process); @@ -27,5 +27,14 @@ namespace Tgstation.Server.Host.System /// /// The to susperesumend. void ResumeProcess(global::System.Diagnostics.Process process); + + /// + /// Create a dump file for a given . + /// + /// The to dump. + /// The full path to the output file. + /// The for the operation. + /// A representing the running operation. + Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index 751bd47eff..452ce62af0 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -93,5 +93,11 @@ namespace Tgstation.Server.Host.System .FirstOrDefault(x => !String.IsNullOrWhiteSpace(x)) ?? "UNPARSABLE"; } + + /// + public Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } } } diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index 044f5fb34c..5f2c3cf5a6 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -165,5 +165,15 @@ namespace Tgstation.Server.Host.System logger.LogTrace("PID {0} Username: {1}", Id, result); return result; } + + /// + public Task CreateDump(string outputFile, CancellationToken cancellationToken) + { + if (outputFile == null) + throw new ArgumentNullException(nameof(outputFile)); + + logger.LogTrace("Dumping PID {0} to {1}...", Id, outputFile); + return processFeatures.CreateDump(handle, outputFile, cancellationToken); + } } } diff --git a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs index 4ea3a49a74..a34bb41687 100644 --- a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using System; using System.Diagnostics; +using System.IO; using System.Linq; using System.Management; using System.Threading; @@ -110,5 +111,28 @@ namespace Tgstation.Server.Host.System return Task.FromResult("NO OWNER"); } + + /// + public async Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) + { + await Task.Factory.StartNew( + () => + { + using var fileStream = new FileStream(outputFile, FileMode.CreateNew); + if (!NativeMethods.MiniDumpWriteDump( + process.Handle, + (uint)process.Id, + fileStream.SafeFileHandle, + NativeMethods.MiniDumpType.Normal, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero)) + throw new Win32Exception(); + }, + cancellationToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Current) + .ConfigureAwait(false); + } } }