Let's rebuild from the ground up.

Overhauls DreamDaemonExecutor and adds DreamDaemonSession
This commit is contained in:
Cyberboss
2018-06-28 15:32:33 -04:00
parent 19edebcbf9
commit 7ba92876ce
6 changed files with 122 additions and 98 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ namespace Tgstation.Server.Host.Service
UseShellExecute = true,
Verb = "runas",
Arguments = String.Join(" ", argList),
FileName = Assembly.GetExecutingAssembly().Location,
FileName = exe,
WorkingDirectory = Environment.CurrentDirectory,
};
using (Process.Start(startInfo))
@@ -1,14 +1,8 @@
using Newtonsoft.Json;
using System;
using System;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Models;
using Tgstation.Server.Host.Core;
namespace Tgstation.Server.Host.Components
{
@@ -35,34 +29,11 @@ namespace Tgstation.Server.Host.Components
}
}
/// <summary>
/// The <see cref="IInstanceShutdownHandler"/> for the <see cref="DreamDaemonExecutor"/>
/// </summary>
readonly IInstanceShutdownHandler instanceShutdownMethod;
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="DreamDaemonExecutor"/>
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="IApplication"/> for the <see cref="DreamDaemonExecutor"/>
/// </summary>
readonly IApplication application;
/// <summary>
/// Construct a <see cref="DreamDaemonExecutor"/>
/// </summary>
/// <param name="instanceShutdownMethod">The value of <see cref="instanceShutdownMethod"/></param>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
/// <param name="application">The value of <see cref="application"/></param>
public DreamDaemonExecutor(IInstanceShutdownHandler instanceShutdownMethod, IIOManager ioManager, IApplication application)
{
this.instanceShutdownMethod = instanceShutdownMethod ?? throw new ArgumentNullException(nameof(instanceShutdownMethod));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.application = application ?? throw new ArgumentNullException(nameof(application));
}
/// <inheritdoc />
public IDreamDaemonSession AttachToDreamDaemon(int processId) => new DreamDaemonSession(Process.GetProcessById(processId));
/// <inheritdoc />
public async Task<int> RunDreamDaemon(DreamDaemonLaunchParameters launchParameters, TaskCompletionSource<object> onSuccessfulStartup, string dreamDaemonPath, IDmbProvider dmbProvider, InteropInfo interopInfo, bool alwaysKill, bool asDefaultOtherServer, CancellationToken cancellationToken)
public IDreamDaemonSession RunDreamDaemon(DreamDaemonLaunchParameters launchParameters, string dreamDaemonPath, IDmbProvider dmbProvider, string parameters, bool useSecondaryPort, bool useSecondaryDirectory)
{
if (launchParameters == null)
throw new ArgumentNullException(nameof(launchParameters));
@@ -70,69 +41,32 @@ namespace Tgstation.Server.Host.Components
throw new ArgumentNullException(nameof(dreamDaemonPath));
if (dmbProvider == null)
throw new ArgumentNullException(nameof(dmbProvider));
if (interopInfo == null)
throw new ArgumentNullException(nameof(interopInfo));
if (parameters == null)
throw new ArgumentNullException(nameof(parameters));
if (interopInfo.NextPort != launchParameters.PrimaryPort && interopInfo.NextPort != launchParameters.SecondaryPort)
throw new ArgumentOutOfRangeException(nameof(interopInfo), interopInfo, "interopInfo.NextPort must match primary or secondary ports!");
var proc = new Process();
var isPrimary = interopInfo.NextPort == launchParameters.SecondaryPort;
//serialize the interop info to the json
var jsonPath = String.Concat(Guid.NewGuid(), ".tgs.json");
var json = JsonConvert.SerializeObject(interopInfo);
using (var proc = new Process())
try
{
proc.StartInfo.FileName = dreamDaemonPath;
proc.StartInfo.WorkingDirectory = isPrimary ? dmbProvider.PrimaryDirectory : dmbProvider.SecondaryDirectory;
await ioManager.WriteAllBytes(ioManager.ConcatPath(proc.StartInfo.WorkingDirectory, jsonPath), Encoding.UTF8.GetBytes(json), cancellationToken).ConfigureAwait(false);
proc.StartInfo.WorkingDirectory = useSecondaryDirectory ? dmbProvider.SecondaryDirectory : dmbProvider.PrimaryDirectory;
proc.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} {2}-close -{3} -verbose -public -params \"{4}={5}&{6}={7}\"",
proc.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} {2}-close -{3} -verbose -public -params \"{4}\"",
dmbProvider.DmbName,
isPrimary && !asDefaultOtherServer ? launchParameters.PrimaryPort : launchParameters.SecondaryPort,
useSecondaryPort ? launchParameters.SecondaryPort : launchParameters.PrimaryPort,
launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
SecurityWord(launchParameters.SecurityLevel.Value),
DreamDaemonParameters.HostVersion, Application.Version,
DreamDaemonParameters.InfoJsonPath, jsonPath);
parameters);
proc.EnableRaisingEvents = true;
var tcs = new TaskCompletionSource<object>();
proc.Exited += (a, b) => tcs.SetResult(null);
proc.Start();
try
{
proc.Start();
await Task.Factory.StartNew(() => proc.WaitForInputIdle(), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
onSuccessfulStartup?.SetResult(null);
try
{
using (cancellationToken.Register(() => tcs.SetCanceled()))
await tcs.Task.ConfigureAwait(false);
}
finally
{
if (!alwaysKill && !await instanceShutdownMethod.PreserveActiveExecutablesIfNecessary(launchParameters, interopInfo.AccessToken, proc.Id, isPrimary).ConfigureAwait(false))
{
proc.Kill();
proc.WaitForExit();
}
}
return proc.ExitCode;
}
catch (Exception e)
{
onSuccessfulStartup?.SetException(e);
throw;
}
return new DreamDaemonSession(proc);
}
catch
{
proc.Dispose();
throw;
}
}
}
}
@@ -0,0 +1,56 @@
using System;
using System.Diagnostics;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components
{
/// <inheritdoc />
sealed class DreamDaemonSession : IDreamDaemonSession
{
/// <inheritdoc />
public int ProcessId => process.Id;
/// <inheritdoc />
public Task SuccessfulStartup { get; }
/// <inheritdoc />
public Task<int> Lifetime => lifetimeTask.Task;
/// <summary>
/// The actual <see cref="Process"/>
/// </summary>
readonly Process process;
/// <summary>
/// The backing <see cref="TaskCompletionSource{TResult}"/> for <see cref="Lifetime"/>
/// </summary>
readonly TaskCompletionSource<int> lifetimeTask;
/// <summary>
/// Construct a <see cref="DreamDaemonSession"/>
/// </summary>
/// <param name="process">The value of <see cref="process"/></param>
public DreamDaemonSession(Process process)
{
this.process = process ?? throw new ArgumentNullException(nameof(process));
SuccessfulStartup = Task.Factory.StartNew(() => process.WaitForInputIdle(), default, TaskCreationOptions.LongRunning, TaskScheduler.Current);
lifetimeTask = new TaskCompletionSource<int>();
process.EnableRaisingEvents = true;
process.Exited += (a, b) => lifetimeTask.SetResult(process.ExitCode);
}
/// <inheritdoc />
public void Dispose() => process.Dispose();
/// <inheritdoc />
public void Terminate()
{
try
{
process.Kill();
process.WaitForExit();
}
catch (InvalidOperationException) { }
}
}
}
@@ -1,12 +1,9 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Models;
using Tgstation.Server.Api.Models.Internal;
namespace Tgstation.Server.Host.Components
{
/// <summary>
/// For running a DreamDaemon instance
/// For creating <see cref="IDreamDaemonSession"/>s
/// </summary>
interface IDreamDaemonExecutor
{
@@ -17,11 +14,17 @@ namespace Tgstation.Server.Host.Components
/// <param name="onSuccessfulStartup">The <see cref="TaskCompletionSource{TResult}"/> that is completed when dream daemon starts without crashing</param>
/// <param name="dreamDaemonPath">The path to the dreamdaemon executable</param>
/// <param name="dmbProvider">The <see cref="IDmbProvider"/> for the .dmb to run</param>
/// <param name="interopInfo">The <see cref="InteropInfo"/> for the run</param>
/// <param name="alwaysKill">If the resulting process should never be left alive</param>
/// <param name="asDefaultOtherServer">If <see langword="true"/> the ports will be swapped for the launch</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> representing the lifetime of the process and resulting in the exit code</returns>
Task<int> RunDreamDaemon(DreamDaemonLaunchParameters launchParameters, TaskCompletionSource<object> onSuccessfulStartup, string dreamDaemonPath, IDmbProvider dmbProvider, InteropInfo interopInfo, bool alwaysKill, bool asDefaultOtherServer, CancellationToken cancellationToken);
/// <param name="parameters">The value of the -params command line option</param>
/// <param name="useSecondaryPort">If the <see cref="DreamDaemonLaunchParameters.SecondaryPort"/> field of <paramref name="launchParameters"/> should be used</param>
/// <param name="useSecondaryDirectory">If the <see cref="IDmbProvider.SecondaryDirectory"/> field of <paramref name="dmbProvider"/> should be used</param>
/// <returns>A new <see cref="IDreamDaemonSession"/></returns>
IDreamDaemonSession RunDreamDaemon(DreamDaemonLaunchParameters launchParameters, string dreamDaemonPath, IDmbProvider dmbProvider, string parameters, bool useSecondaryPort, bool useSecondaryDirectory);
/// <summary>
/// Attach to a running instance of DreamDaemon
/// </summary>
/// <param name="processId">The <see cref="IDreamDaemonSession.ProcessId"/></param>
/// <returns>A new <see cref="IDreamDaemonSession"/></returns>
IDreamDaemonSession AttachToDreamDaemon(int processId);
}
}
@@ -0,0 +1,31 @@
using System;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components
{
/// <summary>
/// Represents a dream daemon process
/// </summary>
interface IDreamDaemonSession : IDisposable
{
/// <summary>
/// The <see cref="System.Diagnostics.Process.Id"/>
/// </summary>
int ProcessId { get; }
/// <summary>
/// A <see cref="Task"/> that completes when DreamDaemon starts pumping the windows message queue after loading a .dmb
/// </summary>
Task SuccessfulStartup { 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; }
/// <summary>
/// Terminates the running process
/// </summary>
void Terminate();
}
}
@@ -59,7 +59,7 @@ namespace Tgstation.Server.Host.Controllers
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="IActionResult"/> of the operation</returns>
[HttpPost]
public virtual Task<IActionResult> UpdateAsync([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
public virtual Task<IActionResult> Update([FromBody]TModel model, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound());
/// <summary>
/// Attempt to delete a <paramref name="model"/>