mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 07:04:57 +01:00
WIP as usual
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user