From dff5f975e6227d508a59e5f8102ada7b02f43cf8 Mon Sep 17 00:00:00 2001 From: Cyberboss Date: Fri, 4 May 2018 11:16:39 -0400 Subject: [PATCH] DreamMaker is done --- src/DMAPI/tgs/v4/api.dm | 36 ++- .../Models/Internal/CompileJob.cs | 8 +- .../Components/DreamDaemon.cs | 19 +- .../Components/DreamMaker.cs | 276 ++++++++++++++++++ .../Components/IByond.cs | 5 +- .../Components/IDmbFactory.cs | 1 + .../Components/IDreamMaker.cs | 8 +- .../Components/IInterop.cs | 14 +- .../Components/IInteropControl.cs | 29 ++ .../Components/IRepository.cs | 1 + .../Components/Instance.cs | 4 +- .../Components/InstanceManager.cs | 10 + .../Components/Models/TestMerge.cs | 3 + .../Components/ServerControlEventType.cs | 6 +- .../Components/TemporaryDmbProvider.cs | 37 +++ .../Components/Watchdog.cs | 235 ++++++++------- 16 files changed, 528 insertions(+), 164 deletions(-) create mode 100644 src/Tgstation.Server.Host/Components/DreamMaker.cs create mode 100644 src/Tgstation.Server.Host/Components/IInteropControl.cs create mode 100644 src/Tgstation.Server.Host/Components/TemporaryDmbProvider.cs diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm index e8ba604067..981a30e1ad 100644 --- a/src/DMAPI/tgs/v4/api.dm +++ b/src/DMAPI/tgs/v4/api.dm @@ -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() diff --git a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs index d31366048f..0833d79e34 100644 --- a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs +++ b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs @@ -26,14 +26,14 @@ namespace Tgstation.Server.Api.Models.Internal public DateTimeOffset FinishedAt { get; set; } /// - /// The detected DMAPI version + /// If the DMAPI version detected is compatible /// - public Version DMApiVersion { get; set; } + public bool DMApiValidated { get; set; } /// /// The .dme file used for compilation /// - public string DmePath { get; set; } + public string DmeName { get; set; } /// /// Textual output of DM @@ -43,7 +43,7 @@ namespace Tgstation.Server.Api.Models.Internal /// /// The Game folder the results were compiled into /// - public Guid? OutputGuid { get; set; } + public Guid? DirectoryName { get; set; } /// /// Exit code of DM. If diff --git a/src/Tgstation.Server.Host/Components/DreamDaemon.cs b/src/Tgstation.Server.Host/Components/DreamDaemon.cs index 04055e8757..1d6846ccb3 100644 --- a/src/Tgstation.Server.Host/Components/DreamDaemon.cs +++ b/src/Tgstation.Server.Host/Components/DreamDaemon.cs @@ -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 /// public void Dispose() => semaphore.Dispose(); - /// - /// Handler for server control events - /// - /// The - /// The for the operation - /// A representing the running operation - static async Task OnServerControl(ServerControlEvent serverControlEventArgs, CancellationToken cancellationToken) - { - if (serverControlEventArgs == null) - throw new ArgumentNullException(nameof(serverControlEventArgs)); - - await Task.Yield(); - throw new NotImplementedException(); - } - /// public async Task CancelGracefulActions(CancellationToken cancellationToken) { diff --git a/src/Tgstation.Server.Host/Components/DreamMaker.cs b/src/Tgstation.Server.Host/Components/DreamMaker.cs new file mode 100644 index 0000000000..4abd5e10b5 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/DreamMaker.cs @@ -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 +{ + /// + sealed class DreamMaker : IDreamMaker + { + /// + /// Name of the primary directory used for compilation + /// + const string ADirectoryName = "A"; + + /// + /// The for + /// + readonly IRepositoryManager repositoryManager; + /// + /// The for + /// + readonly IIOManager ioManager; + /// + /// The for + /// + readonly IConfiguration configuration; + /// + /// The for + /// + readonly IDreamDaemonExecutor dreamDaemonExecutor; + /// + /// The for + /// + readonly IByond byond; + /// + /// The for + /// + readonly IInterop interop; + /// + /// The for + /// + readonly ICryptographySuite cryptographySuite; + + /// + /// Construct + /// + /// The value of + /// The value of + /// The value of + /// The value of + /// The value of + /// The value of + /// The value of + 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)); + } + + /// + /// Run a quick DD instance to test the DMAPI is installed on the target code + /// + /// The path to the DreamDaemon executable + /// The for the operation + /// The for the operation + /// A resulting in if the DMAPI was successfully validated, otherwise + async Task 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(); + 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; + } + } + } + + /// + /// Compiles a .dme with DreamMaker + /// + /// The path to the DreamMaker executable + /// The for the operation + /// The for the operation + /// A representing the running operation + 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(); + 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(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); + } + + /// + public async Task 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; + } + } + } +} diff --git a/src/Tgstation.Server.Host/Components/IByond.cs b/src/Tgstation.Server.Host/Components/IByond.cs index b49cdc826b..acf4f4c7cc 100644 --- a/src/Tgstation.Server.Host/Components/IByond.cs +++ b/src/Tgstation.Server.Host/Components/IByond.cs @@ -27,11 +27,10 @@ namespace Tgstation.Server.Host.Components /// Lock the current installation's location and run an /// /// The return type of - /// A taking the path to either dm.exe or dreamdaemon.exe and returning a + /// A taking the path to either dm.exe and dreamdaemon.exe and returning a /// Use the staged installation if possible - /// Pass the path of dreamdaemon.exe to if dm.exe otherwise /// A resulting in the return type of - Task UseExecutable(Func> operation, bool stagedIfExists, bool dreamDaemon); + Task UseExecutables(Func> operation, bool stagedIfExists); /// /// Clears the cache folder diff --git a/src/Tgstation.Server.Host/Components/IDmbFactory.cs b/src/Tgstation.Server.Host/Components/IDmbFactory.cs index 2ed1a771ff..19ca0dc1ae 100644 --- a/src/Tgstation.Server.Host/Components/IDmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/IDmbFactory.cs @@ -1,5 +1,6 @@ using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components { diff --git a/src/Tgstation.Server.Host/Components/IDreamMaker.cs b/src/Tgstation.Server.Host/Components/IDreamMaker.cs index 9f119ac27d..8e6036dc55 100644 --- a/src/Tgstation.Server.Host/Components/IDreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/IDreamMaker.cs @@ -8,14 +8,14 @@ namespace Tgstation.Server.Host.Components /// /// For managing the compiler /// - interface IDreamMaker : IHostedService + interface IDreamMaker { /// /// Starts a compile /// - /// The .dme file to use + /// The .dme file to use without the extension /// The for the operation - /// A resulting in the partially populated for the operation - Task Compile(string dmePath, CancellationToken cancellationToken); + /// A resulting in the partially populated for the operation. In particular, note the field will only have it's field populated + Task Compile(string dmeName, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/IInterop.cs b/src/Tgstation.Server.Host/Components/IInterop.cs index 32edd2e047..ff5337dc30 100644 --- a/src/Tgstation.Server.Host/Components/IInterop.cs +++ b/src/Tgstation.Server.Host/Components/IInterop.cs @@ -9,18 +9,8 @@ namespace Tgstation.Server.Host.Components /// interface IInterop { - bool SecondaryIsOther { get; set; } + IInteropControl ReconnectToRun(string primaryAccessToken, string secondaryAccessToken, ushort primaryPort, ushort secondaryPort); - void SetServerControlHandler(Func serverControlHandler); - void SetChatMessageHandler(Func chatMessageHandler); - - Task GetApiVersion(CancellationToken cancellationToken); - - void SetRun(ushort? port, string accessToken, bool primary); - - Task ActivateOtherServer(CancellationToken cancellationToken); - - Task ChatCommand(string command, string arguments, CancellationToken cancellationToken); - void OnServerPrimed(Action actionToTake); + IInteropControl CreateRun(ushort primaryPort, ushort? secondaryPort, Func chatMessageHandler); } } diff --git a/src/Tgstation.Server.Host/Components/IInteropControl.cs b/src/Tgstation.Server.Host/Components/IInteropControl.cs new file mode 100644 index 0000000000..2fc3f5ea6d --- /dev/null +++ b/src/Tgstation.Server.Host/Components/IInteropControl.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Components +{ + /// + /// Represents a control set for one or two DreamDaemon instances + /// + interface IInteropControl : IDisposable + { + /// + /// When a server control message arrives + /// + event EventHandler 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); + } +} diff --git a/src/Tgstation.Server.Host/Components/IRepository.cs b/src/Tgstation.Server.Host/Components/IRepository.cs index e52261b3dc..7acd5053df 100644 --- a/src/Tgstation.Server.Host/Components/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/IRepository.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components { diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index ce439ee988..454b1063de 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -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)); } } diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 59f4ee867c..ed93f70fa9 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -49,6 +49,8 @@ namespace Tgstation.Server.Host.Components /// 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 /// 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 /// 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 /// public Task 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(); } } diff --git a/src/Tgstation.Server.Host/Components/Models/TestMerge.cs b/src/Tgstation.Server.Host/Components/Models/TestMerge.cs index 1996ad5bc3..cda4aaa5ea 100644 --- a/src/Tgstation.Server.Host/Components/Models/TestMerge.cs +++ b/src/Tgstation.Server.Host/Components/Models/TestMerge.cs @@ -2,6 +2,9 @@ namespace Tgstation.Server.Host.Components.Models { + /// + /// This model mirrors /datum/tgs_revision_information/test_merge + /// sealed class TestMerge : RevisionInformation { public int Number { get; set; } diff --git a/src/Tgstation.Server.Host/Components/ServerControlEventType.cs b/src/Tgstation.Server.Host/Components/ServerControlEventType.cs index 11a9e23bac..9468bcabaa 100644 --- a/src/Tgstation.Server.Host/Components/ServerControlEventType.cs +++ b/src/Tgstation.Server.Host/Components/ServerControlEventType.cs @@ -16,6 +16,10 @@ /// /// The server requested that it's process be terminated /// - RequestedProcessTermination + RequestedProcessTermination, + /// + /// The server has stopped responding to heartbeats + /// + ServerUnresponsive, } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/TemporaryDmbProvider.cs b/src/Tgstation.Server.Host/Components/TemporaryDmbProvider.cs new file mode 100644 index 0000000000..389256d050 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/TemporaryDmbProvider.cs @@ -0,0 +1,37 @@ +using System; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Components +{ + /// + /// Temporary + /// + sealed class TemporaryDmbProvider : IDmbProvider + { + /// + public string DmbName { get; } + + /// + public string PrimaryDirectory { get; } + + /// + public string SecondaryDirectory => throw new NotSupportedException(); + + /// + public RevisionInformation RevisionInformation => null; + + /// + /// Construct a + /// + /// The value of + /// The value of + public TemporaryDmbProvider(string directory, string dmb) + { + DmbName = dmb ?? throw new ArgumentNullException(nameof(dmb)); + PrimaryDirectory = directory ?? throw new ArgumentNullException(nameof(directory)); + } + + /// + public void Dispose() { } + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog.cs index f7eaf3d327..a98ce38c27 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog.cs @@ -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 /// readonly IDmbFactory dmbFactory; /// - /// The for the - /// - readonly ICryptographySuite cryptographySuite; - /// /// The for the /// readonly IEventConsumer eventConsumer; @@ -65,18 +60,16 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The value of /// The value of /// The value of /// The value of - 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; } + /// + /// Handle + /// + /// The + /// The for the operation + /// A representing the running operation + Task HandleChatMessage(ChatMessageEventArgs e, CancellationToken cancellationToken) + { + if (e == null) + throw new ArgumentNullException(nameof(e)); + return chat.SendMessage(e.ChatResponse.Message, e.ChatResponse.ChannelIds, cancellationToken); + } + /// /// Main loop /// @@ -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(); - 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(); + 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 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 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 PrimaryCrash() => HandleServerCrashed(ddPrimaryTask, interop.SecondaryIsOther, cancellationToken); - Task 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 PrimaryCrash() => HandleServerCrashed(ddPrimaryTask, control.SecondaryIsOther, cancellationToken); + Task 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); }