DreamMaker is done

This commit is contained in:
Cyberboss
2018-05-04 11:16:39 -04:00
parent 39d084473b
commit dff5f975e6
16 changed files with 528 additions and 164 deletions
+28 -8
View File
@@ -2,11 +2,11 @@
#define TGS4_TOPIC_COMMAND "tgs_com"
#define TGS4_TOPIC_TOKEN "tgs_tok"
#define TGS4_TOPIC_SUCCESS "tgs_success"
#define TGS4_TOPIC_SWAP "tgs_swap"
#define TGS4_TOPIC_SWAP_DELAYED "tgs_swap_delayed"
#define TGS4_TOPIC_CHAT_COMMAND "tgs_chat_comm"
#define TGS4_TOPIC_EVENT "tgs_event"
#define TGS4_TOPIC_IDENTIFY "tgs_ident"
#define TGS4_COMM_VALIDATE "tgs_vali"
#define TGS4_COMM_SERVER_PRIMED "tgs_prime"
@@ -23,6 +23,8 @@
var/port_2
var/cached_json
var/json_path
var/list/intercepted_message_queue
var/list/custom_commands
@@ -95,8 +97,14 @@
return json_encode(list("error" = "Error running chat command!"))
return result
if(TGS4_TOPIC_EVENT)
intercepted_message_queue = list()
event_handler.HandleEvent(text2num(params[TGS4_TOPIC_EVENT]))
return TGS4_TOPIC_SUCCESS
. = json_encode(intercepted_message_queue)
intercepted_message_queue = null
return
if(TGS4_TOPIC_IDENTIFY)
//they want to know our initial port
return "[port_1]"
return "Unknown command: [command]"
@@ -104,13 +112,16 @@
set waitfor = FALSE
var/new_port = world.port == port_1 ? port_2 : port_1
event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port)
world.OpenPort("none") //close the port
if(delayed)
world.OpenPort("none") //close the port
sleep(50) //wait for other server to close port
world.OpenPort(new_port)
//do NOT give up, if we remain unresponsive we will be killed
while(!world.OpenPort(new_port))
sleep(10)
/datum/tgs_api/v4/proc/Export(command)
return world.Export("[host_path]/Interop/[instance_id]?command=[url_encode(command)]&access_token=[access_token]")
return file2text(world.Export("[host_path]/Interop/[instance_id]?command=[url_encode(command)]&access_token=[access_token]")["CONTENT"])
/datum/tgs_api/v4/OnReboot()
var/json = Export(TGS4_COMM_SERVER_REBOOT)
@@ -163,15 +174,24 @@
var/datum/tgs_chat_channel/channel = I
ids += channel.id
message = list("message" = message, "channels" = ids)
Export("[TGS4_COMM_CHAT] [json_encode(message)]")
if(interepted_message_queue)
interepted_message_queue += list(message)
else
Export("[TGS4_COMM_CHAT] [json_encode(message)]")
/datum/tgs_api/v4/ChatTargetedBroadcast(message, admin_only)
message = list("message" = message, "channels" = admin_only ? "admin" : "game")
Export("[TGS4_COMM_CHAT] [json_encode(message)]")
if(interepted_message_queue)
interepted_message_queue += list(message)
else
Export("[TGS4_COMM_CHAT] [json_encode(message)]")
/datum/tgs_api/v4/ChatPrivateMessage(message, datum/tgs_chat_user/user)
message = list("message" = message, "user" = list("id" = user.id, "channel" = user.channel.id))
Export("[TGS4_COMM_CHAT] [json_encode(message)]")
if(interepted_message_queue)
interepted_message_queue += list(message)
else
Export("[TGS4_COMM_CHAT] [json_encode(message)]")
/datum/tgs_api/v4/ChatChannelInfo()
. = list()
@@ -26,14 +26,14 @@ namespace Tgstation.Server.Api.Models.Internal
public DateTimeOffset FinishedAt { get; set; }
/// <summary>
/// The detected DMAPI version
/// If the DMAPI version detected is compatible
/// </summary>
public Version DMApiVersion { get; set; }
public bool DMApiValidated { get; set; }
/// <summary>
/// The .dme file used for compilation
/// </summary>
public string DmePath { get; set; }
public string DmeName { get; set; }
/// <summary>
/// Textual output of DM
@@ -43,7 +43,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The Game folder the results were compiled into
/// </summary>
public Guid? OutputGuid { get; set; }
public Guid? DirectoryName { get; set; }
/// <summary>
/// Exit code of DM. If <see langword="null"/>
@@ -68,9 +68,7 @@ namespace Tgstation.Server.Host.Components
this.interop = interop ?? throw new ArgumentNullException(nameof(interop));
this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
currentLaunchParameters = initialSettings ?? throw new ArgumentNullException(nameof(initialSettings));
interop.SetServerControlHandler(OnServerControl);
autoStart = initialSettings.AutoStart;
semaphore = new SemaphoreSlim(1);
@@ -79,21 +77,6 @@ namespace Tgstation.Server.Host.Components
/// <inheritdoc />
public void Dispose() => semaphore.Dispose();
/// <summary>
/// Handler for server control events
/// </summary>
/// <param name="serverControlEventArgs">The <see cref="ServerControlEvent"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
static async Task OnServerControl(ServerControlEvent serverControlEventArgs, CancellationToken cancellationToken)
{
if (serverControlEventArgs == null)
throw new ArgumentNullException(nameof(serverControlEventArgs));
await Task.Yield();
throw new NotImplementedException();
}
/// <inheritdoc />
public async Task CancelGracefulActions(CancellationToken cancellationToken)
{
@@ -0,0 +1,276 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
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;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Components
{
/// <inheritdoc />
sealed class DreamMaker : IDreamMaker
{
/// <summary>
/// Name of the primary directory used for compilation
/// </summary>
const string ADirectoryName = "A";
/// <summary>
/// The <see cref="IRepositoryManager"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IRepositoryManager repositoryManager;
/// <summary>
/// The <see cref="IIOManager"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="IConfiguration"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IConfiguration configuration;
/// <summary>
/// The <see cref="IDreamDaemonExecutor"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IDreamDaemonExecutor dreamDaemonExecutor;
/// <summary>
/// The <see cref="IByond"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IByond byond;
/// <summary>
/// The <see cref="IInterop"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IInterop interop;
/// <summary>
/// The <see cref="ICryptographySuite"/> for <see cref="DreamMaker"/>
/// </summary>
readonly ICryptographySuite cryptographySuite;
/// <summary>
/// Construct <see cref="DreamMaker"/>
/// </summary>
/// <param name="repositoryManager">The value of <see cref="repositoryManager"/></param>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
/// <param name="configuration">The value of <see cref="configuration"/></param>
/// <param name="dreamDaemonExecutor">The value of <see cref="dreamDaemonExecutor"/></param>
/// <param name="byond">The value of <see cref="byond"/></param>
/// <param name="interop">The value of <see cref="interop"/></param>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
public DreamMaker(IRepositoryManager repositoryManager, IIOManager ioManager, IConfiguration configuration, IDreamDaemonExecutor dreamDaemonExecutor, IByond byond, IInterop interop, ICryptographySuite cryptographySuite)
{
this.repositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
this.dreamDaemonExecutor = dreamDaemonExecutor ?? throw new ArgumentNullException(nameof(dreamDaemonExecutor));
this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
this.interop = interop ?? throw new ArgumentNullException(nameof(interop));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
}
/// <summary>
/// Run a quick DD instance to test the DMAPI is installed on the target code
/// </summary>
/// <param name="dreamDaemonPath">The path to the DreamDaemon executable</param>
/// <param name="job">The <see cref="Host.Models.CompileJob"/> for the operation</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the DMAPI was successfully validated, <see langword="false"/> otherwise</returns>
async Task<bool> VerifyApi(string dreamDaemonPath, Host.Models.CompileJob job, CancellationToken cancellationToken)
{
var launchParameters = new DreamDaemonLaunchParameters
{
AllowWebClient = false,
PrimaryPort = 0, //pick any port
SecurityLevel = DreamDaemonSecurity.Safe //all it needs to read the file and exit
};
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
{
var interopInfo = new InteropInfo
{
ApiValidateOnly = true,
HostPath = Application.HostingPath,
AccessToken = cryptographySuite.GetSecureString()
};
using (var control = interop.CreateRun(launchParameters.PrimaryPort, null, null))
{
var ddTcs = new TaskCompletionSource<object>();
control.OnServerControl += (sender, e) => {
if (e.EventType == ServerControlEventType.ServerUnresponsive)
ddTcs.SetResult(null);
};
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
var ddTestTask = dreamDaemonExecutor.RunDreamDaemon(launchParameters, null, dreamDaemonPath, new TemporaryDmbProvider(ioManager.ResolvePath(ioManager.GetDirectoryName(dirA)), ioManager.ResolvePath(ioManager.ConcatPath(dirA, String.Concat(job.DmeName, ".dmb")))), interopInfo, true, cts.Token);
await Task.WhenAny(ddTcs.Task, ddTestTask).ConfigureAwait(false);
if (!ddTestTask.IsCompleted)
cts.Cancel();
return await ddTestTask.ConfigureAwait(false) == 0 && !ddTcs.Task.IsCompleted;
}
}
}
/// <summary>
/// Compiles a .dme with DreamMaker
/// </summary>
/// <param name="dreamMakerPath">The path to the DreamMaker executable</param>
/// <param name="job">The <see cref="Host.Models.CompileJob"/> for the operation</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task RunDreamMaker(string dreamMakerPath, Host.Models.CompileJob job, CancellationToken cancellationToken)
{
using (var dm = new Process())
{
dm.StartInfo.FileName = dreamMakerPath;
dm.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "-clean {0}.dme", job.DmeName);
dm.StartInfo.WorkingDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
dm.StartInfo.RedirectStandardOutput = true;
dm.StartInfo.RedirectStandardError = true;
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;
dm.EnableRaisingEvents = true;
var dmTcs = new TaskCompletionSource<object>();
dm.Exited += (a, b) => dmTcs.SetResult(null);
dm.Start();
try
{
using (cancellationToken.Register(() => dmTcs.SetCanceled()))
await dmTcs.Task.ConfigureAwait(false);
}
finally
{
if (!dm.HasExited)
{
dm.Kill();
dm.WaitForExit();
}
}
job.Output = OutputList.ToString();
job.ExitCode = dm.ExitCode;
}
}
async Task ModifyDme(Host.Models.CompileJob job, CancellationToken cancellationToken)
{
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
var dmePath = ioManager.ConcatPath(dirA, String.Concat(job.DmeName, ".dme"));
var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
var dmeModificationsTask = configuration.CopyDMFilesTo(ioManager.ResolvePath(dirA), cancellationToken);
var dmeBytes = await dmeReadTask.ConfigureAwait(false);
var dme = Encoding.UTF8.GetString(dmeBytes);
var dmeModifications = await dmeModificationsTask.ConfigureAwait(false);
if (!dmeModifications.Any())
return;
var dmeLines = new List<string>(dme.Split(new[] { Environment.NewLine }, StringSplitOptions.None));
for (var I = 0; I < dmeLines.Count; ++I)
{
var line = dmeLines[I];
if (line.Contains("BEGIN_INCLUDE"))
{
dmeLines.InsertRange(I + 1, dmeModifications);
break;
}
}
dmeBytes = Encoding.UTF8.GetBytes(String.Join(Environment.NewLine, dmeLines));
await ioManager.WriteAllBytes(dmePath, dmeBytes, cancellationToken).ConfigureAwait(false);
}
/// <inheritdoc />
public async Task<Host.Models.CompileJob> Compile(string dmeName, CancellationToken cancellationToken)
{
var job = new Host.Models.CompileJob
{
DirectoryName = Guid.NewGuid(),
StartedAt = DateTimeOffset.Now,
DmeName = dmeName
};
await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false);
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), "B");
async Task CleanupFailedCompile()
{
try
{
await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false);
}
catch { }
};
try
{
//copy the repository
var fullDirA = ioManager.ResolvePath(dirA);
using (var repository = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
job.RevisionInformation = new Host.Models.RevisionInformation
{
Commit = repository.Head
};
await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);
}
await ModifyDme(job, cancellationToken).ConfigureAwait(false);
//run compiler, verify api
var ddVerified = await byond.UseExecutables(async (dreamMakerPath, dreamDaemonPath) =>
{
await RunDreamMaker(dreamMakerPath, job, cancellationToken).ConfigureAwait(false);
return await VerifyApi(dreamDaemonPath, job, cancellationToken).ConfigureAwait(false);
}, true).ConfigureAwait(false);
if(!ddVerified)
{
//server never validated
job.FinishedAt = DateTimeOffset.Now;
await CleanupFailedCompile().ConfigureAwait(false);
return job;
}
job.DMApiValidated = true;
//duplicate the dmb et al
await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
//symlink in the static data
var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
await configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken).ConfigureAwait(false);
await symATask.ConfigureAwait(false);
job.FinishedAt = DateTimeOffset.Now;
return job;
}
catch
{
await CleanupFailedCompile().ConfigureAwait(false);
throw;
}
}
}
}
@@ -27,11 +27,10 @@ namespace Tgstation.Server.Host.Components
/// Lock the current installation's location and run an <paramref name="operation"/>
/// </summary>
/// <typeparam name="T">The return type of <paramref name="operation"/></typeparam>
/// <param name="operation">A <see cref="Func{T, TResult}"/> taking the path to either dm.exe or dreamdaemon.exe and returning a <see cref="Task"/></param>
/// <param name="operation">A <see cref="Func{T1, T2, TResult}"/> taking the path to either dm.exe and dreamdaemon.exe and returning a <see cref="Task"/></param>
/// <param name="stagedIfExists">Use the staged installation if possible</param>
/// <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{TResult}"/> resulting in the return type of <paramref name="operation"/></returns>
Task<T> UseExecutable<T>(Func<string, Task<T>> operation, bool stagedIfExists, bool dreamDaemon);
Task<T> UseExecutables<T>(Func<string, string, Task<T>> operation, bool stagedIfExists);
/// <summary>
/// Clears the cache folder
@@ -1,5 +1,6 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components
{
@@ -8,14 +8,14 @@ namespace Tgstation.Server.Host.Components
/// <summary>
/// For managing the compiler
/// </summary>
interface IDreamMaker : IHostedService
interface IDreamMaker
{
/// <summary>
/// Starts a compile
/// </summary>
/// <param name="dmePath">The .dme file to use</param>
/// <param name="dmeName">The .dme file to use without the extension</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the partially populated <see cref="CompileJob"/> for the operation</returns>
Task<CompileJob> Compile(string dmePath, CancellationToken cancellationToken);
/// <returns>A <see cref="Task{TResult}"/> resulting in the partially populated <see cref="CompileJob"/> for the operation. In particular, note the <see cref="CompileJob.RevisionInformation"/> field will only have it's <see cref="Api.Models.Internal.RevisionInformation.Commit"/> field populated</returns>
Task<CompileJob> Compile(string dmeName, CancellationToken cancellationToken);
}
}
@@ -9,18 +9,8 @@ namespace Tgstation.Server.Host.Components
/// </summary>
interface IInterop
{
bool SecondaryIsOther { get; set; }
IInteropControl ReconnectToRun(string primaryAccessToken, string secondaryAccessToken, ushort primaryPort, ushort secondaryPort);
void SetServerControlHandler(Func<ServerControlEvent, CancellationToken, Task> serverControlHandler);
void SetChatMessageHandler(Func<ChatMessageEventArgs, CancellationToken, Task> chatMessageHandler);
Task<Version> GetApiVersion(CancellationToken cancellationToken);
void SetRun(ushort? port, string accessToken, bool primary);
Task ActivateOtherServer(CancellationToken cancellationToken);
Task<string> ChatCommand(string command, string arguments, CancellationToken cancellationToken);
void OnServerPrimed(Action actionToTake);
IInteropControl CreateRun(ushort primaryPort, ushort? secondaryPort, Func<ChatMessageEventArgs, CancellationToken, Task> chatMessageHandler);
}
}
@@ -0,0 +1,29 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.Components
{
/// <summary>
/// Represents a control set for one or two DreamDaemon instances
/// </summary>
interface IInteropControl : IDisposable
{
/// <summary>
/// When a server control message arrives
/// </summary>
event EventHandler<ServerControlEvent> OnServerControl;
bool TwinServerMode { get; }
bool SecondaryIsOther { get; }
string PrimaryAccessToken { get; }
string SecondaryAccessToken { get; }
Task ActivateOtherServer(CancellationToken cancellationToken);
Task ChangePort(ushort newPort, bool forPrimary, CancellationToken cancellationToken);
}
}
@@ -1,6 +1,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components
{
@@ -40,8 +40,8 @@ namespace Tgstation.Server.Host.Components
metadata.Name = newName;
}
public Task StartAsync(CancellationToken cancellationToken) => Task.WhenAll(RepositoryManager.StartAsync(cancellationToken), DreamMaker.StartAsync(cancellationToken), DreamDaemon.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken));
public Task StartAsync(CancellationToken cancellationToken) => Task.WhenAll(RepositoryManager.StartAsync(cancellationToken), DreamDaemon.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken));
public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(RepositoryManager.StopAsync(cancellationToken), DreamMaker.StopAsync(cancellationToken), DreamDaemon.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken));
public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(RepositoryManager.StopAsync(cancellationToken), DreamDaemon.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken));
}
}
@@ -49,6 +49,8 @@ namespace Tgstation.Server.Host.Components
/// <inheritdoc />
public IInstance GetInstance(Host.Models.Instance metadata)
{
if (metadata == null)
throw new ArgumentNullException(nameof(metadata));
lock (this)
{
if (!instances.TryGetValue(metadata.Id, out IInstance instance))
@@ -86,6 +88,8 @@ namespace Tgstation.Server.Host.Components
/// <inheritdoc />
public async Task OfflineInstance(Host.Models.Instance metadata, CancellationToken cancellationToken)
{
if (metadata == null)
throw new ArgumentNullException(nameof(metadata));
IInstance instance;
lock (this)
{
@@ -99,6 +103,8 @@ namespace Tgstation.Server.Host.Components
/// <inheritdoc />
public async Task OnlineInstance(Host.Models.Instance metadata, CancellationToken cancellationToken)
{
if (metadata == null)
throw new ArgumentNullException(nameof(metadata));
var instance = instanceFactory.CreateInstance(metadata);
lock (this)
{
@@ -133,6 +139,10 @@ namespace Tgstation.Server.Host.Components
/// <inheritdoc />
public Task<bool> PreserveActiveExecutablesIfNecessary(DreamDaemonLaunchParameters launchParameters, string accessToken, int pid, bool primary)
{
if (launchParameters == null)
throw new ArgumentNullException(nameof(launchParameters));
if (accessToken == null)
throw new ArgumentNullException(nameof(accessToken));
throw new NotImplementedException();
}
}
@@ -2,6 +2,9 @@
namespace Tgstation.Server.Host.Components.Models
{
/// <summary>
/// This model mirrors /datum/tgs_revision_information/test_merge
/// </summary>
sealed class TestMerge : RevisionInformation
{
public int Number { get; set; }
@@ -16,6 +16,10 @@
/// <summary>
/// The server requested that it's process be terminated
/// </summary>
RequestedProcessTermination
RequestedProcessTermination,
/// <summary>
/// The server has stopped responding to heartbeats
/// </summary>
ServerUnresponsive,
}
}
@@ -0,0 +1,37 @@
using System;
using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components
{
/// <summary>
/// Temporary <see cref="IDmbProvider"/>
/// </summary>
sealed class TemporaryDmbProvider : IDmbProvider
{
/// <inheritdoc />
public string DmbName { get; }
/// <inheritdoc />
public string PrimaryDirectory { get; }
/// <inheritdoc />
public string SecondaryDirectory => throw new NotSupportedException();
/// <inheritdoc />
public RevisionInformation RevisionInformation => null;
/// <summary>
/// Construct a <see cref="TemporaryDmbProvider"/>
/// </summary>
/// <param name="directory">The value of <see cref="PrimaryDirectory"/></param>
/// <param name="dmb">The value of <see cref="DmbName"/></param>
public TemporaryDmbProvider(string directory, string dmb)
{
DmbName = dmb ?? throw new ArgumentNullException(nameof(dmb));
PrimaryDirectory = directory ?? throw new ArgumentNullException(nameof(directory));
}
/// <inheritdoc />
public void Dispose() { }
}
}
+123 -112
View File
@@ -4,7 +4,6 @@ using System.Threading.Tasks;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Models;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Security;
namespace Tgstation.Server.Host.Components
{
@@ -32,10 +31,6 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IDmbFactory dmbFactory;
/// <summary>
/// The <see cref="ICryptographySuite"/> for the <see cref="Watchdog"/>
/// </summary>
readonly ICryptographySuite cryptographySuite;
/// <summary>
/// The <see cref="IEventConsumer"/> for the <see cref="Watchdog"/>
/// </summary>
readonly IEventConsumer eventConsumer;
@@ -65,18 +60,16 @@ namespace Tgstation.Server.Host.Components
/// <param name="dreamDaemonExecutor">The value of <see cref="dreamDaemonExecutor"/></param>
/// <param name="interop">The value of <see cref="interop"/></param>
/// <param name="dmbFactory">The value of <see cref="dmbFactory"/></param>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/></param>
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/></param>
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
/// <param name="instanceId">The value of <see cref="instanceId"/></param>
public Watchdog(IByond byond, IChat chat, IDreamDaemonExecutor dreamDaemonExecutor, IInterop interop, IDmbFactory dmbFactory, ICryptographySuite cryptographySuite, IEventConsumer eventConsumer, IInstanceManager instanceManager, long instanceId)
public Watchdog(IByond byond, IChat chat, IDreamDaemonExecutor dreamDaemonExecutor, IInterop interop, IDmbFactory dmbFactory, IEventConsumer eventConsumer, IInstanceManager instanceManager, long instanceId)
{
this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
this.dreamDaemonExecutor = dreamDaemonExecutor ?? throw new ArgumentNullException(nameof(dreamDaemonExecutor));
this.interop = interop ?? throw new ArgumentNullException(nameof(interop));
this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
@@ -154,12 +147,11 @@ namespace Tgstation.Server.Host.Components
HostPath = Application.HostingPath,
InstanceId = instanceId,
NextPort = isPrimary ? launchParameters.SecondaryPort : launchParameters.PrimaryPort,
//this line feels hacky, change it and remove the instanceManager dep?
InstanceName = instanceManager.GetInstance(new Host.Models.Instance { Id = instanceId }).GetMetadata().Name,
};
var ddTask = byond.UseExecutable(dreamDaemonPath => RunServer(launchParameters, onSuccessfulStartup, interopInfo, dreamDaemonPath, ddToken), false, true);
interop.SetRun(isPrimary ? launchParameters.PrimaryPort : launchParameters.SecondaryPort, accessToken, isPrimary);
return ddTask;
return byond.UseExecutables((dreamMakerPath, dreamDaemonPath) => RunServer(launchParameters, onSuccessfulStartup, interopInfo, dreamDaemonPath, ddToken), false);
}
catch
{
@@ -191,6 +183,19 @@ namespace Tgstation.Server.Host.Components
return false;
}
/// <summary>
/// Handle <see cref="ChatMessageEventArgs"/>
/// </summary>
/// <param name="e">The <see cref="ChatMessageEventArgs"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task HandleChatMessage(ChatMessageEventArgs e, CancellationToken cancellationToken)
{
if (e == null)
throw new ArgumentNullException(nameof(e));
return chat.SendMessage(e.ChatResponse.Message, e.ChatResponse.ChannelIds, cancellationToken);
}
/// <summary>
/// Main <see cref="Watchdog"/> loop
/// </summary>
@@ -203,134 +208,140 @@ namespace Tgstation.Server.Host.Components
if (await byond.GetVersion(cancellationToken).ConfigureAwait(false) == null)
throw new InvalidOperationException("No byond version installed!");
await byond.ClearCache(cancellationToken).ConfigureAwait(false);
var accessToken = cryptographySuite.GetSecureString();
var initialLaunchParameters = await launchParametersFactory.GetLaunchParameters(cancellationToken).ConfigureAwait(false);
var retries = 0;
do
{
var retryDelay = (int)Math.Min(Math.Pow(2, retries), TimeSpan.FromHours(1).Milliseconds); //max of one hour
await Task.Delay(retryDelay, cancellationToken).ConfigureAwait(false);
//load the event tcs' and get the initial launch parameters
var primaryPrimedTcs = new TaskCompletionSource<object>();
interop.OnServerPrimed(() => primaryPrimedTcs.SetResult(null));
var initialLaunchParameters = await launchParametersFactory.GetLaunchParameters(cancellationToken).ConfigureAwait(false);
//start the primary server
var ddPrimaryTask = StartServer(initialLaunchParameters, onSuccessfulStartup, accessToken, true, cancellationToken, out CancellationTokenSource primaryCts);
try
using (var control = interop.CreateRun(initialLaunchParameters.PrimaryPort, initialLaunchParameters.SecondaryPort, HandleChatMessage))
{
//wait to make sure we got this far
await onSuccessfulStartup.Task.ConfigureAwait(false);
onSuccessfulStartup = null;
//wait for either the server to exit or be primed
await Task.WhenAny(ddPrimaryTask, primaryPrimedTcs.Task).ConfigureAwait(false);
if (ddPrimaryTask.IsCompleted)
var primaryPrimedTcs = new TaskCompletionSource<object>();
control.OnServerControl += (sender, e) =>
{
if (await HandleServerCrashed(ddPrimaryTask, true, cancellationToken).ConfigureAwait(false))
return;
++retries;
continue;
}
if (e.FromPrimaryServer && e.EventType == ServerControlEventType.ServerPrimed)
primaryPrimedTcs.TrySetResult(null);
};
var launchParameters = initialLaunchParameters;
Task<int> ddSecondaryTask = null;
CancellationTokenSource secondaryCts = null;
//start the primary server
var ddPrimaryTask = StartServer(initialLaunchParameters, onSuccessfulStartup, control.PrimaryAccessToken, true, cancellationToken, out CancellationTokenSource primaryCts);
try
{
do
//wait to make sure we got this far
await onSuccessfulStartup.Task.ConfigureAwait(false);
onSuccessfulStartup = null;
//wait for either the server to exit or be primed
await Task.WhenAny(ddPrimaryTask, primaryPrimedTcs.Task).ConfigureAwait(false);
if (ddPrimaryTask.IsCompleted)
{
if (ddSecondaryTask == null)
//start the secondary server
ddSecondaryTask = StartServer(initialLaunchParameters, null, accessToken, false, cancellationToken, out secondaryCts);
if (await HandleServerCrashed(ddPrimaryTask, true, cancellationToken).ConfigureAwait(false))
return;
++retries;
continue;
}
var newDmbTask = dmbFactory.OnNewerDmb();
//now we wait for something to happen
await Task.WhenAny(ddSecondaryTask, ddPrimaryTask, newDmbTask).ConfigureAwait(false);
//some helpers
void PrimaryRestart()
var launchParameters = initialLaunchParameters;
Task<int> ddSecondaryTask = null;
CancellationTokenSource secondaryCts = null;
try
{
do
{
primaryCts.Dispose();
ddPrimaryTask = StartServer(initialLaunchParameters, null, accessToken, true, cancellationToken, out primaryCts);
}
void SecondaryRestart()
{
ddSecondaryTask = null;
secondaryCts.Dispose();
};
Task<bool> PrimaryCrash() => HandleServerCrashed(ddPrimaryTask, interop.SecondaryIsOther, cancellationToken);
Task<bool> SecondaryCrash() => HandleServerCrashed(ddSecondaryTask, !interop.SecondaryIsOther, cancellationToken);
if (ddSecondaryTask == null)
//start the secondary server
ddSecondaryTask = StartServer(initialLaunchParameters, null, control.SecondaryAccessToken, false, cancellationToken, out secondaryCts);
//update available
if (newDmbTask.IsCompleted)
{
//restart the other server but don't treat it as an error
launchParameters = await launchParametersFactory.GetLaunchParameters(cancellationToken).ConfigureAwait(false);
//restart other server
if (interop.SecondaryIsOther)
var newDmbTask = dmbFactory.OnNewerDmb();
//now we wait for something to happen
await Task.WhenAny(ddSecondaryTask, ddPrimaryTask, newDmbTask).ConfigureAwait(false);
//some helpers
void PrimaryRestart()
{
secondaryCts.Cancel();
await ddSecondaryTask.ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
primaryCts.Dispose();
ddPrimaryTask = StartServer(initialLaunchParameters, null, control.PrimaryAccessToken, true, cancellationToken, out primaryCts);
}
void SecondaryRestart()
{
ddSecondaryTask = null;
secondaryCts.Dispose();
};
Task<bool> PrimaryCrash() => HandleServerCrashed(ddPrimaryTask, control.SecondaryIsOther, cancellationToken);
Task<bool> SecondaryCrash() => HandleServerCrashed(ddSecondaryTask, !control.SecondaryIsOther, cancellationToken);
//update available
if (newDmbTask.IsCompleted)
{
//restart the other server but don't treat it as an error
launchParameters = await launchParametersFactory.GetLaunchParameters(cancellationToken).ConfigureAwait(false);
//restart other server
if (control.SecondaryIsOther)
{
secondaryCts.Cancel();
await ddSecondaryTask.ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
SecondaryRestart();
}
else
{
primaryCts.Cancel();
await ddPrimaryTask.ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
PrimaryRestart();
}
continue;
}
//crash of both servers
if (ddSecondaryTask.IsCompleted && ddPrimaryTask.IsCompleted)
{
//catastrophic, start over
var t1 = PrimaryCrash();
await Task.WhenAll(t1, SecondaryCrash()).ConfigureAwait(false);
if (t1.Result)
return;
++retries;
continue;
}
//below this point: crash of single server
//activate the other server and load new launch params
var otherServerActivation = control.ActivateOtherServer(cancellationToken);
launchParameters = await launchParametersFactory.GetLaunchParameters(cancellationToken).ConfigureAwait(false);
await otherServerActivation.ConfigureAwait(false);
//crash of secondary server
if (ddSecondaryTask.IsCompleted)
{
if (await SecondaryCrash().ConfigureAwait(false))
return;
SecondaryRestart();
}
//crash of primary server
else
{
primaryCts.Cancel();
await ddPrimaryTask.ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
if (await PrimaryCrash().ConfigureAwait(false))
return;
PrimaryRestart();
}
continue;
}
//crash of both servers
if (ddSecondaryTask.IsCompleted && ddPrimaryTask.IsCompleted)
{
//catastrophic, start over
var t1 = PrimaryCrash();
await Task.WhenAll(t1, SecondaryCrash()).ConfigureAwait(false);
if (t1.Result)
return;
++retries;
continue;
}
//below this point: crash of single server
//activate the other server and load new launch params
var otherServerActivation = interop.ActivateOtherServer(cancellationToken);
launchParameters = await launchParametersFactory.GetLaunchParameters(cancellationToken).ConfigureAwait(false);
await otherServerActivation.ConfigureAwait(false);
//crash of secondary server
if (ddSecondaryTask.IsCompleted)
{
if (await SecondaryCrash().ConfigureAwait(false))
return;
SecondaryRestart();
}
//crash of primary server
else
{
if (await PrimaryCrash().ConfigureAwait(false))
return;
PrimaryRestart();
}
} while (true);
} while (true);
}
finally
{
secondaryCts?.Dispose();
}
}
finally
{
secondaryCts?.Dispose();
primaryCts.Dispose();
}
}
finally
{
primaryCts.Dispose();
}
} while (true);
}