mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-23 04:57:17 +01:00
+3
-3
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user