mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-30 00:22:40 +01:00
Adds IProcess
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
@@ -47,6 +46,11 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IProcessExecutor"/> for the <see cref="WindowsByondInstaller"/>
|
||||
/// </summary>
|
||||
readonly IProcessExecutor processExecutor;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="WindowsByondInstaller"/>
|
||||
/// </summary>
|
||||
@@ -66,10 +70,12 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
/// Construct a <see cref="WindowsByondInstaller"/>
|
||||
/// </summary>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="processExecutor">The value of <see cref="processExecutor"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public WindowsByondInstaller(IIOManager ioManager, ILogger<WindowsByondInstaller> logger)
|
||||
public WindowsByondInstaller(IIOManager ioManager, IProcessExecutor processExecutor, ILogger<WindowsByondInstaller> logger)
|
||||
{
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
semaphore = new SemaphoreSlim(1);
|
||||
@@ -115,43 +121,22 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
//after this version lummox made DD depend of directx lol
|
||||
if (version.Major >= 512 && version.Minor >= 1427 && !installedDirectX)
|
||||
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!installedDirectX)
|
||||
{
|
||||
//always install it, it's pretty fast and will do better redundancy checking than us
|
||||
using (var p = new Process())
|
||||
var rbdx = ioManager.ConcatPath(path, ByondDXDir);
|
||||
using (var p = processExecutor.LaunchProcess(ioManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent"))
|
||||
{
|
||||
p.StartInfo.Arguments = "/silent";
|
||||
var rbdx = ioManager.ConcatPath(path, ByondDXDir);
|
||||
p.StartInfo.FileName = rbdx + "/DXSETUP.exe";
|
||||
p.StartInfo.UseShellExecute = false;
|
||||
p.StartInfo.WorkingDirectory = rbdx;
|
||||
p.EnableRaisingEvents = true;
|
||||
var tcs = new TaskCompletionSource<object>();
|
||||
p.Exited += (a, b) => tcs.TrySetResult(null);
|
||||
try
|
||||
{
|
||||
p.Start();
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled()))
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!p.HasExited)
|
||||
{
|
||||
p.Kill();
|
||||
p.WaitForExit();
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
}
|
||||
int exitCode;
|
||||
using (cancellationToken.Register(() => p.Terminate()))
|
||||
exitCode = await p.Lifetime.ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (p.ExitCode != 0)
|
||||
throw new Exception("Failed to install included DirectX! Exit code: " + p.ExitCode);
|
||||
if (exitCode != 0)
|
||||
throw new Exception(String.Format(CultureInfo.InvariantCulture, "Failed to install included DirectX! Exit code: {0}", exitCode));
|
||||
installedDirectX = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await setNoPromptTrustedModeTask.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -74,6 +73,10 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
/// </summary>
|
||||
readonly IChat chat;
|
||||
/// <summary>
|
||||
/// The <see cref="IProcessExecutor"/> for <see cref="DreamMaker"/>
|
||||
/// </summary>
|
||||
readonly IProcessExecutor processExecutor;
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for <see cref="DreamMaker"/>
|
||||
/// </summary>
|
||||
readonly ILogger<DreamMaker> logger;
|
||||
@@ -89,8 +92,9 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
/// <param name="application">The value of <see cref="application"/></param>
|
||||
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/></param>
|
||||
/// <param name="chat">The value of <see cref="chat"/></param>
|
||||
/// <param name="processExecutor">The value of <see cref="processExecutor"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, IChat chat, ILogger<DreamMaker> logger)
|
||||
public DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, IChat chat, IProcessExecutor processExecutor, ILogger<DreamMaker> logger)
|
||||
{
|
||||
this.byond = byond;
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
@@ -100,6 +104,7 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
this.application = application ?? throw new ArgumentNullException(nameof(application));
|
||||
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
|
||||
this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
@@ -158,50 +163,14 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
async Task RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
using (var dm = new Process())
|
||||
using (var dm = processExecutor.LaunchProcess(dreamMakerPath, ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)), String.Format(CultureInfo.InvariantCulture, "-clean {0}.{1}", job.DmeName, DmeExtension), true, true))
|
||||
{
|
||||
dm.StartInfo.FileName = dreamMakerPath;
|
||||
dm.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "-clean {0}.{1}", job.DmeName, DmeExtension);
|
||||
dm.StartInfo.WorkingDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
|
||||
dm.StartInfo.RedirectStandardOutput = true;
|
||||
dm.StartInfo.RedirectStandardError = true;
|
||||
dm.StartInfo.UseShellExecute = false;
|
||||
var outputList = new StringBuilder();
|
||||
var eventHandler = new DataReceivedEventHandler(
|
||||
delegate (object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
outputList.Append(Environment.NewLine);
|
||||
outputList.Append(e.Data);
|
||||
}
|
||||
);
|
||||
dm.OutputDataReceived += eventHandler;
|
||||
dm.ErrorDataReceived += eventHandler;
|
||||
using (cancellationToken.Register(() => dm.Terminate()))
|
||||
job.ExitCode = await dm.Lifetime.ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
dm.EnableRaisingEvents = true;
|
||||
var dmTcs = new TaskCompletionSource<object>();
|
||||
dm.Exited += (a, b) => dmTcs.TrySetResult(null);
|
||||
|
||||
logger.LogTrace("Running DreamMaker...");
|
||||
dm.Start();
|
||||
dm.BeginOutputReadLine();
|
||||
dm.BeginErrorReadLine();
|
||||
try
|
||||
{
|
||||
using (cancellationToken.Register(() => dmTcs.TrySetCanceled()))
|
||||
await dmTcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!dm.HasExited)
|
||||
{
|
||||
dm.Kill();
|
||||
dm.WaitForExit();
|
||||
}
|
||||
}
|
||||
|
||||
job.ExitCode = dm.ExitCode;
|
||||
logger.LogDebug("DreamMaker exit code: {0}", job.ExitCode);
|
||||
job.Output = outputList.ToString();
|
||||
job.Output = dm.GetCombinedOutput();
|
||||
logger.LogTrace("DreamMaker output: {0}", job.Output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,9 +78,9 @@ namespace Tgstation.Server.Host.Components
|
||||
readonly IProviderFactory providerFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IScriptExecutor"/> for the <see cref="InstanceFactory"/>
|
||||
/// The <see cref="IProcessExecutor"/> for the <see cref="InstanceFactory"/>
|
||||
/// </summary>
|
||||
readonly IScriptExecutor scriptExecutor;
|
||||
readonly IProcessExecutor processExecutor;
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="InstanceFactory"/>
|
||||
@@ -97,8 +97,8 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <param name="symlinkFactory">The value of <see cref="symlinkFactory"/></param>
|
||||
/// <param name="byondInstaller">The value of <see cref="byondInstaller"/></param>
|
||||
/// <param name="providerFactory">The value of <see cref="providerFactory"/></param>
|
||||
/// <param name="scriptExecutor">The value of <see cref="scriptExecutor"/></param>
|
||||
public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerControl serverUpdater, ICryptographySuite cryptographySuite, IExecutor executor, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IProviderFactory providerFactory, IScriptExecutor scriptExecutor)
|
||||
/// <param name="processExecutor">The value of <see cref="processExecutor"/></param>
|
||||
public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerControl serverUpdater, ICryptographySuite cryptographySuite, IExecutor executor, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IProviderFactory providerFactory, IProcessExecutor processExecutor)
|
||||
{
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
|
||||
@@ -112,7 +112,7 @@ namespace Tgstation.Server.Host.Components
|
||||
this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
|
||||
this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
|
||||
this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory));
|
||||
this.scriptExecutor = scriptExecutor ?? throw new ArgumentNullException(nameof(scriptExecutor));
|
||||
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -127,7 +127,7 @@ namespace Tgstation.Server.Host.Components
|
||||
var gameIoManager = new ResolvingIOManager(instanceIoManager, "Game");
|
||||
var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration");
|
||||
|
||||
var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, scriptExecutor, loggerFactory.CreateLogger<StaticFiles.Configuration>());
|
||||
var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, processExecutor, loggerFactory.CreateLogger<StaticFiles.Configuration>());
|
||||
var eventConsumer = new EventConsumer(configuration);
|
||||
|
||||
var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger<DmbFactory>(), metadata.CloneMetadata());
|
||||
@@ -152,7 +152,7 @@ namespace Tgstation.Server.Host.Components
|
||||
commandFactory.SetWatchdog(watchdog);
|
||||
try
|
||||
{
|
||||
var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, chat, loggerFactory.CreateLogger<DreamMaker>());
|
||||
var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, chat, processExecutor, loggerFactory.CreateLogger<DreamMaker>());
|
||||
|
||||
return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, loggerFactory.CreateLogger<Instance>());
|
||||
}
|
||||
|
||||
@@ -50,9 +50,9 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
readonly ISymlinkFactory symlinkFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IScriptExecutor"/> for <see cref="Configuration"/>
|
||||
/// The <see cref="IProcessExecutor"/> for <see cref="Configuration"/>
|
||||
/// </summary>
|
||||
readonly IScriptExecutor scriptExecutor;
|
||||
readonly IProcessExecutor processExecutor;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for <see cref="Configuration"/>
|
||||
@@ -70,14 +70,14 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="synchronousIOManager">The value of <see cref="synchronousIOManager"/></param>
|
||||
/// <param name="symlinkFactory">The value of <see cref="symlinkFactory"/></param>
|
||||
/// <param name="scriptExecutor">The value of <see cref="scriptExecutor"/></param>
|
||||
/// <param name="processExecutor">The value of <see cref="processExecutor"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IScriptExecutor scriptExecutor, ILogger<Configuration> logger)
|
||||
public Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IProcessExecutor processExecutor, ILogger<Configuration> logger)
|
||||
{
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
|
||||
this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
|
||||
this.scriptExecutor = scriptExecutor ?? throw new ArgumentNullException(nameof(scriptExecutor));
|
||||
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
semaphore = new SemaphoreSlim(1);
|
||||
@@ -314,8 +314,14 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
|
||||
|
||||
foreach (var I in files.Select(x => ioManager.GetFileName(x)).Where(x => x.StartsWith(scriptName, StringComparison.Ordinal)))
|
||||
if ((await scriptExecutor.ExecuteScript(ioManager.ConcatPath(resolvedScriptsDir, I), parameters, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
return false;
|
||||
using (var script = processExecutor.LaunchProcess(ioManager.ConcatPath(resolvedScriptsDir, I), String.Concat(parameters)))
|
||||
using (cancellationToken.Register(() => script.Terminate()))
|
||||
{
|
||||
var exitCode = await script.Lifetime.ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (exitCode != 0)
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
{
|
||||
/// <summary>
|
||||
/// For executing system shell scripts
|
||||
/// </summary>
|
||||
interface IScriptExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// Execute a shell script and get the result
|
||||
/// </summary>
|
||||
/// <param name="scriptPath">The absolute path to the script</param>
|
||||
/// <param name="parameters">Command line parameters for the script</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="System.Diagnostics.Process.ExitCode"/> or <see langword="null"/> if an error occurred</returns>
|
||||
Task<int?> ExecuteScript(string scriptPath, IEnumerable<string> parameters, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.IO;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class ScriptExecutor : IScriptExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for the <see cref="ScriptExecutor"/>
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="ScriptExecutor"/>
|
||||
/// </summary>
|
||||
readonly ILogger<ScriptExecutor> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="ScriptExecutor"/>
|
||||
/// </summary>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public ScriptExecutor(IIOManager ioManager, ILogger<ScriptExecutor> logger)
|
||||
{
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<int?> ExecuteScript(string scriptPath, IEnumerable<string> parameters, CancellationToken cancellationToken)
|
||||
{
|
||||
var joinedParams = String.Join(" ", parameters);
|
||||
logger.LogInformation("Running script {0} {1}", scriptPath, joinedParams);
|
||||
try
|
||||
{
|
||||
using (var process = new Process())
|
||||
{
|
||||
process.StartInfo.FileName = scriptPath;
|
||||
process.StartInfo.WorkingDirectory = ioManager.GetDirectoryName(scriptPath);
|
||||
process.StartInfo.Arguments = joinedParams;
|
||||
process.EnableRaisingEvents = true;
|
||||
|
||||
var tcs = new TaskCompletionSource<object>();
|
||||
process.Exited += (a, b) => tcs.TrySetResult(null);
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled()))
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit();
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
}
|
||||
|
||||
return process.ExitCode;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Error running shell script {0}! Exception: {1}", scriptPath, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Api.Models.Internal;
|
||||
using Tgstation.Server.Host.Components.Byond;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class Executor : IExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IProcessExecutor"/> for the <see cref="Executor"/>
|
||||
/// </summary>
|
||||
readonly IProcessExecutor processExecutor;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="Executor"/>
|
||||
/// </summary>
|
||||
@@ -39,14 +44,16 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <summary>
|
||||
/// Construct an <see cref="Executor"/>
|
||||
/// </summary>
|
||||
/// <param name="processExecutor">The value of <see cref="processExecutor"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public Executor(ILogger<Executor> logger)
|
||||
public Executor(IProcessExecutor processExecutor, ILogger<Executor> logger)
|
||||
{
|
||||
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISession AttachToDreamDaemon(int processId, IByondExecutableLock byondLock) => new Session(Process.GetProcessById(processId), byondLock);
|
||||
public ISession AttachToDreamDaemon(int processId, IByondExecutableLock byondLock) => new Session(processExecutor.GetProcess(processId), byondLock);
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISession RunDreamDaemon(DreamDaemonLaunchParameters launchParameters, IByondExecutableLock byondLock, IDmbProvider dmbProvider, string parameters, bool useSecondaryPort, bool useSecondaryDirectory)
|
||||
@@ -60,30 +67,21 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
if (parameters == null)
|
||||
throw new ArgumentNullException(nameof(parameters));
|
||||
|
||||
var proc = new Process();
|
||||
try
|
||||
{
|
||||
proc.StartInfo.FileName = byondLock.DreamDaemonPath;
|
||||
proc.StartInfo.WorkingDirectory = useSecondaryDirectory ? dmbProvider.SecondaryDirectory : dmbProvider.PrimaryDirectory;
|
||||
var fileName = byondLock.DreamDaemonPath;
|
||||
var workingDirectory = useSecondaryDirectory ? dmbProvider.SecondaryDirectory : dmbProvider.PrimaryDirectory;
|
||||
|
||||
proc.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} {2}-close -{3} -verbose -public -params \"{4}\"",
|
||||
dmbProvider.DmbName,
|
||||
useSecondaryPort ? launchParameters.SecondaryPort : launchParameters.PrimaryPort,
|
||||
launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
|
||||
SecurityWord(launchParameters.SecurityLevel.Value),
|
||||
parameters);
|
||||
|
||||
logger.LogTrace("Running DreamDaemon in {0}: {1} {2}", proc.StartInfo.WorkingDirectory, proc.StartInfo.FileName, proc.StartInfo.Arguments);
|
||||
var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} {2}-close -{3} -verbose -public -params \"{4}\"",
|
||||
dmbProvider.DmbName,
|
||||
useSecondaryPort ? launchParameters.SecondaryPort : launchParameters.PrimaryPort,
|
||||
launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
|
||||
SecurityWord(launchParameters.SecurityLevel.Value),
|
||||
parameters);
|
||||
|
||||
proc.Start();
|
||||
logger.LogTrace("Running DreamDaemon in {0}: {1} {2}", workingDirectory, fileName, arguments);
|
||||
|
||||
return new Session(proc, byondLock);
|
||||
}
|
||||
catch
|
||||
{
|
||||
proc.Dispose();
|
||||
throw;
|
||||
}
|
||||
var proc = processExecutor.LaunchProcess(fileName, workingDirectory, arguments);
|
||||
|
||||
return new Session(proc, byondLock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a dream daemon process
|
||||
/// </summary>
|
||||
interface ISession : ISessionBase
|
||||
interface ISession : ISessionBase, IProcess
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="System.Diagnostics.Process.Id"/>
|
||||
/// </summary>
|
||||
int ProcessId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the running process
|
||||
/// </summary>
|
||||
void Terminate();
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,13 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
interface ISessionBase : IDisposable
|
||||
interface ISessionBase : IProcessBase
|
||||
{
|
||||
/// <summary>
|
||||
/// A <see cref="Task"/> that completes when DreamDaemon starts pumping the windows message queue after loading a .dmb or when it crashes
|
||||
/// </summary>
|
||||
Task<LaunchResult> LaunchResult { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="Task"/> representing the lifetime of the <see cref="System.Diagnostics.Process"/> and resulting in the <see cref="System.Diagnostics.Process.ExitCode"/>
|
||||
/// </summary>
|
||||
Task<int> Lifetime { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Components.Byond;
|
||||
using Tgstation.Server.Host.Core;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
@@ -9,78 +9,67 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
sealed class Session : ISession
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int ProcessId => process.Id;
|
||||
public int Id => process.Id;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Startup => process.Startup;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<LaunchResult> LaunchResult { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> Lifetime => lifetimeTask.Task;
|
||||
public Task<int> Lifetime => process.Lifetime;
|
||||
|
||||
/// <summary>
|
||||
/// The actual <see cref="Process"/>
|
||||
/// The actual <see cref="IProcess"/>
|
||||
/// </summary>
|
||||
readonly Process process;
|
||||
readonly IProcess process;
|
||||
/// <summary>
|
||||
/// The <see cref="IByondExecutableLock"/> for the <see cref="process"/>
|
||||
/// </summary>
|
||||
readonly IByondExecutableLock byondLock;
|
||||
|
||||
/// <summary>
|
||||
/// The backing <see cref="TaskCompletionSource{TResult}"/> for <see cref="Lifetime"/>
|
||||
/// </summary>
|
||||
readonly TaskCompletionSource<int> lifetimeTask;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="Session"/>
|
||||
/// </summary>
|
||||
/// <param name="process">The value of <see cref="process"/></param>
|
||||
/// <param name="byondLock">The value of <see cref="byondLock"/></param>
|
||||
public Session(Process process, IByondExecutableLock byondLock)
|
||||
public Session(IProcess process, IByondExecutableLock byondLock)
|
||||
{
|
||||
this.process = process ?? throw new ArgumentNullException(nameof(process));
|
||||
this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock));
|
||||
|
||||
LaunchResult = Task.Factory.StartNew(() =>
|
||||
async Task<LaunchResult> GetLaunchResult()
|
||||
{
|
||||
var startTime = DateTimeOffset.Now;
|
||||
try
|
||||
{
|
||||
process.WaitForInputIdle();
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
await process.Startup.ConfigureAwait(false);
|
||||
var result = new LaunchResult
|
||||
{
|
||||
ExitCode = process.HasExited ? (int?)process.ExitCode : null,
|
||||
ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null,
|
||||
StartupTime = DateTimeOffset.Now - startTime
|
||||
};
|
||||
return result;
|
||||
}, default, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
lifetimeTask = new TaskCompletionSource<int>();
|
||||
try
|
||||
{
|
||||
process.EnableRaisingEvents = true;
|
||||
process.Exited += (a, b) => lifetimeTask.TrySetResult(process.ExitCode);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
//dead proccess
|
||||
lifetimeTask.TrySetResult(process.ExitCode);
|
||||
}
|
||||
};
|
||||
LaunchResult = GetLaunchResult();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => process.Dispose();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Terminate()
|
||||
public void Dispose()
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill();
|
||||
process.WaitForExit();
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
process.Dispose();
|
||||
byondLock.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Terminate() => process.Terminate();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetErrorOutput() => process.GetErrorOutput();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetStandardOutput() => process.GetStandardOutput();
|
||||
|
||||
/// <inheritdoc />
|
||||
public string GetCombinedOutput() => process.GetCombinedOutput();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
Dmb = dmbProvider,
|
||||
IsPrimary = primaryDirectory,
|
||||
Port = portToUse.Value,
|
||||
ProcessId = session.ProcessId,
|
||||
ProcessId = session.Id,
|
||||
ChatChannelsJson = interopInfo.ChatChannelsJson,
|
||||
ChatCommandsJson = interopInfo.ChatCommandsJson,
|
||||
ServerCommandsJson = interopInfo.ServerCommandsJson,
|
||||
|
||||
@@ -198,8 +198,8 @@ namespace Tgstation.Server.Host.Core
|
||||
services.AddSingleton<IByondInstaller, PosixByondInstaller>();
|
||||
}
|
||||
|
||||
services.AddSingleton<IProcessExecutor, ProcessExecutor>();
|
||||
services.AddSingleton<IExecutor, Executor>();
|
||||
services.AddSingleton<IScriptExecutor, ScriptExecutor>();
|
||||
services.AddSingleton<IProviderFactory, ProviderFactory>();
|
||||
services.AddSingleton<IByondTopicSender>(new ByondTopicSender
|
||||
{
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstraction over a <see cref="System.Diagnostics.Process"/>
|
||||
/// </summary>
|
||||
interface IProcess : IProcessBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IProcess"/>' ID
|
||||
/// </summary>
|
||||
int Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Task"/> representing the time until the <see cref="IProcess"/> becomes "idle"
|
||||
/// </summary>
|
||||
Task Startup { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Get the stderr output of the <see cref="IProcess"/>
|
||||
/// </summary>
|
||||
/// <returns>The stderr output of the <see cref="IProcess"/></returns>
|
||||
string GetErrorOutput();
|
||||
|
||||
/// <summary>
|
||||
/// Get the stdout output of the <see cref="IProcess"/>
|
||||
/// </summary>
|
||||
/// <returns>The stdout output of the <see cref="IProcess"/></returns>
|
||||
string GetStandardOutput();
|
||||
|
||||
/// <summary>
|
||||
/// Get the stderr and stdout output of the <see cref="IProcess"/>
|
||||
/// </summary>
|
||||
/// <returns>The stderr and stdout output of the <see cref="IProcess"/></returns>
|
||||
string GetCombinedOutput();
|
||||
|
||||
/// <summary>
|
||||
/// Terminates the process
|
||||
/// </summary>
|
||||
void Terminate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents process lifetime
|
||||
/// </summary>
|
||||
interface IProcessBase : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Task{TResult}"/> resulting in the exit code of the process
|
||||
/// </summary>
|
||||
Task<int> Lifetime { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// For launching <see cref="IProcess"/>'
|
||||
/// </summary>
|
||||
interface IProcessExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// Launch a <see cref="IProcess"/>
|
||||
/// </summary>
|
||||
/// <param name="fileName">The full path to the executable file</param>
|
||||
/// <param name="arguments">The arguments for the <see cref="IProcess"/></param>
|
||||
/// <param name="workingDirectory">The working directory for the <see cref="IProcess"/></param>
|
||||
/// <param name="readOutput">If standard output should be read</param>
|
||||
/// <param name="readError">If standard error should be read</param>
|
||||
/// <returns>A new <see cref="IProcess"/></returns>
|
||||
IProcess LaunchProcess(string fileName, string workingDirectory, string arguments = null, bool readOutput = false, bool readError = false);
|
||||
|
||||
/// <summary>
|
||||
/// Get a <see cref="IProcess"/> by <paramref name="id"/>
|
||||
/// </summary>
|
||||
/// <param name="id">The <see cref="IProcess.Id"/></param>
|
||||
/// <returns>The <see cref="IProcess"/> represented by <paramref name="id"/></returns>
|
||||
IProcess GetProcess(int id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class Process : IProcess
|
||||
{
|
||||
|
||||
public int Id { get; }
|
||||
|
||||
public Task Startup { get; }
|
||||
|
||||
public Task<int> Lifetime { get; }
|
||||
|
||||
readonly System.Diagnostics.Process handle;
|
||||
|
||||
readonly StringBuilder outputStringBuilder;
|
||||
readonly StringBuilder errorStringBuilder;
|
||||
readonly StringBuilder combinedStringBuilder;
|
||||
|
||||
public Process(System.Diagnostics.Process handle, Task<int> lifetime, StringBuilder outputStringBuilder, StringBuilder errorStringBuilder, StringBuilder combinedStringBuilder)
|
||||
{
|
||||
this.handle = handle ?? throw new ArgumentNullException(nameof(handle));
|
||||
Lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime));
|
||||
|
||||
this.outputStringBuilder = outputStringBuilder;
|
||||
this.errorStringBuilder = errorStringBuilder;
|
||||
this.combinedStringBuilder = combinedStringBuilder;
|
||||
|
||||
Id = handle.Id;
|
||||
Startup = Task.Factory.StartNew(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
handle.WaitForInputIdle();
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
}, default, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
}
|
||||
|
||||
public void Dispose() => handle.Dispose();
|
||||
|
||||
public string GetCombinedOutput()
|
||||
{
|
||||
if (combinedStringBuilder == null)
|
||||
throw new InvalidOperationException("Output/Error reading was not enabled!");
|
||||
return combinedStringBuilder.ToString();
|
||||
}
|
||||
|
||||
public string GetErrorOutput()
|
||||
{
|
||||
if (errorStringBuilder == null)
|
||||
throw new InvalidOperationException("Error reading was not enabled!");
|
||||
return errorStringBuilder.ToString();
|
||||
}
|
||||
|
||||
public string GetStandardOutput()
|
||||
{
|
||||
if (outputStringBuilder == null)
|
||||
throw new InvalidOperationException("Output reading was not enabled!");
|
||||
return errorStringBuilder.ToString();
|
||||
}
|
||||
|
||||
public void Terminate()
|
||||
{
|
||||
try
|
||||
{
|
||||
handle.Kill();
|
||||
handle.WaitForExit();
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class ProcessExecutor : IProcessExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a <see cref="Task{TResult}"/> resulting in the exit code of a given <paramref name="handle"/>
|
||||
/// </summary>
|
||||
/// <param name="handle">The <see cref="System.Diagnostics.Process"/> to attach the <see cref="Task{TResult}"/> for</param>
|
||||
/// <returns>A new <see cref="Task{TResult}"/> resulting in the exit code of <paramref name="handle"/></returns>
|
||||
static Task<int> AttachExitHandler(System.Diagnostics.Process handle)
|
||||
{
|
||||
handle.EnableRaisingEvents = true;
|
||||
var tcs = new TaskCompletionSource<int>();
|
||||
handle.Exited += (a, b) => tcs.SetResult(handle.ExitCode);
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IProcess GetProcess(int id)
|
||||
{
|
||||
var handle = System.Diagnostics.Process.GetProcessById(id);
|
||||
try
|
||||
{
|
||||
return new Process(handle, AttachExitHandler(handle), null, null, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
handle.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IProcess LaunchProcess(string fileName, string workingDirectory, string arguments, bool readOutput, bool readError)
|
||||
{
|
||||
var handle = new System.Diagnostics.Process();
|
||||
try
|
||||
{
|
||||
handle.StartInfo.FileName = fileName;
|
||||
handle.StartInfo.Arguments = arguments;
|
||||
handle.StartInfo.WorkingDirectory = workingDirectory;
|
||||
|
||||
StringBuilder outputStringBuilder = null, errorStringBuilder = null, combinedStringBuilder = null;
|
||||
if (readOutput || readError)
|
||||
{
|
||||
handle.StartInfo.UseShellExecute = false;
|
||||
combinedStringBuilder = new StringBuilder();
|
||||
if (readOutput)
|
||||
{
|
||||
outputStringBuilder = new StringBuilder();
|
||||
handle.StartInfo.RedirectStandardOutput = true;
|
||||
var eventHandler = new DataReceivedEventHandler(
|
||||
delegate (object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
combinedStringBuilder.Append(Environment.NewLine);
|
||||
combinedStringBuilder.Append(e.Data);
|
||||
outputStringBuilder.Append(Environment.NewLine);
|
||||
outputStringBuilder.Append(e.Data);
|
||||
}
|
||||
);
|
||||
handle.OutputDataReceived += eventHandler;
|
||||
}
|
||||
if (readError)
|
||||
{
|
||||
errorStringBuilder = new StringBuilder();
|
||||
handle.StartInfo.RedirectStandardError = true;
|
||||
var eventHandler = new DataReceivedEventHandler(
|
||||
delegate (object sender, DataReceivedEventArgs e)
|
||||
{
|
||||
combinedStringBuilder.Append(Environment.NewLine);
|
||||
combinedStringBuilder.Append(e.Data);
|
||||
errorStringBuilder.Append(Environment.NewLine);
|
||||
errorStringBuilder.Append(e.Data);
|
||||
}
|
||||
);
|
||||
handle.ErrorDataReceived += eventHandler;
|
||||
}
|
||||
}
|
||||
|
||||
var lifetimeTask = AttachExitHandler(handle);
|
||||
|
||||
handle.Start();
|
||||
try
|
||||
{
|
||||
if (readOutput)
|
||||
handle.BeginOutputReadLine();
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
try
|
||||
{
|
||||
if (readError)
|
||||
handle.BeginErrorReadLine();
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
|
||||
return new Process(handle, lifetimeTask, outputStringBuilder, errorStringBuilder, combinedStringBuilder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
handle.Dispose();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user