mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-30 08:33:19 +01:00
Implement minidumps on Windows
This commit is contained in:
@@ -48,5 +48,10 @@ namespace Tgstation.Server.Api.Models
|
||||
/// If the server is undergoing a soft shutdown
|
||||
/// </summary>
|
||||
public bool? SoftShutdown { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If a dump of the active DreamDaemon executable should be created.
|
||||
/// </summary>
|
||||
public bool? CreateDump { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,5 +77,10 @@ namespace Tgstation.Server.Api.Rights
|
||||
/// User can change <see cref="Models.Internal.DreamDaemonLaunchParameters.HeartbeatSeconds"/>
|
||||
/// </summary>
|
||||
SetHeartbeatInterval = 4096,
|
||||
|
||||
/// <summary>
|
||||
/// User can create DreamDaemon process dumps.
|
||||
/// </summary>
|
||||
CreateDump = 8192,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,6 +43,11 @@ namespace Tgstation.Server.Api
|
||||
/// </summary>
|
||||
public const string DreamDaemon = Root + nameof(Models.DreamDaemon);
|
||||
|
||||
/// <summary>
|
||||
/// For accessing DD diagnostics
|
||||
/// </summary>
|
||||
public const string Diagnostics = DreamDaemon + "/Diagnostics";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Models.ConfigurationFile"/> controller
|
||||
/// </summary>
|
||||
|
||||
@@ -123,5 +123,8 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CreateDump(string outputFile, CancellationToken cancellationToken) => throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -696,5 +696,8 @@ namespace Tgstation.Server.Host.Components.Session
|
||||
new TopicParameters(
|
||||
new ChatUpdate(newChannels)),
|
||||
cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CreateDump(string outputFile, CancellationToken cancellationToken) => process.CreateDump(outputFile, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,5 +80,12 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task ResetRebootState(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to create a process dump for DreamDaemon.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task CreateDump(CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
readonly IRestartRegistration restartRegistration;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> pointing to the Diagnostics directory.
|
||||
/// </summary>
|
||||
readonly IIOManager diagnosticsIOManager;
|
||||
|
||||
/// <summary>
|
||||
/// <see langword="lock"/> <see cref="object"/> used for <see cref="DisposeAndNullControllers"/>.
|
||||
/// </summary>
|
||||
@@ -919,5 +925,22 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
|
||||
/// <inheritdoc />
|
||||
public abstract Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="Api.Models.Job"/> to generate a DreamDaemon process dump.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the request</returns>
|
||||
/// <response code="202">Dump <see cref="Api.Models.Job"/> started successfully.</response>
|
||||
[HttpPost(Routes.Diagnostics)]
|
||||
[TgsAuthorize(DreamDaemonRights.CreateDump)]
|
||||
[ProducesResponseType(typeof(Api.Models.Job), 202)]
|
||||
public async Task<IActionResult> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Tgstation.Server.Host.Extensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Extension methods for the <see cref="DateTimeOffset"/> <see langword="class"/>.
|
||||
/// </summary>
|
||||
static class DateTimeOffsetExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert a given <paramref name="dateTimeOffset"/> into a <see cref="string"/> that can be used to stamp file creation times.
|
||||
/// </summary>
|
||||
/// <param name="dateTimeOffset">The <see cref="DateTimeOffset"/> to convert.</param>
|
||||
/// <returns><paramref name="dateTimeOffset"/> as a file stamp <see cref="string"/>.</returns>
|
||||
public static string ToFileStamp(this DateTimeOffset dateTimeOffset)
|
||||
=> dateTimeOffset.ToString("yyyyMMddhhmmss", CultureInfo.InvariantCulture);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using System.Text;
|
||||
namespace Tgstation.Server.Host
|
||||
{
|
||||
/// <summary>
|
||||
/// Native Windows methods used by the code
|
||||
/// Native Windows methods used by the code.
|
||||
/// </summary>
|
||||
#pragma warning disable SA1600
|
||||
#pragma warning disable SA1602
|
||||
@@ -32,6 +32,15 @@ namespace Tgstation.Server.Host
|
||||
SuspendResume = 0x0002,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See https://docs.microsoft.com/en-us/windows/win32/api/minidumpapiset/ne-minidumpapiset-minidump_type
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MiniDumpType : uint
|
||||
{
|
||||
Normal = 0x00000000
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getwindowthreadprocessid
|
||||
/// </summary>
|
||||
@@ -102,5 +111,18 @@ namespace Tgstation.Server.Host
|
||||
/// </summary>
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
public static extern uint ResumeThread(IntPtr hThread);
|
||||
|
||||
/// <summary>
|
||||
/// See https://docs.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
void Resume();
|
||||
|
||||
/// <summary>
|
||||
/// Create a dump file of the process.
|
||||
/// </summary>
|
||||
/// <param name="outputFile">The full path to the output file.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task CreateDump(string outputFile, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Tgstation.Server.Host.System
|
||||
Task<string> GetExecutingUsername(global::System.Diagnostics.Process process, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Suspend a given <see cref="Process"/>.
|
||||
/// Suspend a given <paramref name="process"/>.
|
||||
/// </summary>
|
||||
/// <param name="process">The <see cref="Process"/> to suspend.</param>
|
||||
void SuspendProcess(global::System.Diagnostics.Process process);
|
||||
@@ -27,5 +27,14 @@ namespace Tgstation.Server.Host.System
|
||||
/// </summary>
|
||||
/// <param name="process">The <see cref="Process"/> to susperesumend.</param>
|
||||
void ResumeProcess(global::System.Diagnostics.Process process);
|
||||
|
||||
/// <summary>
|
||||
/// Create a dump file for a given <paramref name="process"/>.
|
||||
/// </summary>
|
||||
/// <param name="process">The <see cref="Process"/> to dump.</param>
|
||||
/// <param name="outputFile">The full path to the output file.</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
|
||||
Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,5 +93,11 @@ namespace Tgstation.Server.Host.System
|
||||
.FirstOrDefault(x => !String.IsNullOrWhiteSpace(x))
|
||||
?? "UNPARSABLE";
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,5 +165,15 @@ namespace Tgstation.Server.Host.System
|
||||
logger.LogTrace("PID {0} Username: {1}", Id, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user