Adds IProcess

This commit is contained in:
Cyberboss
2018-08-10 10:40:06 -04:00
parent b055f32e07
commit fb8aa23960
17 changed files with 384 additions and 273 deletions
@@ -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);
}
}
+77
View File
@@ -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;
}
}
}
}