Merge pull request #1784 from tgstation/1750-DotnetDump

Use `dotnet-dump` for OpenDream
This commit is contained in:
Jordan Dominion
2024-02-02 12:36:53 -05:00
committed by GitHub
27 changed files with 280 additions and 51 deletions
@@ -33,6 +33,9 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
public override bool PreferFileLogging => false;
/// <inheritdoc />
public override bool UseDotnetDump => false;
/// <inheritdoc />
public override Task InstallationTask { get; }
@@ -41,8 +41,8 @@ namespace Tgstation.Server.Host.Components.Engine
=> DelegateCall(version, installer => installer.DownloadVersion(version, jobProgressReporter, cancellationToken));
/// <inheritdoc />
public ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken)
=> DelegateCall(version, installer => installer.Install(version, path, cancellationToken));
public ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
=> DelegateCall(version, installer => installer.Install(version, path, deploymentPipelineProcesses, cancellationToken));
/// <inheritdoc />
public ValueTask TrustDmbPath(EngineVersion version, string fullDmbPath, CancellationToken cancellationToken)
@@ -36,6 +36,9 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
public Task InstallationTask => Instance.InstallationTask;
/// <inheritdoc />
public bool UseDotnetDump => Instance.UseDotnetDump;
/// <inheritdoc />
public void DoNotDeleteThisSession() => DangerousDropReference();
@@ -39,6 +39,9 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
public abstract bool PromptsForNetworkAccess { get; }
/// <inheritdoc />
public abstract bool UseDotnetDump { get; }
/// <inheritdoc />
public abstract Task InstallationTask { get; }
@@ -46,7 +46,7 @@ namespace Tgstation.Server.Host.Components.Engine
public abstract Task CleanCache(CancellationToken cancellationToken);
/// <inheritdoc />
public abstract ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken);
public abstract ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken);
/// <inheritdoc />
public abstract ValueTask UpgradeInstallation(EngineVersion version, string path, CancellationToken cancellationToken);
@@ -438,6 +438,7 @@ namespace Tgstation.Server.Host.Components.Engine
installLock = installationContainer.AddReference();
}
var deploymentPipelineProcesses = !neededForLock;
try
{
if (installedOrInstalling)
@@ -471,18 +472,18 @@ namespace Tgstation.Server.Host.Components.Engine
progressReporter.StageName = "Running event";
var versionString = version.ToString();
await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List<string> { versionString }, false, cancellationToken);
await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List<string> { versionString }, deploymentPipelineProcesses, cancellationToken);
await InstallVersionFiles(progressReporter, version, customVersionStream, cancellationToken);
await InstallVersionFiles(progressReporter, version, customVersionStream, deploymentPipelineProcesses, cancellationToken);
ourTcs.SetResult();
await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List<string> { versionString }, false, cancellationToken);
await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List<string> { versionString }, deploymentPipelineProcesses, cancellationToken);
}
catch (Exception ex)
{
if (ex is not OperationCanceledException)
await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List<string> { ex.Message }, false, cancellationToken);
await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List<string> { ex.Message }, deploymentPipelineProcesses, cancellationToken);
lock (installedVersions)
installedVersions.Remove(version);
@@ -506,9 +507,15 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="progressReporter">The optional <see cref="JobProgressReporter"/> for the operation.</param>
/// <param name="version">The <see cref="EngineVersion"/> being installed with the <see cref="Version.Build"/> number set if appropriate.</param>
/// <param name="customVersionStream">Custom zip file <see cref="Stream"/> to use. Will cause a <see cref="Version.Build"/> number to be added.</param>
/// <param name="deploymentPipelineProcesses">If processes should be launched as part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask InstallVersionFiles(JobProgressReporter? progressReporter, EngineVersion version, Stream? customVersionStream, CancellationToken cancellationToken)
async ValueTask InstallVersionFiles(
JobProgressReporter? progressReporter,
EngineVersion version,
Stream? customVersionStream,
bool deploymentPipelineProcesses,
CancellationToken cancellationToken)
{
var installFullPath = ioManager.ResolvePath(version.ToString());
async ValueTask DirectoryCleanup()
@@ -554,7 +561,7 @@ namespace Tgstation.Server.Host.Components.Engine
if (progressReporter != null)
progressReporter.StageName = "Running installation actions";
await engineInstaller.Install(version, installFullPath, cancellationToken);
await engineInstaller.Install(version, installFullPath, deploymentPipelineProcesses, cancellationToken);
if (progressReporter != null)
progressReporter.StageName = "Writing version file";
@@ -46,6 +46,11 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
bool PreferFileLogging { get; }
/// <summary>
/// If dotnet-dump should be used to create process dumps for this installation.
/// </summary>
bool UseDotnetDump { get; }
/// <summary>
/// The <see cref="Task"/> that completes when the BYOND version finished installing.
/// </summary>
@@ -34,9 +34,10 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
/// <param name="version">The <see cref="EngineVersion"/> being installed.</param>
/// <param name="path">The path to the installation.</param>
/// <param name="deploymentPipelineProcesses">If the operation should consider processes it launches to be part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken);
ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken);
/// <summary>
/// Does actions necessary to get upgrade a version installed by a previous version of TGS.
@@ -44,6 +44,9 @@ namespace Tgstation.Server.Host.Components.Engine
/// <inheritdoc />
public override bool PreferFileLogging => true;
/// <inheritdoc />
public override bool UseDotnetDump => true;
/// <inheritdoc />
public override Task InstallationTask { get; }
@@ -9,7 +9,6 @@ using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Common.Http;
using Tgstation.Server.Host.Common;
using Tgstation.Server.Host.Components.Repository;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.IO;
@@ -193,7 +192,7 @@ namespace Tgstation.Server.Host.Components.Engine
}
/// <inheritdoc />
public override async ValueTask Install(EngineVersion version, string installPath, CancellationToken cancellationToken)
public override async ValueTask Install(EngineVersion version, string installPath, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
{
CheckVersionValidity(version);
ArgumentNullException.ThrowIfNull(installPath);
@@ -232,21 +231,10 @@ namespace Tgstation.Server.Host.Components.Engine
await Task.WhenAll(dirsMoveTasks.Concat(filesMoveTask));
}
var dotnetPaths = DotnetHelper.GetPotentialDotnetPaths(platformIdentifier.IsWindows)
.ToList();
var tasks = dotnetPaths
.Select(path => IOManager.FileExists(path, cancellationToken))
.ToList();
await Task.WhenAll(tasks);
var selectedPathIndex = tasks.FindIndex(pathValidTask => pathValidTask.Result);
if (selectedPathIndex == -1)
var dotnetPath = await DotnetHelper.GetDotnetPath(platformIdentifier, IOManager, cancellationToken);
if (dotnetPath == null)
throw new JobException(ErrorCode.OpenDreamCantFindDotnet);
var dotnetPath = dotnetPaths[selectedPathIndex];
const string DeployDir = "tgs_deploy";
int? buildExitCode = null;
await HandleExtremelyLongPathOperation(
@@ -262,7 +250,7 @@ namespace Tgstation.Server.Host.Components.Engine
!GeneralConfiguration.OpenDreamSuppressInstallOutput,
!GeneralConfiguration.OpenDreamSuppressInstallOutput);
if (SessionConfiguration.LowPriorityDeploymentProcesses)
if (deploymentPipelineProcesses && SessionConfiguration.LowPriorityDeploymentProcesses)
buildProcess.AdjustPriority(false);
using (cancellationToken.Register(() => buildProcess.Terminate()))
@@ -71,7 +71,7 @@ namespace Tgstation.Server.Host.Components.Engine
}
/// <inheritdoc />
public override ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken)
public override ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
{
CheckVersionValidity(version);
ArgumentNullException.ThrowIfNull(path);
@@ -127,7 +127,7 @@ namespace Tgstation.Server.Host.Components.Engine
public void Dispose() => semaphore.Dispose();
/// <inheritdoc />
public override ValueTask Install(EngineVersion version, string path, CancellationToken cancellationToken)
public override ValueTask Install(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
{
CheckVersionValidity(version);
ArgumentNullException.ThrowIfNull(path);
@@ -142,7 +142,7 @@ namespace Tgstation.Server.Host.Components.Engine
if (!generalConfiguration.SkipAddingByondFirewallException)
{
var firewallTask = AddDreamDaemonToFirewall(version, path, cancellationToken);
var firewallTask = AddDreamDaemonToFirewall(version, path, deploymentPipelineProcesses, cancellationToken);
tasks.Add(firewallTask);
}
@@ -165,7 +165,7 @@ namespace Tgstation.Server.Host.Components.Engine
return;
Logger.LogInformation("BYOND Version {version} needs dd.exe added to firewall", version);
await AddDreamDaemonToFirewall(version, path, cancellationToken);
await AddDreamDaemonToFirewall(version, path, true, cancellationToken);
}
/// <inheritdoc />
@@ -243,9 +243,10 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
/// <param name="version">The BYOND <see cref="EngineVersion"/>.</param>
/// <param name="path">The path to the BYOND installation.</param>
/// <param name="deploymentPipelineProcesses">If the operation is part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask AddDreamDaemonToFirewall(EngineVersion version, string path, CancellationToken cancellationToken)
async ValueTask AddDreamDaemonToFirewall(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
{
var dreamDaemonName = GetDreamDaemonName(version.Version!, out var usesDDExe);
@@ -268,7 +269,7 @@ namespace Tgstation.Server.Host.Components.Engine
Logger,
ruleName,
dreamDaemonPath,
sessionConfiguration.LowPriorityDeploymentProcesses,
deploymentPipelineProcesses && sessionConfiguration.LowPriorityDeploymentProcesses,
cancellationToken);
}
catch (Exception ex)
@@ -66,15 +66,17 @@ namespace Tgstation.Server.Host.Components.Engine
}
/// <inheritdoc />
public override ValueTask Install(EngineVersion version, string installPath, CancellationToken cancellationToken)
public override ValueTask Install(EngineVersion version, string installPath, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
{
var installTask = base.Install(
version,
installPath,
deploymentPipelineProcesses,
cancellationToken);
var firewallTask = AddServerFirewallException(
version,
installPath,
deploymentPipelineProcesses,
cancellationToken);
return ValueTaskExtensions.WhenAll(installTask, firewallTask);
@@ -101,9 +103,10 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
/// <param name="version">The BYOND <see cref="EngineVersion"/>.</param>
/// <param name="path">The path to the BYOND installation.</param>
/// <param name="deploymentPipelineProcesses">If the operation is part of the deployment pipeline.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask AddServerFirewallException(EngineVersion version, string path, CancellationToken cancellationToken)
async ValueTask AddServerFirewallException(EngineVersion version, string path, bool deploymentPipelineProcesses, CancellationToken cancellationToken)
{
if (GeneralConfiguration.SkipAddingByondFirewallException)
return;
@@ -123,7 +126,7 @@ namespace Tgstation.Server.Host.Components.Engine
Logger,
ruleName,
serverExePath,
SessionConfiguration.LowPriorityDeploymentProcesses,
deploymentPipelineProcesses && SessionConfiguration.LowPriorityDeploymentProcesses,
cancellationToken);
}
catch (Exception ex)
@@ -135,6 +135,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IAsyncDelayer asyncDelayer;
/// <summary>
/// The <see cref="IDotnetDumpService"/> for the <see cref="InstanceFactory"/>.
/// </summary>
readonly IDotnetDumpService dotnetDumpService;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="InstanceFactory"/>.
/// </summary>
@@ -177,6 +182,7 @@ namespace Tgstation.Server.Host.Components
/// <param name="fileTransferService">The value of <see cref="fileTransferService"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The value of <see cref="remoteDeploymentManagerFactory"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="dotnetDumpService">The value of <see cref="dotnetDumpService"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="sessionConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="sessionConfiguration"/>.</param>
public InstanceFactory(
@@ -201,6 +207,7 @@ namespace Tgstation.Server.Host.Components
IFileTransferTicketProvider fileTransferService,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IAsyncDelayer asyncDelayer,
IDotnetDumpService dotnetDumpService,
IOptions<GeneralConfiguration> generalConfigurationOptions,
IOptions<SessionConfiguration> sessionConfigurationOptions)
{
@@ -225,6 +232,7 @@ namespace Tgstation.Server.Host.Components
this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService));
this.remoteDeploymentManagerFactory = remoteDeploymentManagerFactory ?? throw new ArgumentNullException(nameof(remoteDeploymentManagerFactory));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions));
}
@@ -271,7 +279,11 @@ namespace Tgstation.Server.Host.Components
var repoManager = repositoryManagerFactory.CreateRepositoryManager(repoIoManager, eventConsumer);
try
{
var engineManager = new EngineManager(byondIOManager, engineInstaller, eventConsumer, loggerFactory.CreateLogger<EngineManager>());
var engineManager = new EngineManager(
byondIOManager,
engineInstaller,
eventConsumer,
loggerFactory.CreateLogger<EngineManager>());
var dmbFactory = new DmbFactory(
databaseContextFactory,
@@ -309,6 +321,7 @@ namespace Tgstation.Server.Host.Components
serverPortProvider,
eventConsumer,
asyncDelayer,
dotnetDumpService,
loggerFactory,
loggerFactory.CreateLogger<SessionControllerFactory>(),
sessionConfiguration,
@@ -84,6 +84,11 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
bool DMApiAvailable { get; }
/// <summary>
/// The file extension to use for process dumps created from this session.
/// </summary>
string DumpFileExtension { get; }
/// <summary>
/// Releases the <see cref="IProcess"/> without terminating it. Also calls <see cref="IDisposable.Dispose"/>.
/// </summary>
@@ -104,6 +104,11 @@ namespace Tgstation.Server.Host.Components.Session
/// <inheritdoc />
public bool ProcessingRebootBridgeRequest => rebootBridgeRequestsProcessing > 0;
/// <inheritdoc />
public string DumpFileExtension => engineLock.UseDotnetDump
? ".net.dmp"
: ".dmp";
/// <summary>
/// The up to date <see cref="Session.ReattachInformation"/>.
/// </summary>
@@ -149,6 +154,11 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
readonly IAsyncDelayer asyncDelayer;
/// <summary>
/// The <see cref="IDotnetDumpService"/> for the <see cref="SessionController"/>.
/// </summary>
readonly IDotnetDumpService dotnetDumpService;
/// <summary>
/// The <see cref="TaskCompletionSource"/> that completes when DD makes it's first bridge request.
/// </summary>
@@ -236,7 +246,8 @@ namespace Tgstation.Server.Host.Components.Session
/// <param name="chat">The value of <see cref="chat"/>.</param>
/// <param name="chatTrackingContext">The value of <see cref="chatTrackingContext"/>.</param>
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> for the <see cref="SessionController"/>.</param>
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="SessionController"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="dotnetDumpService">The value of <see cref="dotnetDumpService"/>.</param>
/// <param name="logger">The value of <see cref="Chunker.Logger"/>.</param>
/// <param name="postLifetimeCallback">The <see cref="Func{TResult}"/> returning a <see cref="ValueTask"/> to be run after the <paramref name="process"/> ends.</param>
/// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/>.</param>
@@ -253,6 +264,7 @@ namespace Tgstation.Server.Host.Components.Session
IChatManager chat,
IAssemblyInformationProvider assemblyInformationProvider,
IAsyncDelayer asyncDelayer,
IDotnetDumpService dotnetDumpService,
ILogger<SessionController> logger,
Func<ValueTask> postLifetimeCallback,
uint? startupTimeout,
@@ -272,6 +284,7 @@ namespace Tgstation.Server.Host.Components.Session
ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService));
apiValidationSession = apiValidate;
@@ -474,7 +487,13 @@ namespace Tgstation.Server.Host.Components.Session
cancellationToken);
/// <inheritdoc />
public ValueTask CreateDump(string outputFile, CancellationToken cancellationToken) => process.CreateDump(outputFile, cancellationToken);
public ValueTask CreateDump(string outputFile, CancellationToken cancellationToken)
{
if (engineLock.UseDotnetDump)
return dotnetDumpService.Dump(process, outputFile, cancellationToken);
return process.CreateDump(outputFile, cancellationToken);
}
/// <summary>
/// The <see cref="Task{TResult}"/> for <see cref="LaunchResult"/>.
@@ -106,6 +106,11 @@ namespace Tgstation.Server.Host.Components.Session
/// </summary>
readonly IAsyncDelayer asyncDelayer;
/// <summary>
/// The <see cref="IDotnetDumpService"/> for the <see cref="SessionControllerFactory"/>.
/// </summary>
readonly IDotnetDumpService dotnetDumpService;
/// <summary>
/// The <see cref="ILoggerFactory"/> for the <see cref="SessionControllerFactory"/>.
/// </summary>
@@ -178,6 +183,7 @@ namespace Tgstation.Server.Host.Components.Session
/// <param name="serverPortProvider">The value of <see cref="serverPortProvider"/>.</param>
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="dotnetDumpService">The value of <see cref="dotnetDumpService"/>.</param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="sessionConfiguration">The value of <see cref="sessionConfiguration"/>.</param>
@@ -196,6 +202,7 @@ namespace Tgstation.Server.Host.Components.Session
IServerPortProvider serverPortProvider,
IEventConsumer eventConsumer,
IAsyncDelayer asyncDelayer,
IDotnetDumpService dotnetDumpService,
ILoggerFactory loggerFactory,
ILogger<SessionControllerFactory> logger,
SessionConfiguration sessionConfiguration,
@@ -215,6 +222,7 @@ namespace Tgstation.Server.Host.Components.Session
this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.sessionConfiguration = sessionConfiguration ?? throw new ArgumentNullException(nameof(sessionConfiguration));
@@ -346,6 +354,7 @@ namespace Tgstation.Server.Host.Components.Session
chat,
assemblyInformationProvider,
asyncDelayer,
dotnetDumpService,
loggerFactory.CreateLogger<SessionController>(),
() => LogDDOutput(
process,
@@ -436,6 +445,7 @@ namespace Tgstation.Server.Host.Components.Session
chat,
assemblyInformationProvider,
asyncDelayer,
dotnetDumpService,
loggerFactory.CreateLogger<SessionController>(),
() => ValueTask.CompletedTask,
null,
@@ -1225,21 +1225,27 @@ namespace Tgstation.Server.Host.Components.Watchdog
async ValueTask CreateDumpNoLock(CancellationToken cancellationToken)
{
const string DumpDirectory = "ProcessDumps";
var session = GetActiveController();
if (session?.Lifetime.IsCompleted != false)
throw new JobException(ErrorCode.GameServerOffline);
var dumpFileExtension = session.DumpFileExtension;
var dumpFileNameTemplate = diagnosticsIOManager.ResolvePath(
diagnosticsIOManager.ConcatPath(
DumpDirectory,
$"DreamDaemon-{DateTimeOffset.UtcNow.ToFileStamp()}.dmp"));
$"DreamDaemon-{DateTimeOffset.UtcNow.ToFileStamp()}"));
var dumpFileName = dumpFileNameTemplate;
var dumpFileName = $"{dumpFileNameTemplate}{dumpFileExtension}";
var iteration = 0;
while (await diagnosticsIOManager.FileExists(dumpFileName, cancellationToken))
dumpFileName = $"{dumpFileNameTemplate} ({++iteration})";
dumpFileName = $"{dumpFileNameTemplate} ({++iteration}){dumpFileExtension}";
if (iteration == 0)
await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken);
var session = GetActiveController();
if (session?.Lifetime.IsCompleted != false)
if (session.Lifetime.IsCompleted)
throw new JobException(ErrorCode.GameServerOffline);
Logger.LogInformation("Dumping session to {dumpFileName}...", dumpFileName);
@@ -365,11 +365,9 @@ namespace Tgstation.Server.Host.Core
}
// only global repo manager should be for the OD repo
// god help me if we need more
var openDreamRepositoryDirectory = ioManager.ConcatPath(
Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData,
Environment.SpecialFolderOption.DoNotVerify),
assemblyInformationProvider.VersionPrefix,
ioManager.GetPathInLocalDirectory(assemblyInformationProvider),
"OpenDreamRepository");
services.AddSingleton(
services => services
@@ -416,6 +414,7 @@ namespace Tgstation.Server.Host.Core
services.AddSingleton<IChatManagerFactory, ChatManagerFactory>();
services.AddSingleton<IServerUpdater, ServerUpdater>();
services.AddSingleton<IServerUpdateInitiator, ServerUpdateInitiator>();
services.AddSingleton<IDotnetDumpService, DotnetDumpService>();
// configure misc services
services.AddSingleton<IProcessExecutor, ProcessExecutor>();
@@ -0,0 +1,31 @@
using System;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.Extensions
{
/// <summary>
/// Extension methods for <see cref="IIOManager"/>.
/// </summary>
static class IOManagerExtensions
{
/// <summary>
/// Gets the local application data folder used by TGS.
/// </summary>
/// <param name="ioManager">The <see cref="IIOManager"/> to use.</param>
/// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> to use.</param>
/// <returns>The path to the local application data directory used by TGS.</returns>
public static string GetPathInLocalDirectory(this IIOManager ioManager, IAssemblyInformationProvider assemblyInformationProvider)
{
ArgumentNullException.ThrowIfNull(ioManager);
ArgumentNullException.ThrowIfNull(assemblyInformationProvider);
return ioManager.ConcatPath(
Environment.GetFolderPath(
Environment.SpecialFolder.LocalApplicationData, // we use local application data here instead of comman application data because we store stuff here we don't want other users interfering with
Environment.SpecialFolderOption.DoNotVerify),
assemblyInformationProvider.VersionPrefix);
}
}
}
@@ -0,0 +1,48 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Diagnostics.NETCore.Client;
using Microsoft.Extensions.Logging;
namespace Tgstation.Server.Host.System
{
/// <inheritdoc />
sealed class DotnetDumpService : IDotnetDumpService
{
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="DotnetDumpService"/>.
/// </summary>
readonly ILogger<DotnetDumpService> logger;
/// <summary>
/// Initializes a new instance of the <see cref="DotnetDumpService"/> class.
/// </summary>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public DotnetDumpService(
ILogger<DotnetDumpService> logger)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public async ValueTask Dump(IProcess process, string outputFile, CancellationToken cancellationToken)
{
// need to use an extra timeout here because if the process is truly deadlocked. A cooperative dump will hang forever
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
const int TimeoutMinutes = 5;
cts.CancelAfter(TimeSpan.FromMinutes(TimeoutMinutes));
cts.Token.Register(() =>
{
if (!cancellationToken.IsCancellationRequested)
logger.LogError("dotnet-dump timed out after {minutes} minutes!", TimeoutMinutes);
});
var pid = process.Id;
logger.LogDebug("dotnet-dump requested for PID {pid}...", pid);
var client = new DiagnosticsClient(pid);
await client.WriteDumpAsync(DumpType.Full, outputFile, false, cts.Token);
}
}
}
@@ -0,0 +1,45 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.System
{
/// <summary>
/// Helper methods for working with the dotnet executable.
/// </summary>
static class DotnetHelper
{
/// <summary>
/// Locate a dotnet executable to use.
/// </summary>
/// <param name="platformIdentifier">The <see cref="IPlatformIdentifier"/> to use.</param>
/// <param name="ioManager">The <see cref="IIOManager"/> to use.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask{TResult}"/> resulting in a dotnet executable path to use on success, <see langword="null"/> otherwise.</returns>
public static async ValueTask<string?> GetDotnetPath(IPlatformIdentifier platformIdentifier, IIOManager ioManager, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(platformIdentifier);
ArgumentNullException.ThrowIfNull(ioManager);
var dotnetPaths = Common.DotnetHelper.GetPotentialDotnetPaths(platformIdentifier.IsWindows)
.ToList();
var tasks = dotnetPaths
.Select(path => ioManager.FileExists(path, cancellationToken))
.ToList();
await Task.WhenAll(tasks);
var selectedPathIndex = tasks.FindIndex(pathValidTask => pathValidTask.Result);
if (selectedPathIndex == -1)
return null;
var dotnetPath = dotnetPaths[selectedPathIndex];
return dotnetPath;
}
}
}
@@ -0,0 +1,20 @@
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.System
{
/// <summary>
/// Service for managing the dotnet-dump installation.
/// </summary>
public interface IDotnetDumpService
{
/// <summary>
/// Attempt to dump a given <paramref name="process"/>.
/// </summary>
/// <param name="process">The <see cref="IProcess"/> to dump.</param>
/// <param name="outputFile">The path to the output dump file.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
ValueTask Dump(IProcess process, string outputFile, CancellationToken cancellationToken);
}
}
@@ -83,6 +83,8 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="8.0.1" />
<!-- Usage: Using target JSON serializer for API -->
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="8.0.1" />
<!-- Usage: Generating dumps of dotnet engine processes -->
<PackageReference Include="Microsoft.Diagnostics.NETCore.Client" Version="0.2.505301" />
<!-- Usage: Database ORM -->
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.1" />
<!-- Usage: Automatic migration generation using command line -->
@@ -90,7 +90,7 @@ namespace Tgstation.Server.Host.Components.Engine.Tests
var installer = new PosixByondInstaller(mockPostWriteHandler.Object, mockIOManager.Object, mockFileDownloader, mockLogger.Object);
const string FakePath = "fake";
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.Install(null, null, default).AsTask());
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.Install(null, null, false, default).AsTask());
var byondVersion = new EngineVersion
{
@@ -98,10 +98,10 @@ namespace Tgstation.Server.Host.Components.Engine.Tests
Version = new Version(123, 252345),
};
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.Install(byondVersion, null, default).AsTask());
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.Install(byondVersion, null, false, default).AsTask());
byondVersion.Version = new Version(511, 1385);
await installer.Install(byondVersion, FakePath, default);
await installer.Install(byondVersion, FakePath, false, default);
mockPostWriteHandler.Verify(x => x.HandleWrite(It.IsAny<string>()), Times.Exactly(4));
}
@@ -813,7 +813,21 @@ namespace Tgstation.Server.Tests.Live.Instance
ourProcessHandler.SuspendProcess();
global::System.Console.WriteLine($"WATCHDOG TEST {instanceClient.Metadata.Id}: FINISH PROCESS SUSPEND FOR HEALTH CHECK DEATH. WAITING FOR LIFETIME {ourProcessHandler.Id}.");
if (testVersion.Engine == EngineType.OpenDream && checkDump)
{
// because dotnet diagnostics relies on the engine process to write its own dump, we actually have to unpause it after the watchdog has decided to kill it
// incredibly cursed, because we don't have the means to accurately tell when that will happen. ESP in CI
return; // CBA rn
/*
await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
ourProcessHandler.ResumeProcess();
global::System.Console.WriteLine($"WATCHDOG TEST {instanceClient.Metadata.Id}: PROCESS RESUMING FOR DOTNET DUMP. WAITING FOR LIFETIME {ourProcessHandler.Id}.");*/
}
await Task.WhenAny(ourProcessHandler.Lifetime, Task.Delay(TimeSpan.FromMinutes(4), cancellationToken));
if (testVersion.Engine == EngineType.OpenDream && checkDump && !ourProcessHandler.Lifetime.IsCompleted)
return;
Assert.IsTrue(ourProcessHandler.Lifetime.IsCompleted);
var timeout = 20;
+1 -1
View File
@@ -477,7 +477,7 @@ namespace Tgstation.Server.Tests
if (byondInstaller is WindowsByondInstaller)
typeof(WindowsByondInstaller).GetField("installedDirectX", BindingFlags.Instance | BindingFlags.NonPublic).SetValue(byondInstaller, true);
await byondInstaller.Install(engineVersion, tempPath, default);
await byondInstaller.Install(engineVersion, tempPath, false, default);
var binPath = (string)typeof(ByondInstallerBase).GetField("ByondBinPath", BindingFlags.Static | BindingFlags.NonPublic).GetValue(null);
var ddNameFunc = installerType.GetMethod("GetDreamDaemonName", BindingFlags.Instance | BindingFlags.NonPublic);