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