mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-27 15:07:03 +01:00
DreamMaker is done
This commit is contained in:
+28
-8
@@ -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() { }
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user