Implement minidumps on Windows

This commit is contained in:
Jordan Brown
2020-06-12 18:06:15 -04:00
parent 6b4699930f
commit 413ef4c32f
15 changed files with 184 additions and 2 deletions
@@ -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,
}
}
+5
View File
@@ -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);
}
}
+23 -1
View File
@@ -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);
}
}
}