Use dotnet-dump when dumping OpenDream

Closes #1750
This commit is contained in:
Jordan Dominion
2024-01-31 23:12:11 -05:00
parent a0ab7fca09
commit 8d9233659f
19 changed files with 445 additions and 35 deletions
+3 -3
View File
@@ -5,10 +5,10 @@
<PropertyGroup>
<TgsCoreVersion>6.1.5</TgsCoreVersion>
<TgsConfigVersion>5.1.0</TgsConfigVersion>
<TgsApiVersion>10.0.0</TgsApiVersion>
<TgsApiVersion>10.1.0</TgsApiVersion>
<TgsCommonLibraryVersion>7.0.0</TgsCommonLibraryVersion>
<TgsApiLibraryVersion>13.0.1</TgsApiLibraryVersion>
<TgsClientVersion>15.0.1</TgsClientVersion>
<TgsApiLibraryVersion>14.0.0</TgsApiLibraryVersion>
<TgsClientVersion>16.0.0</TgsClientVersion>
<TgsDmapiVersion>7.0.2</TgsDmapiVersion>
<TgsInteropVersion>5.8.0</TgsInteropVersion>
<TgsHostWatchdogVersion>1.4.1</TgsHostWatchdogVersion>
+12 -6
View File
@@ -528,10 +528,10 @@ namespace Tgstation.Server.Api.Models
MissingGCore,
/// <summary>
/// Non-zero gcore exit code.
/// Non-zero gcore/dotnet-dump exit code.
/// </summary>
[Description("Could not create dump as gcore exited with a non-zero exit code!")]
GCoreFailure,
[Description("Could not create dump as the dumping process exited with a non-zero exit code!")]
DumpProcessFailure,
/// <summary>
/// Attempted to test merge with an invalid remote repository.
@@ -636,15 +636,21 @@ namespace Tgstation.Server.Api.Models
BroadcastFailure,
/// <summary>
/// Could not compile OpenDream due to a missing dotnet executable.
/// Unable to locate the dotnet executable for a necessary operation.
/// </summary>
[Description("OpenDream could not be compiled due to being unable to locate the dotnet executable!")]
OpenDreamCantFindDotnet,
[Description("Unable to locate the dotnet executable!")]
CantFindDotnet,
/// <summary>
/// Could not install OpenDream due to it not meeting the minimum version requirements.
/// </summary>
[Description("The specified OpenDream version is too old!")]
OpenDreamTooOld,
/// <summary>
/// Could not locally install the dotnet-dump tool.
/// </summary>
[Description("Could not locally install the dotnet-dump tool!")]
CantInstallDotnetDump,
}
}
@@ -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; }
@@ -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; }
@@ -14,6 +14,7 @@ using Tgstation.Server.Common.Extensions;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.System;
using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Engine
@@ -59,6 +60,11 @@ namespace Tgstation.Server.Host.Components.Engine
/// </summary>
readonly IEventConsumer eventConsumer;
/// <summary>
/// The <see cref="IDotnetDumpService"/> for the <see cref="EngineManager"/>.
/// </summary>
readonly IDotnetDumpService dotnetDumpService;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="EngineManager"/>.
/// </summary>
@@ -100,12 +106,14 @@ namespace Tgstation.Server.Host.Components.Engine
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
/// <param name="engineInstaller">The value of <see cref="engineInstaller"/>.</param>
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/>.</param>
/// <param name="dotnetDumpService">The value of <see cref="dotnetDumpService"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public EngineManager(IIOManager ioManager, IEngineInstaller engineInstaller, IEventConsumer eventConsumer, ILogger<EngineManager> logger)
public EngineManager(IIOManager ioManager, IEngineInstaller engineInstaller, IEventConsumer eventConsumer, IDotnetDumpService dotnetDumpService, ILogger<EngineManager> logger)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.engineInstaller = engineInstaller ?? throw new ArgumentNullException(nameof(engineInstaller));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.dotnetDumpService = dotnetDumpService ?? throw new ArgumentNullException(nameof(dotnetDumpService));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
installedVersions = new Dictionary<EngineVersion, ReferenceCountingContainer<IEngineInstallation, EngineExecutableLock>>();
@@ -380,6 +388,23 @@ namespace Tgstation.Server.Host.Components.Engine
await ioManager.DeleteFile(ActiveVersionFileName, cancellationToken);
}
}
bool needsDotnetDump;
lock (installedVersions)
needsDotnetDump = installedVersions.Values.Any(container => container.Instance.UseDotnetDump);
if (needsDotnetDump)
{
logger.LogDebug("One or more engine installations uses dotnet-dump. Ensuring installation...");
try
{
await dotnetDumpService.EnsureInstalled(true, cancellationToken);
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to install dotnet-dump! Engine versions that use it will instead use standard process dumps!");
}
}
}
/// <inheritdoc />
@@ -473,6 +498,14 @@ namespace Tgstation.Server.Host.Components.Engine
var versionString = version.ToString();
await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List<string> { versionString }, false, cancellationToken);
if (installLock.UseDotnetDump)
{
if (progressReporter != null)
progressReporter.StageName = "Installing dotnet-dump";
await dotnetDumpService.EnsureInstalled(false, cancellationToken);
}
await InstallVersionFiles(progressReporter, version, customVersionStream, cancellationToken);
ourTcs.SetResult();
@@ -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>
@@ -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;
@@ -232,21 +231,7 @@ 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)
throw new JobException(ErrorCode.OpenDreamCantFindDotnet);
var dotnetPath = dotnetPaths[selectedPathIndex];
var dotnetPath = await DotnetHelper.GetDotnetPath(platformIdentifier, IOManager, cancellationToken);
const string DeployDir = "tgs_deploy";
int? buildExitCode = null;
await HandleExtremelyLongPathOperation(
@@ -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,12 @@ 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,
dotnetDumpService,
loggerFactory.CreateLogger<EngineManager>());
var dmbFactory = new DmbFactory(
databaseContextFactory,
@@ -309,6 +322,7 @@ namespace Tgstation.Server.Host.Components
serverPortProvider,
eventConsumer,
asyncDelayer,
dotnetDumpService,
loggerFactory,
loggerFactory.CreateLogger<SessionControllerFactory>(),
sessionConfiguration,
@@ -149,6 +149,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 +241,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 +259,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 +279,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 +482,14 @@ namespace Tgstation.Server.Host.Components.Session
cancellationToken);
/// <inheritdoc />
public ValueTask CreateDump(string outputFile, CancellationToken cancellationToken) => process.CreateDump(outputFile, cancellationToken);
public async ValueTask CreateDump(string outputFile, CancellationToken cancellationToken)
{
if (engineLock.UseDotnetDump
&& await dotnetDumpService.Dump(process, outputFile, cancellationToken))
return;
await 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,
@@ -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,225 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.System
{
/// <inheritdoc />
sealed class DotnetDumpService : IDotnetDumpService, IDisposable
{
/// <summary>
/// The <see cref="IProcessExecutor"/> for the <see cref="DotnetDumpService"/>.
/// </summary>
readonly IProcessExecutor processExecutor;
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="DotnetDumpService"/>.
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="IAssemblyInformationProvider"/> for the <see cref="DotnetDumpService"/>.
/// </summary>
readonly IAssemblyInformationProvider assemblyInformationProvider;
/// <summary>
/// The <see cref="IPlatformIdentifier"/> for the <see cref="DotnetDumpService"/>.
/// </summary>
readonly IPlatformIdentifier platformIdentifier;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="DotnetDumpService"/>.
/// </summary>
readonly ILogger<DotnetDumpService> logger;
/// <summary>
/// The <see cref="SessionConfiguration"/> for the <see cref="DotnetDumpService"/>.
/// </summary>
readonly SessionConfiguration sessionConfiguration;
/// <summary>
/// <see cref="SemaphoreSlim"/> used for checking for the presence of and installing dotnet-dump.
/// </summary>
readonly SemaphoreSlim installCheckSemaphore;
/// <summary>
/// The result of the last installation check. <see langword="true"/> means installed. <see langword="false"/> means not installed. <see langword="null"/> means the check was never run.
/// </summary>
bool? lastInstallCheckResult;
/// <summary>
/// Initializes a new instance of the <see cref="DotnetDumpService"/> class.
/// </summary>
/// <param name="processExecutor">The value of <see cref="processExecutor"/>.</param>
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
/// <param name="assemblyInformationProvider">The value of <see cref="assemblyInformationProvider"/>.</param>
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="sessionConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="sessionConfiguration"/>.</param>
public DotnetDumpService(
IProcessExecutor processExecutor,
IIOManager ioManager,
IAssemblyInformationProvider assemblyInformationProvider,
IPlatformIdentifier platformIdentifier,
ILogger<DotnetDumpService> logger,
IOptions<SessionConfiguration> sessionConfigurationOptions)
{
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
sessionConfiguration = sessionConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(sessionConfigurationOptions));
installCheckSemaphore = new SemaphoreSlim(1);
}
/// <inheritdoc />
public void Dispose() => installCheckSemaphore.Dispose();
/// <inheritdoc />
public async ValueTask EnsureInstalled(bool deploymentPipeline, CancellationToken cancellationToken)
{
logger.LogTrace("EnsureInstalled");
if (lastInstallCheckResult == true)
return;
using (await SemaphoreSlimContext.Lock(installCheckSemaphore, cancellationToken))
{
var installDir = await CheckInstalled(cancellationToken);
if (lastInstallCheckResult == true)
return;
await Install(installDir ?? GetDirectoryPath(), deploymentPipeline, cancellationToken);
}
}
/// <inheritdoc />
public async ValueTask<bool> Dump(IProcess process, string outputFile, CancellationToken cancellationToken)
{
logger.LogTrace("dotnet-dump requested...");
string? installDir = null;
if (!lastInstallCheckResult.HasValue)
using (await SemaphoreSlimContext.Lock(installCheckSemaphore, cancellationToken))
installDir = await CheckInstalled(cancellationToken);
if (lastInstallCheckResult != true)
return false;
installDir ??= GetDirectoryPath();
var exeExtension = platformIdentifier.IsWindows
? ".exe"
: String.Empty;
var resolvedInstallDir = ioManager.ResolvePath(installDir);
var executablePath = ioManager.ConcatPath(
resolvedInstallDir,
$"dotnet-dump{exeExtension}");
await using var dumpProcess = processExecutor.LaunchProcess(
executablePath,
resolvedInstallDir,
$"collect -p {process.Id} -o \"{outputFile}\"",
readStandardHandles: true,
noShellExecute: true);
int? exitCode;
using (cancellationToken.Register(() => dumpProcess.Terminate()))
exitCode = await dumpProcess.Lifetime;
var output = await dumpProcess.GetCombinedOutput(cancellationToken);
if (exitCode != 0)
throw new JobException(
ErrorCode.DumpProcessFailure,
new JobException(
$"Exit Code: {exitCode}{Environment.NewLine}Output:{Environment.NewLine}{output}"));
logger.LogDebug("dotnet-dump output:{newline}{output}", Environment.NewLine, output);
return true;
}
/// <summary>
/// Sets <see cref="lastInstallCheckResult"/> if it is <see langword="null"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns><see langword="null"/> if <see cref="lastInstallCheckResult"/> was not <see langword="null"/>. The result of <see cref="GetDirectoryPath"/> otherwise.</returns>
async ValueTask<string?> CheckInstalled(CancellationToken cancellationToken)
{
if (lastInstallCheckResult.HasValue)
return null;
logger.LogTrace("Checking if dotnet-dump is installed...");
var directory = GetDirectoryPath();
lastInstallCheckResult = await ioManager.DirectoryExists(directory, cancellationToken);
logger.LogTrace("dotnet-dump installed: {result}", lastInstallCheckResult.Value);
return directory;
}
/// <summary>
/// Locally install the dotnet-dump tool.
/// </summary>
/// <param name="installDir">The directory to install dotnet dump in.</param>
/// <param name="deploymentPipeline">If this 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 Install(string installDir, bool deploymentPipeline, CancellationToken cancellationToken)
{
var dotnetPath = await DotnetHelper.GetDotnetPath(platformIdentifier, ioManager, cancellationToken);
logger.LogTrace("Ensuring installation directory is gone...");
await ioManager.DeleteDirectory(installDir, cancellationToken);
var resolvedInstallDir = ioManager.ResolvePath(installDir);
logger.LogTrace("Installing dotnet-dump...");
await using var installProcess = processExecutor.LaunchProcess(
dotnetPath,
ioManager.ResolvePath(),
$"tool install --tool-path \"{resolvedInstallDir}\" dotnet-dump",
readStandardHandles: true,
noShellExecute: true);
if (deploymentPipeline && sessionConfiguration.LowPriorityDeploymentProcesses)
installProcess.AdjustPriority(false);
int? exitCode;
using (cancellationToken.Register(() => installProcess.Terminate()))
exitCode = await installProcess.Lifetime;
var output = await installProcess.GetCombinedOutput(cancellationToken);
if (exitCode != 0)
throw new JobException(
ErrorCode.CantInstallDotnetDump,
new JobException(
$"Exit Code: {exitCode}{Environment.NewLine}Output:{Environment.NewLine}{output}"));
logger.LogDebug("dotnet tool install output:{newline}{output}", Environment.NewLine, output);
}
/// <summary>
/// Get the path to the dotnet-dump installation directory TGS uses.
/// </summary>
/// <returns>The path to the dotnet-dump installation directory.</returns>
string GetDirectoryPath() => ioManager.ConcatPath(
ioManager.GetPathInLocalDirectory(assemblyInformationProvider),
"dotnet-dump");
}
}
@@ -0,0 +1,47 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
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 dotnet executable path to use.</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)
throw new JobException(ErrorCode.CantFindDotnet);
var dotnetPath = dotnetPaths[selectedPathIndex];
return dotnetPath;
}
}
}
@@ -0,0 +1,28 @@
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 install dotnet-dump if it is not installed.
/// </summary>
/// <param name="deploymentPipeline">If this 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>
ValueTask EnsureInstalled(bool deploymentPipeline, CancellationToken cancellationToken);
/// <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><see langword="true"/> if the dump proceeded, <see langword="false"/> if dotnet-dump was not installed.</returns>
ValueTask<bool> Dump(IProcess process, string outputFile, CancellationToken cancellationToken);
}
}
@@ -104,7 +104,7 @@ namespace Tgstation.Server.Host.System
if (exitCode != 0)
throw new JobException(
ErrorCode.GCoreFailure,
ErrorCode.DumpProcessFailure,
new JobException(
$"Exit Code: {exitCode}{Environment.NewLine}Output:{Environment.NewLine}{output}"));
@@ -575,7 +575,7 @@ namespace Tgstation.Server.Tests.Live.Instance
await WaitForJob(restartJob, 20, false, null, cancellationToken);
}
Assert.IsTrue(job.ErrorCode == ErrorCode.GameServerOffline || job.ErrorCode == ErrorCode.GCoreFailure, $"{job.ErrorCode}: {job.ExceptionDetails}");
Assert.IsTrue(job.ErrorCode == ErrorCode.GameServerOffline || job.ErrorCode == ErrorCode.DumpProcessFailure, $"{job.ErrorCode}: {job.ExceptionDetails}");
var restartJob2 = await instanceClient.DreamDaemon.Restart(cancellationToken);
await WaitForJob(restartJob2, 20, false, null, cancellationToken);