From fb8aa23960a5183ddcce963f848175757921c2d3 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 10 Aug 2018 10:40:06 -0400 Subject: [PATCH] Adds IProcess --- .../Components/Byond/WindowsByondInstaller.cs | 51 +++----- .../Components/Compiler/DreamMaker.cs | 55 ++------- .../Components/InstanceFactory.cs | 14 +-- .../Components/StaticFiles/Configuration.cs | 20 ++-- .../Components/StaticFiles/IScriptExecutor.cs | 21 ---- .../Components/StaticFiles/ScriptExecutor.cs | 77 ------------ .../Components/Watchdog/Executor.cs | 46 ++++---- .../Components/Watchdog/ISession.cs | 15 +-- .../Components/Watchdog/ISessionBase.cs | 11 +- .../Components/Watchdog/Session.cs | 69 +++++------ .../Watchdog/SessionControllerFactory.cs | 2 +- src/Tgstation.Server.Host/Core/Application.cs | 2 +- src/Tgstation.Server.Host/Core/IProcess.cs | 44 +++++++ .../Core/IProcessBase.cs | 16 +++ .../Core/IProcessExecutor.cs | 26 ++++ src/Tgstation.Server.Host/Core/Process.cs | 77 ++++++++++++ .../Core/ProcessExecutor.cs | 111 ++++++++++++++++++ 17 files changed, 384 insertions(+), 273 deletions(-) delete mode 100644 src/Tgstation.Server.Host/Components/StaticFiles/IScriptExecutor.cs delete mode 100644 src/Tgstation.Server.Host/Components/StaticFiles/ScriptExecutor.cs create mode 100644 src/Tgstation.Server.Host/Core/IProcess.cs create mode 100644 src/Tgstation.Server.Host/Core/IProcessBase.cs create mode 100644 src/Tgstation.Server.Host/Core/IProcessExecutor.cs create mode 100644 src/Tgstation.Server.Host/Core/Process.cs create mode 100644 src/Tgstation.Server.Host/Core/ProcessExecutor.cs diff --git a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs index d2a7329345..588f2e9fbe 100644 --- a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs @@ -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 /// readonly IIOManager ioManager; + /// + /// The for the + /// + readonly IProcessExecutor processExecutor; + /// /// The for the /// @@ -66,10 +70,12 @@ namespace Tgstation.Server.Host.Components.Byond /// Construct a /// /// The value of + /// The value of /// The value of - public WindowsByondInstaller(IIOManager ioManager, ILogger logger) + public WindowsByondInstaller(IIOManager ioManager, IProcessExecutor processExecutor, ILogger 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(); - 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); } diff --git a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs index 622a3e9780..0d10d11185 100644 --- a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs @@ -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 /// readonly IChat chat; /// + /// The for + /// + readonly IProcessExecutor processExecutor; + /// /// The for /// readonly ILogger logger; @@ -89,8 +92,9 @@ namespace Tgstation.Server.Host.Components.Compiler /// The value of /// The value of /// The value of + /// The value of /// The value of - public DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, IChat chat, ILogger logger) + public DreamMaker(IByondManager byond, IIOManager ioManager, StaticFiles.IConfiguration configuration, ISessionControllerFactory sessionControllerFactory, ICompileJobConsumer compileJobConsumer, IApplication application, IEventConsumer eventConsumer, IChat chat, IProcessExecutor processExecutor, ILogger 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 /// A representing the running operation 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(); - 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); } } diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 7befe71b2c..89125b337b 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -78,9 +78,9 @@ namespace Tgstation.Server.Host.Components readonly IProviderFactory providerFactory; /// - /// The for the + /// The for the /// - readonly IScriptExecutor scriptExecutor; + readonly IProcessExecutor processExecutor; /// /// Construct an @@ -97,8 +97,8 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The value of - 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) + /// The value of + 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)); } /// @@ -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()); + var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, processExecutor, loggerFactory.CreateLogger()); var eventConsumer = new EventConsumer(configuration); var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger(), 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()); + var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, chat, processExecutor, loggerFactory.CreateLogger()); return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, loggerFactory.CreateLogger()); } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index 26076d6d09..fce5cf4a23 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -50,9 +50,9 @@ namespace Tgstation.Server.Host.Components.StaticFiles readonly ISymlinkFactory symlinkFactory; /// - /// The for + /// The for /// - readonly IScriptExecutor scriptExecutor; + readonly IProcessExecutor processExecutor; /// /// The for @@ -70,14 +70,14 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// The value of /// The value of /// The value of - /// The value of + /// The value of /// The value of - public Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IScriptExecutor scriptExecutor, ILogger logger) + public Configuration(IIOManager ioManager, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IProcessExecutor processExecutor, ILogger 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; } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/IScriptExecutor.cs b/src/Tgstation.Server.Host/Components/StaticFiles/IScriptExecutor.cs deleted file mode 100644 index a9e26ff72f..0000000000 --- a/src/Tgstation.Server.Host/Components/StaticFiles/IScriptExecutor.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; - -namespace Tgstation.Server.Host.Components.StaticFiles -{ - /// - /// For executing system shell scripts - /// - interface IScriptExecutor - { - /// - /// Execute a shell script and get the result - /// - /// The absolute path to the script - /// Command line parameters for the script - /// The for the operation - /// A resulting in the or if an error occurred - Task ExecuteScript(string scriptPath, IEnumerable parameters, CancellationToken cancellationToken); - } -} diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/ScriptExecutor.cs b/src/Tgstation.Server.Host/Components/StaticFiles/ScriptExecutor.cs deleted file mode 100644 index ff2a90c625..0000000000 --- a/src/Tgstation.Server.Host/Components/StaticFiles/ScriptExecutor.cs +++ /dev/null @@ -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 -{ - /// - sealed class ScriptExecutor : IScriptExecutor - { - /// - /// The for the - /// - readonly IIOManager ioManager; - - /// - /// The for the - /// - readonly ILogger logger; - - /// - /// Construct a - /// - /// The value of - /// The value of - public ScriptExecutor(IIOManager ioManager, ILogger logger) - { - this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public async Task ExecuteScript(string scriptPath, IEnumerable 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(); - 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; - } - } - } -} diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Executor.cs b/src/Tgstation.Server.Host/Components/Watchdog/Executor.cs index 85ec573591..130e1d0b96 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Executor.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Executor.cs @@ -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 { /// sealed class Executor : IExecutor { + /// + /// The for the + /// + readonly IProcessExecutor processExecutor; + /// /// The for the /// @@ -39,14 +44,16 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Construct an /// + /// The value of /// The value of - public Executor(ILogger logger) + public Executor(IProcessExecutor processExecutor, ILogger logger) { + this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// - 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); /// 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); } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ISession.cs b/src/Tgstation.Server.Host/Components/Watchdog/ISession.cs index 5fb7fb35dd..1728cc2ad3 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ISession.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ISession.cs @@ -1,18 +1,11 @@ -namespace Tgstation.Server.Host.Components.Watchdog +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Components.Watchdog { /// /// Represents a dream daemon process /// - interface ISession : ISessionBase + interface ISession : ISessionBase, IProcess { - /// - /// The - /// - int ProcessId { get; } - - /// - /// Terminates the running process - /// - void Terminate(); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ISessionBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/ISessionBase.cs index 672fbee8cf..bd1d9ee0dc 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ISessionBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ISessionBase.cs @@ -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 { /// /// A that completes when DreamDaemon starts pumping the windows message queue after loading a .dmb or when it crashes /// Task LaunchResult { get; } - - /// - /// A representing the lifetime of the and resulting in the - /// - Task Lifetime { get; } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Session.cs b/src/Tgstation.Server.Host/Components/Watchdog/Session.cs index a26f14e130..01a1a7bdd9 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Session.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Session.cs @@ -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 { /// - public int ProcessId => process.Id; + public int Id => process.Id; + + /// + public Task Startup => process.Startup; /// public Task LaunchResult { get; } /// - public Task Lifetime => lifetimeTask.Task; + public Task Lifetime => process.Lifetime; /// - /// The actual + /// The actual /// - readonly Process process; + readonly IProcess process; /// /// The for the /// readonly IByondExecutableLock byondLock; - /// - /// The backing for - /// - readonly TaskCompletionSource lifetimeTask; - /// /// Construct a /// /// The value of /// The value of - 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 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(); - try - { - process.EnableRaisingEvents = true; - process.Exited += (a, b) => lifetimeTask.TrySetResult(process.ExitCode); - } - catch (InvalidOperationException) - { - //dead proccess - lifetimeTask.TrySetResult(process.ExitCode); - } + }; + LaunchResult = GetLaunchResult(); } /// - public void Dispose() => process.Dispose(); - - /// - public void Terminate() + public void Dispose() { - try - { - process.Kill(); - process.WaitForExit(); - } - catch (InvalidOperationException) { } + process.Dispose(); + byondLock.Dispose(); } + + /// + public void Terminate() => process.Terminate(); + + /// + public string GetErrorOutput() => process.GetErrorOutput(); + + /// + public string GetStandardOutput() => process.GetStandardOutput(); + + /// + public string GetCombinedOutput() => process.GetCombinedOutput(); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs index 57a3d47a4e..ea25ecc487 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs @@ -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, diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index a3cd319530..03eeab5463 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -198,8 +198,8 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); } + services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); services.AddSingleton(new ByondTopicSender { diff --git a/src/Tgstation.Server.Host/Core/IProcess.cs b/src/Tgstation.Server.Host/Core/IProcess.cs new file mode 100644 index 0000000000..740b09951f --- /dev/null +++ b/src/Tgstation.Server.Host/Core/IProcess.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Abstraction over a + /// + interface IProcess : IProcessBase + { + /// + /// The ' ID + /// + int Id { get; } + + /// + /// The representing the time until the becomes "idle" + /// + Task Startup { get; } + + /// + /// Get the stderr output of the + /// + /// The stderr output of the + string GetErrorOutput(); + + /// + /// Get the stdout output of the + /// + /// The stdout output of the + string GetStandardOutput(); + + /// + /// Get the stderr and stdout output of the + /// + /// The stderr and stdout output of the + string GetCombinedOutput(); + + /// + /// Terminates the process + /// + void Terminate(); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Core/IProcessBase.cs b/src/Tgstation.Server.Host/Core/IProcessBase.cs new file mode 100644 index 0000000000..b96b93b2ae --- /dev/null +++ b/src/Tgstation.Server.Host/Core/IProcessBase.cs @@ -0,0 +1,16 @@ +using System; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Represents process lifetime + /// + interface IProcessBase : IDisposable + { + /// + /// The resulting in the exit code of the process + /// + Task Lifetime { get; } + } +} diff --git a/src/Tgstation.Server.Host/Core/IProcessExecutor.cs b/src/Tgstation.Server.Host/Core/IProcessExecutor.cs new file mode 100644 index 0000000000..20c0325a45 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/IProcessExecutor.cs @@ -0,0 +1,26 @@ +namespace Tgstation.Server.Host.Core +{ + /// + /// For launching ' + /// + interface IProcessExecutor + { + /// + /// Launch a + /// + /// The full path to the executable file + /// The arguments for the + /// The working directory for the + /// If standard output should be read + /// If standard error should be read + /// A new + IProcess LaunchProcess(string fileName, string workingDirectory, string arguments = null, bool readOutput = false, bool readError = false); + + /// + /// Get a by + /// + /// The + /// The represented by + IProcess GetProcess(int id); + } +} diff --git a/src/Tgstation.Server.Host/Core/Process.cs b/src/Tgstation.Server.Host/Core/Process.cs new file mode 100644 index 0000000000..edca9f0516 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/Process.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Core +{ + /// + sealed class Process : IProcess + { + + public int Id { get; } + + public Task Startup { get; } + + public Task Lifetime { get; } + + readonly System.Diagnostics.Process handle; + + readonly StringBuilder outputStringBuilder; + readonly StringBuilder errorStringBuilder; + readonly StringBuilder combinedStringBuilder; + + public Process(System.Diagnostics.Process handle, Task 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) { } + } + } +} diff --git a/src/Tgstation.Server.Host/Core/ProcessExecutor.cs b/src/Tgstation.Server.Host/Core/ProcessExecutor.cs new file mode 100644 index 0000000000..d42474b5c4 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/ProcessExecutor.cs @@ -0,0 +1,111 @@ +using System; +using System.Diagnostics; +using System.Text; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Core +{ + /// + sealed class ProcessExecutor : IProcessExecutor + { + /// + /// Create a resulting in the exit code of a given + /// + /// The to attach the for + /// A new resulting in the exit code of + static Task AttachExitHandler(System.Diagnostics.Process handle) + { + handle.EnableRaisingEvents = true; + var tcs = new TaskCompletionSource(); + handle.Exited += (a, b) => tcs.SetResult(handle.ExitCode); + return tcs.Task; + } + + /// + 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; + } + } + + /// + 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; + } + } + } +} \ No newline at end of file