WIP as usual

This commit is contained in:
Cyberboss
2018-04-23 15:04:13 -04:00
parent 36c1ac6508
commit fff7d1b10a
12 changed files with 336 additions and 12 deletions
@@ -18,7 +18,7 @@ namespace Tgstation.Server.Api.Models
/// The current status of <see cref="DreamDaemon"/>
/// </summary>
[Permissions(DenyWrite = true, ReadRight = DreamDaemonRights.ReadMetadata)]
public DreamDaemonStatus? Status { get; set; }
public bool? Running { get; set; }
/// <summary>
/// The current <see cref="DreamDaemonSecurity"/> of <see cref="DreamDaemon"/>
@@ -0,0 +1,276 @@
using System;
using System.Diagnostics;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Components
{
/// <inheritdoc />
sealed class DreamDaemon : IDreamDaemon, IDisposable
{
/// <inheritdoc />
public bool Running { get; private set; }
/// <inheritdoc />
public ushort? CurrentPort { get; private set; }
/// <inheritdoc />
public string AccessToken { get; private set; }
/// <inheritdoc />
public DreamDaemonSecurity? CurrentSecurity { get; private set; }
/// <inheritdoc />
public bool SoftRebooting { get; private set; }
/// <inheritdoc />
public bool SoftStopping { get; private set; }
/// <summary>
/// The <see cref="IEventConsumer"/> for <see cref="DreamDaemon"/>
/// </summary>
readonly IEventConsumer eventConsumer;
/// <summary>
/// The <see cref="IByond"/> for <see cref="DreamDaemon"/>
/// </summary>
readonly IByond byond;
/// <summary>
/// The <see cref="ICryptographySuite"/> for <see cref="DreamDaemon"/>
/// </summary>
readonly ICryptographySuite cryptographySuite;
/// <summary>
/// The <see cref="IInterop"/> for <see cref="DreamDaemon"/>
/// </summary>
readonly IInterop interop;
/// <summary>
/// The <see cref="IInstanceShutdownMethod"/> for <see cref="DreamDaemon"/>
/// </summary>
readonly IInstanceShutdownMethod instanceShutdownMethod;
readonly IIOManager ioManager;
readonly bool autoStart;
readonly SemaphoreSlim semaphore;
DreamDaemonLaunchParameters currentLaunchParameters;
CancellationTokenSource watchdogCancellationTokenSource;
Task watchdogTask;
public DreamDaemon(IEventConsumer eventConsumer, IByond byond, ICryptographySuite cryptographySuite, IInterop interop, IInstanceShutdownMethod instanceShutdownMethod, DreamDaemonSettings initialSettings)
{
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
this.interop = interop ?? throw new ArgumentNullException(nameof(interop));
this.instanceShutdownMethod = instanceShutdownMethod ?? throw new ArgumentNullException(nameof(instanceShutdownMethod));
interop.SetServerControlHandler(OnServerControl);
currentLaunchParameters = initialSettings ?? throw new ArgumentNullException(nameof(initialSettings));
autoStart = initialSettings.AutoStart;
semaphore = new SemaphoreSlim(1);
}
public void Dispose()
{
if (watchdogCancellationTokenSource != null)
watchdogCancellationTokenSource.Dispose();
semaphore.Dispose();
}
Task OnServerControl(ServerControlEventArgs serverControlEventArgs, CancellationToken cancellationToken)
{
lock (this)
{
CancelGracefulActions();
if (serverControlEventArgs.ProcessRestart)
return Restart(serverControlEventArgs.Graceful, cancellationToken);
if(!serverControlEventArgs.ServerReboot)
return Terminate(serverControlEventArgs.Graceful, cancellationToken);
return Task.CompletedTask;
}
}
static string SecurityWord(DreamDaemonSecurity securityLevel)
{
switch (securityLevel)
{
case DreamDaemonSecurity.Safe:
return "safe";
case DreamDaemonSecurity.Trusted:
return "trusted";
case DreamDaemonSecurity.Ultrasafe:
return "ultrasafe";
default:
throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel));
}
}
async Task Watchdog(DreamDaemonLaunchParameters launchParameters, TaskCompletionSource<object> onSuccessfulStartup, CancellationToken cancellationToken)
{
async Task RunOnce(string executablePath)
{
if (await byond.GetVersion(cancellationToken).ConfigureAwait(false) == null)
throw new InvalidOperationException("No byond version installed!");
await byond.ClearCache(cancellationToken).ConfigureAwait(false);
using (var proc = new Process())
{
var accessToken = cryptographySuite.GetSecureString();
proc.StartInfo.FileName = executablePath;
proc.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} {5}-close -verbose -params \"server_service={3}&server_service_version={4}&{6}={7}\" -{2} -public", DMB, launchParameters.PrimaryPort, SecurityWord(launchParameters.SecurityLevel), accessToken, Application.Version, launchParameters.AllowWebClient ? "-webclient " : String.Empty);
proc.EnableRaisingEvents = true;
var tcs = new TaskCompletionSource<object>();
proc.Exited += (a, b) => tcs.SetResult(null);
try
{
proc.Start();
await Task.Factory.StartNew(() => proc.WaitForInputIdle(), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
if (onSuccessfulStartup != null)
{
onSuccessfulStartup.SetResult(null);
onSuccessfulStartup = null;
}
try
{
using (cancellationToken.Register(() => tcs.SetCanceled()))
await tcs.Task.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return;
}
finally
{
if (!instanceShutdownMethod.GracefulShutdown)
{
proc.Kill();
proc.WaitForExit();
}
}
}
catch (Exception e)
{
onSuccessfulStartup?.SetException(e);
throw;
}
await eventConsumer.HandleEvent(proc.ExitCode != 0 ? EventType.DDCrash : EventType.DDExit, null, cancellationToken).ConfigureAwait(false);
}
};
do
{
await byond.UseExecutable(RunOnce, false, true).ConfigureAwait(false);
} while (!cancellationToken.IsCancellationRequested);
}
public void CancelGracefulActions()
{
lock (this)
{
SoftRebooting = false;
SoftStopping = false;
}
}
public async Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
{
Task launchTask;
lock (this)
{
currentLaunchParameters = launchParameters;
if (!Running)
launchTask = Launch(launchParameters, cancellationToken);
else
launchTask = Restart(true, cancellationToken);
}
await launchTask.ConfigureAwait(false);
}
public async Task Launch(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken)
{
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (Running)
throw new InvalidOperationException("DreamDaemon already running!");
Running = true;
await eventConsumer.HandleEvent(EventType.DDLaunched, null, cancellationToken).ConfigureAwait(false);
watchdogCancellationTokenSource?.Dispose();
watchdogCancellationTokenSource = new CancellationTokenSource();
var startupTcs = new TaskCompletionSource<object>();
watchdogTask = Watchdog(currentLaunchParameters, startupTcs, watchdogCancellationTokenSource.Token);
await startupTcs.Task.ConfigureAwait(false);
}
finally
{
semaphore.Release();
}
}
public async Task Restart(bool graceful, CancellationToken cancellationToken)
{
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (graceful)
{
if (!Running)
throw new InvalidOperationException("DreamDaemon not running!");
if (SoftStopping)
throw new InvalidOperationException("DreamDaemon has a graceful stop queued!");
SoftRebooting = true;
return;
}
await eventConsumer.HandleEvent(EventType.DDRestart, null, cancellationToken).ConfigureAwait(false);
if (Running)
watchdogCancellationTokenSource.Cancel();
await Launch(currentLaunchParameters, cancellationToken).ConfigureAwait(false);
}
finally
{
semaphore.Release();
}
}
public Task StartAsync(CancellationToken cancellationToken) => autoStart ? Launch(currentLaunchParameters, cancellationToken) : Task.CompletedTask;
public Task StopAsync(CancellationToken cancellationToken) => Terminate(false, cancellationToken);
public async Task Terminate(bool graceful, CancellationToken cancellationToken)
{
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (graceful)
{
if (!Running)
throw new InvalidOperationException("DreamDaemon not running!");
SoftRebooting = false;
SoftStopping = true;
return;
}
await eventConsumer.HandleEvent(EventType.DDTerminated, null, cancellationToken).ConfigureAwait(false);
watchdogCancellationTokenSource.Cancel();
await watchdogTask.ConfigureAwait(false);
}
finally
{
semaphore.Release();
}
}
}
}
@@ -31,5 +31,12 @@ namespace Tgstation.Server.Host.Components
/// <param name="dreamDaemon">Pass the path of dreamdaemon.exe to <paramref name="operation"/> if <see langword="true"/> dm.exe otherwise</param>
/// <returns>A <see cref="Task"/> representing the running <paramref name="operation"/></returns>
Task UseExecutable(Func<string, Task> operation, bool stagedIfExists, bool dreamDaemon);
/// <summary>
/// Clears the cache folder
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task ClearCache(CancellationToken cancellationToken);
}
}
@@ -1,6 +1,7 @@
using Microsoft.Extensions.Hosting;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Api.Models.Internal;
namespace Tgstation.Server.Host.Components
@@ -15,11 +16,26 @@ namespace Tgstation.Server.Host.Components
/// </summary>
bool Running { get; }
/// <summary>
/// If DreamDaemon is going to soft reboot
/// </summary>
bool SoftRebooting { get; }
/// <summary>
/// If DreamDaemon is going to soft stop
/// </summary>
bool SoftStopping { get; }
/// <summary>
/// The port DreamDaemon is currently running on
/// </summary>
ushort? CurrentPort { get; }
/// <summary>
/// The current <see cref="DreamDaemonSecurity"/> of <see cref="DreamDaemon"/>
/// </summary>
DreamDaemonSecurity? CurrentSecurity { get; }
/// <summary>
/// The access token used for communication with the DMAPI
/// </summary>
@@ -37,7 +53,9 @@ namespace Tgstation.Server.Host.Components
/// Changes the <see cref="DreamDaemonLaunchParameters"/> if currently <see cref="Running"/>. Triggers a graceful restart
/// </summary>
/// <param name="launchParameters">The new <see cref="DreamDaemonLaunchParameters"/></param>
void ChangeSettings(DreamDaemonLaunchParameters launchParameters);
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken);
/// <summary>
/// Restarts DreamDaemon
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// For managing <see cref="IInstance"/>s
/// </summary>
interface IInstanceManager
interface IInstanceManager : IInstanceShutdownMethod
{
/// <summary>
/// Get the <see cref="IInstance"/> associated with given <paramref name="metadata"/>
@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Tgstation.Server.Host.Components
{
interface IInstanceShutdownMethod
{
bool GracefulShutdown { get; set; }
}
}
@@ -9,15 +9,15 @@ namespace Tgstation.Server.Host.Components
/// </summary>
interface IInterop
{
event EventHandler<ServerControlEventArgs> OnServerControl;
event EventHandler<ChatMessageEventArgs> OnChatMessage;
void SetServerControlHandler(Func<ServerControlEventArgs, CancellationToken, Task> serverControlHandler);
void SetChatMessageHandler(Func<ChatMessageEventArgs, CancellationToken, Task> chatMessageHandler);
Version MinimumApiVersion { get; }
Version MaximumApiVersion { get; }
Task<Version> GetApiVersion(CancellationToken cancellationToken);
void SetActivePort(ushort? port);
void SetRun(ushort? port, string accessToken);
Task<string> ChatCommand(string command, string arguments, CancellationToken cancellationToken);
@@ -14,6 +14,9 @@ namespace Tgstation.Server.Host.Components
/// <inheritdoc />
sealed class InstanceManager : IInstanceManager, IHostedService
{
/// <inheritdoc />
public bool GracefulShutdown { get; set; }
/// <summary>
/// The <see cref="IInstanceFactory"/> for the <see cref="IInstanceManager"/>
/// </summary>
@@ -10,16 +10,16 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// If <see langword="true"/> the event will result in a new run of the world
/// </summary>
bool ServerReboot { get; set; }
public bool ServerReboot { get; set; }
/// <summary>
/// If <see langword="true"/> the event will result in a restart of the process. If both this and <see cref="ServerReboot"/> are <see langword="false"/>, the server is being terminated
/// </summary>
bool ProcessRestart { get; set; }
public bool ProcessRestart { get; set; }
/// <summary>
/// If the action to be taken is graceful
/// </summary>
bool Graceful { get; set; }
public bool Graceful { get; set; }
}
}
@@ -56,5 +56,8 @@ namespace Tgstation.Server.Host.Security
}
return true;
}
/// <inheritdoc />
public string GetSecureString() => Convert.ToBase64String(GetSecureBytes(30));
}
}
@@ -21,5 +21,11 @@ namespace Tgstation.Server.Host.Security
/// <param name="password">The password to check</param>
/// <returns><see langword="true"/> if <paramref name="password"/> matches the hash, <see langword="false"/> otherwise</returns>
bool CheckUserPassword(User user, string password);
/// <summary>
/// Generates a 40-length secure ascii <see cref="string"/>
/// </summary>
/// <returns>A 40-length secure ascii <see cref="string"/></returns>
string GetSecureString();
}
}
+2 -2
View File
@@ -10,7 +10,7 @@ using Tgstation.Server.Host.Startup;
namespace Tgstation.Server.Host
{
/// <inheritdoc />
sealed class Server : IServer, IServerUpdateConsumer
sealed class Server : IServer, IServerControl
{
/// <inheritdoc />
public string UpdatePath { get; private set; }
@@ -39,7 +39,7 @@ namespace Tgstation.Server.Host
using (cancellationTokenSource)
using (var webHost = webHostBuilder
.UseStartup<Application>()
.ConfigureServices((serviceCollection) => serviceCollection.AddSingleton<IServerUpdateConsumer>(this))
.ConfigureServices((serviceCollection) => serviceCollection.AddSingleton<IServerControl>(this))
.Build()
)
await webHost.RunAsync(cancellationToken).ConfigureAwait(false);