From 64b4202b769933a15b84db36159edda6938c040a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Wed, 27 Jun 2018 14:36:39 -0400 Subject: [PATCH] Revert "Revert "Some watchdog stuff I forgot to merge then went looking for"" --- appveyor.yml | 2 + .../Internal/DreamDaemonLaunchParameters.cs | 15 +- .../Models/Internal/DreamDaemonSettings.cs | 12 +- .../Components/DreamDaemon.cs | 9 +- .../Components/DreamDaemonExecutor.cs | 8 +- .../Components/DreamMaker.cs | 4 +- .../Components/IDmbProvider.cs | 6 +- .../Components/IDreamDaemon.cs | 10 ++ .../Components/IDreamDaemonExecutor.cs | 3 +- .../Components/IInteropControl.cs | 2 + .../Components/IWatchdog.cs | 5 + .../Components/TemporaryDmbProvider.cs | 2 +- .../Components/Watchdog.cs | 45 +++-- .../Controllers/DreamDaemonController.cs | 164 ++++++++++++++++++ .../Controllers/DreamMakerController.cs | 7 +- .../Controllers/JobController.cs | 15 +- .../Controllers/ModelController.cs | 9 + 17 files changed, 279 insertions(+), 39 deletions(-) create mode 100644 src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs diff --git a/appveyor.yml b/appveyor.yml index ebfe1041dc..98c9cb64f1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -41,6 +41,8 @@ test_script: - ps: $wc = New-Object 'System.Net.WebClient' - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx)) - vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Host.Tests\bin\%CONFIGURATION%\netcoreapp2.0\Tgstation.Server.Host.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64 + - ps: $wc = New-Object 'System.Net.WebClient' + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx)) - vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Host.Console.Tests\bin\%CONFIGURATION%\netcoreapp2.0\Tgstation.Server.Host.Console.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64 - ps: $wc = New-Object 'System.Net.WebClient' - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx)) diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs index 8602b302b5..62e0f7eb87 100644 --- a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs +++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs @@ -1,4 +1,5 @@ -using Tgstation.Server.Api.Rights; +using System.ComponentModel.DataAnnotations; +using Tgstation.Server.Api.Rights; namespace Tgstation.Server.Api.Models.Internal { @@ -12,24 +13,28 @@ namespace Tgstation.Server.Api.Models.Internal /// If the BYOND web client can be used to connect to the game server /// [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetWebClient)] - public bool AllowWebClient { get; set; } + [Required] + public bool? AllowWebClient { get; set; } /// /// The level of /// [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetSecurity)] - public DreamDaemonSecurity SecurityLevel { get; set; } + [Required] + public DreamDaemonSecurity? SecurityLevel { get; set; } /// /// The first port uses. This should be the publically advertised port /// [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)] - public ushort PrimaryPort { get; set; } + [Required] + public ushort? PrimaryPort { get; set; } /// /// The second port uses /// [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetPorts)] - public ushort SecondaryPort { get; set; } + [Required] + public ushort? SecondaryPort { get; set; } } } \ No newline at end of file diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonSettings.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonSettings.cs index b4fd279881..72260ee5fc 100644 --- a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonSettings.cs +++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonSettings.cs @@ -1,4 +1,5 @@ -using Tgstation.Server.Api.Rights; +using System.ComponentModel.DataAnnotations; +using Tgstation.Server.Api.Rights; namespace Tgstation.Server.Api.Models.Internal { @@ -11,18 +12,21 @@ namespace Tgstation.Server.Api.Models.Internal /// If starts when it's starts /// [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SetAutoStart)] - public bool AutoStart { get; set; } + [Required] + public bool? AutoStart { get; set; } /// /// If the server is undergoing a soft reset. This may be automatically set by changes to other fields /// [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SoftRestart)] - public bool SoftRestart { get; set; } + [Required] + public bool? SoftRestart { get; set; } /// /// If the server is undergoing a soft shutdown /// [Permissions(ReadRight = DreamDaemonRights.ReadMetadata, WriteRight = DreamDaemonRights.SoftShutdown)] - public bool SoftShutdown { get; set; } + [Required] + public bool? SoftShutdown { get; set; } } } diff --git a/src/Tgstation.Server.Host/Components/DreamDaemon.cs b/src/Tgstation.Server.Host/Components/DreamDaemon.cs index 1d6846ccb3..f699150079 100644 --- a/src/Tgstation.Server.Host/Components/DreamDaemon.cs +++ b/src/Tgstation.Server.Host/Components/DreamDaemon.cs @@ -27,6 +27,12 @@ namespace Tgstation.Server.Host.Components /// public bool SoftStopping { get; private set; } + /// + public DreamDaemonLaunchParameters LastLaunchParameters { get; private set; } + + /// + public Host.Models.CompileJob LastCompileJob => watchdog.CurrentCompileJob; + /// /// The for /// @@ -69,7 +75,7 @@ namespace Tgstation.Server.Host.Components this.watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog)); currentLaunchParameters = initialSettings ?? throw new ArgumentNullException(nameof(initialSettings)); - autoStart = initialSettings.AutoStart; + autoStart = initialSettings.AutoStart.Value; semaphore = new SemaphoreSlim(1); } @@ -198,6 +204,7 @@ namespace Tgstation.Server.Host.Components await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false); try { + LastLaunchParameters = currentLaunchParameters; return currentLaunchParameters; } finally diff --git a/src/Tgstation.Server.Host/Components/DreamDaemonExecutor.cs b/src/Tgstation.Server.Host/Components/DreamDaemonExecutor.cs index 0f3a47b026..3a54783935 100644 --- a/src/Tgstation.Server.Host/Components/DreamDaemonExecutor.cs +++ b/src/Tgstation.Server.Host/Components/DreamDaemonExecutor.cs @@ -56,7 +56,7 @@ namespace Tgstation.Server.Host.Components } /// - public async Task RunDreamDaemon(DreamDaemonLaunchParameters launchParameters, TaskCompletionSource onSuccessfulStartup, string dreamDaemonPath, IDmbProvider dmbProvider, InteropInfo interopInfo, bool alwaysKill, CancellationToken cancellationToken) + public async Task RunDreamDaemon(DreamDaemonLaunchParameters launchParameters, TaskCompletionSource onSuccessfulStartup, string dreamDaemonPath, IDmbProvider dmbProvider, InteropInfo interopInfo, bool alwaysKill, bool asDefaultOtherServer, CancellationToken cancellationToken) { if (launchParameters == null) throw new ArgumentNullException(nameof(launchParameters)); @@ -86,9 +86,9 @@ namespace Tgstation.Server.Host.Components proc.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} {2}-close -{3} -verbose -public -params \"{4}={5}&{6}={7}\"", dmbProvider.DmbName, - isPrimary ? launchParameters.PrimaryPort : launchParameters.SecondaryPort, - launchParameters.AllowWebClient ? "-webclient " : String.Empty, - SecurityWord(launchParameters.SecurityLevel), + isPrimary && !asDefaultOtherServer ? launchParameters.PrimaryPort : launchParameters.SecondaryPort, + launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty, + SecurityWord(launchParameters.SecurityLevel.Value), DreamDaemonParameters.HostVersion, Application.Version, DreamDaemonParameters.InfoJsonPath, jsonPath); diff --git a/src/Tgstation.Server.Host/Components/DreamMaker.cs b/src/Tgstation.Server.Host/Components/DreamMaker.cs index ffd577f98b..e71476b1a4 100644 --- a/src/Tgstation.Server.Host/Components/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/DreamMaker.cs @@ -98,7 +98,7 @@ namespace Tgstation.Server.Host.Components }; using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken)) - using (var control = interop.CreateRun(launchParameters.PrimaryPort, null, null)) + using (var control = interop.CreateRun(launchParameters.PrimaryPort.Value, null, null)) { var interopInfo = new InteropInfo { @@ -113,7 +113,7 @@ namespace Tgstation.Server.Host.Components 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, DmbExtension)))), interopInfo, true, cts.Token); + var ddTestTask = dreamDaemonExecutor.RunDreamDaemon(launchParameters, null, dreamDaemonPath, new TemporaryDmbProvider(ioManager.ResolvePath(ioManager.GetDirectoryName(dirA)), ioManager.ResolvePath(ioManager.ConcatPath(dirA, String.Concat(job.DmeName, DmbExtension)))), interopInfo, true, false, cts.Token); await Task.WhenAny(ddTcs.Task, ddTestTask).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Components/IDmbProvider.cs b/src/Tgstation.Server.Host/Components/IDmbProvider.cs index 4825fb2914..18a5432bd2 100644 --- a/src/Tgstation.Server.Host/Components/IDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/IDmbProvider.cs @@ -22,10 +22,10 @@ namespace Tgstation.Server.Host.Components /// The secondary game directory with a trailing directory separator /// string SecondaryDirectory { get; } - + /// - /// The of the .dmb + /// The of the .dmb /// - RevisionInformation RevisionInformation { get; } + CompileJob CompileJob { get; } } } diff --git a/src/Tgstation.Server.Host/Components/IDreamDaemon.cs b/src/Tgstation.Server.Host/Components/IDreamDaemon.cs index dbd32b9d2a..e435a3af69 100644 --- a/src/Tgstation.Server.Host/Components/IDreamDaemon.cs +++ b/src/Tgstation.Server.Host/Components/IDreamDaemon.cs @@ -41,6 +41,16 @@ namespace Tgstation.Server.Host.Components /// string AccessToken { get; } + /// + /// The most recently used + /// + DreamDaemonLaunchParameters LastLaunchParameters { get; } + + /// + /// The most recently used + /// + Host.Models.CompileJob LastCompileJob { get; } + /// /// Launch DreamDaemon /// diff --git a/src/Tgstation.Server.Host/Components/IDreamDaemonExecutor.cs b/src/Tgstation.Server.Host/Components/IDreamDaemonExecutor.cs index 2f7cbc150d..77980fbe04 100644 --- a/src/Tgstation.Server.Host/Components/IDreamDaemonExecutor.cs +++ b/src/Tgstation.Server.Host/Components/IDreamDaemonExecutor.cs @@ -19,8 +19,9 @@ namespace Tgstation.Server.Host.Components /// The for the .dmb to run /// The for the run /// If the resulting process should never be left alive + /// If the ports will be swapped for the launch /// The for the operation /// A representing the lifetime of the process and resulting in the exit code - Task RunDreamDaemon(DreamDaemonLaunchParameters launchParameters, TaskCompletionSource onSuccessfulStartup, string dreamDaemonPath, IDmbProvider dmbProvider, InteropInfo interopInfo, bool alwaysKill, CancellationToken cancellationToken); + Task RunDreamDaemon(DreamDaemonLaunchParameters launchParameters, TaskCompletionSource onSuccessfulStartup, string dreamDaemonPath, IDmbProvider dmbProvider, InteropInfo interopInfo, bool alwaysKill, bool asDefaultOtherServer, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/IInteropControl.cs b/src/Tgstation.Server.Host/Components/IInteropControl.cs index 2fc3f5ea6d..43b5751415 100644 --- a/src/Tgstation.Server.Host/Components/IInteropControl.cs +++ b/src/Tgstation.Server.Host/Components/IInteropControl.cs @@ -25,5 +25,7 @@ namespace Tgstation.Server.Host.Components Task ActivateOtherServer(CancellationToken cancellationToken); Task ChangePort(ushort newPort, bool forPrimary, CancellationToken cancellationToken); + + void OnNextSwap(Action action); } } diff --git a/src/Tgstation.Server.Host/Components/IWatchdog.cs b/src/Tgstation.Server.Host/Components/IWatchdog.cs index 59882f6c89..5504baa960 100644 --- a/src/Tgstation.Server.Host/Components/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/IWatchdog.cs @@ -8,6 +8,11 @@ namespace Tgstation.Server.Host.Components /// interface IWatchdog { + /// + /// The latest used by the + /// + Host.Models.CompileJob CurrentCompileJob { get; } + /// /// Start the /// diff --git a/src/Tgstation.Server.Host/Components/TemporaryDmbProvider.cs b/src/Tgstation.Server.Host/Components/TemporaryDmbProvider.cs index 389256d050..fc4fa15408 100644 --- a/src/Tgstation.Server.Host/Components/TemporaryDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/TemporaryDmbProvider.cs @@ -18,7 +18,7 @@ namespace Tgstation.Server.Host.Components public string SecondaryDirectory => throw new NotSupportedException(); /// - public RevisionInformation RevisionInformation => null; + public CompileJob CompileJob => null; /// /// Construct a diff --git a/src/Tgstation.Server.Host/Components/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog.cs index a98ce38c27..0194890f02 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog.cs @@ -10,6 +10,9 @@ namespace Tgstation.Server.Host.Components /// sealed class Watchdog : IWatchdog, IDisposable { + /// + public Host.Models.CompileJob CurrentCompileJob { get; private set; } + /// /// The for the /// @@ -85,13 +88,26 @@ namespace Tgstation.Server.Host.Components /// The for the run /// The to be completed once the server starts if any /// The the server + /// The for the operation /// The path to the DreamDaemon executable + /// If the ports should be swapped /// The for the operation /// A resulting in the exit code of DreamDaemon - async Task RunServer(DreamDaemonLaunchParameters launchParameters, TaskCompletionSource onSuccessfulStartup, InteropInfo interopInfo, string dreamDaemonPath, CancellationToken cancellationToken) + async Task RunServer(DreamDaemonLaunchParameters launchParameters, TaskCompletionSource onSuccessfulStartup, InteropInfo interopInfo, IInteropControl control, string dreamDaemonPath, bool asDefaultOtherServer, CancellationToken cancellationToken) { using (var dmb = await dmbFactory.LockNextDmb(cancellationToken).ConfigureAwait(false)) { + void OnBecomingCurrentServer() => CurrentCompileJob = dmb.CompileJob; + + var runningNow = launchParameters.SecondaryPort == interopInfo.NextPort; + if (asDefaultOtherServer) + runningNow = !runningNow; + + if (runningNow) + OnBecomingCurrentServer(); + else + control.OnNextSwap(OnBecomingCurrentServer); + var chatJsonGuid = Guid.NewGuid(); var isPrimary = interopInfo.NextPort == launchParameters.SecondaryPort; @@ -102,8 +118,8 @@ namespace Tgstation.Server.Host.Components interopInfo.ChatCommandsJson = String.Concat(chatJsonGuid, ".commands.json"); //set up revision - interopInfo.Revision = dmb.RevisionInformation; - foreach (var I in dmb.RevisionInformation.TestMerges) + interopInfo.Revision = dmb.CompileJob.RevisionInformation; + foreach (var I in dmb.CompileJob.RevisionInformation.TestMerges) interopInfo.TestMerges.Add(new Models.TestMerge { Author = I.Author, @@ -119,21 +135,22 @@ namespace Tgstation.Server.Host.Components }); using (await chat.TrackJsons(isPrimary ? dmb.PrimaryDirectory : dmb.SecondaryDirectory, interopInfo.ChatChannelsJson, isPrimary ? interopInfo.ChatCommandsJson : null, cancellationToken).ConfigureAwait(false)) - return await dreamDaemonExecutor.RunDreamDaemon(launchParameters, onSuccessfulStartup, dreamDaemonPath, dmb, interopInfo, false, cancellationToken).ConfigureAwait(false); + return await dreamDaemonExecutor.RunDreamDaemon(launchParameters, onSuccessfulStartup, dreamDaemonPath, dmb, interopInfo, false, asDefaultOtherServer, cancellationToken).ConfigureAwait(false); } } /// - /// Locks in a version and runs a server through + /// Locks in a version and runs a server through /// /// The for the run /// The to be completed once the server starts if any - /// The access token for the server + /// The for the operation /// If a primary server is being launched + /// If the ports should be swapped /// The for the operation /// A tied to the lifetime of the resulting /// A resulting in the exit code of DreamDaemon - Task StartServer(DreamDaemonLaunchParameters launchParameters, TaskCompletionSource onSuccessfulStartup, string accessToken, bool isPrimary, CancellationToken cancellationToken, out CancellationTokenSource cancellationTokenSource) + Task StartServer(DreamDaemonLaunchParameters launchParameters, TaskCompletionSource onSuccessfulStartup, IInteropControl control, bool isPrimary, bool isDefaultOther, CancellationToken cancellationToken, out CancellationTokenSource cancellationTokenSource) { cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); try @@ -142,16 +159,16 @@ namespace Tgstation.Server.Host.Components var interopInfo = new InteropInfo { - AccessToken = accessToken, + AccessToken = isPrimary ? control.PrimaryAccessToken : control.SecondaryAccessToken, ApiValidateOnly = false, HostPath = Application.HostingPath, InstanceId = instanceId, - NextPort = isPrimary ? launchParameters.SecondaryPort : launchParameters.PrimaryPort, + NextPort = isPrimary ? launchParameters.SecondaryPort.Value : launchParameters.PrimaryPort.Value, //this line feels hacky, change it and remove the instanceManager dep? InstanceName = instanceManager.GetInstance(new Host.Models.Instance { Id = instanceId }).GetMetadata().Name, }; - return byond.UseExecutables((dreamMakerPath, dreamDaemonPath) => RunServer(launchParameters, onSuccessfulStartup, interopInfo, dreamDaemonPath, ddToken), false); + return byond.UseExecutables((dreamMakerPath, dreamDaemonPath) => RunServer(launchParameters, onSuccessfulStartup, interopInfo, control, dreamDaemonPath, isDefaultOther, ddToken), false); } catch { @@ -216,7 +233,7 @@ namespace Tgstation.Server.Host.Components var retryDelay = (int)Math.Min(Math.Pow(2, retries), TimeSpan.FromHours(1).Milliseconds); //max of one hour await Task.Delay(retryDelay, cancellationToken).ConfigureAwait(false); - using (var control = interop.CreateRun(initialLaunchParameters.PrimaryPort, initialLaunchParameters.SecondaryPort, HandleChatMessage)) + using (var control = interop.CreateRun(initialLaunchParameters.PrimaryPort.Value, initialLaunchParameters.SecondaryPort.Value, HandleChatMessage)) { var primaryPrimedTcs = new TaskCompletionSource(); control.OnServerControl += (sender, e) => @@ -226,7 +243,7 @@ namespace Tgstation.Server.Host.Components }; //start the primary server - var ddPrimaryTask = StartServer(initialLaunchParameters, onSuccessfulStartup, control.PrimaryAccessToken, true, cancellationToken, out CancellationTokenSource primaryCts); + var ddPrimaryTask = StartServer(initialLaunchParameters, onSuccessfulStartup, control, true, false, cancellationToken, out CancellationTokenSource primaryCts); try { //wait to make sure we got this far @@ -253,7 +270,7 @@ namespace Tgstation.Server.Host.Components { if (ddSecondaryTask == null) //start the secondary server - ddSecondaryTask = StartServer(initialLaunchParameters, null, control.SecondaryAccessToken, false, cancellationToken, out secondaryCts); + ddSecondaryTask = StartServer(initialLaunchParameters, null, control, false, false, cancellationToken, out secondaryCts); var newDmbTask = dmbFactory.OnNewerDmb(); @@ -264,7 +281,7 @@ namespace Tgstation.Server.Host.Components void PrimaryRestart() { primaryCts.Dispose(); - ddPrimaryTask = StartServer(initialLaunchParameters, null, control.PrimaryAccessToken, true, cancellationToken, out primaryCts); + ddPrimaryTask = StartServer(initialLaunchParameters, null, control, true, true, cancellationToken, out primaryCts); } void SecondaryRestart() { diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs new file mode 100644 index 0000000000..a4570e2ff7 --- /dev/null +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -0,0 +1,164 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Linq.Expressions; +using System.Net; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api.Models; +using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.Core; +using Tgstation.Server.Host.Models; +using Tgstation.Server.Host.Security; + +namespace Tgstation.Server.Host.Controllers +{ + /// + /// for managing + /// + [Route("/" + nameof(Api.Models.DreamDaemon))] + public sealed class DreamDaemonController : ModelController + { + /// + /// The for the + /// + readonly IJobManager jobManager; + /// + /// The for the + /// + readonly IInstanceManager instanceManager; + + /// + /// Construct a + /// + /// The for the + /// The for the + /// The value of + /// The value of + public DreamDaemonController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IJobManager jobManager, IInstanceManager instanceManager) : base(databaseContext, authenticationContextFactory) + { + this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); + this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); + } + + /// + [TgsAuthorize(DreamDaemonRights.Start)] + public override async Task Create([FromBody] Api.Models.DreamDaemon model, CancellationToken cancellationToken) + { + var instance = instanceManager.GetInstance(Instance); + + if (instance.DreamDaemon.Running) + return StatusCode(HttpStatusCode.Gone); + + var launchParams = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => new DreamDaemonLaunchParameters + { + AllowWebClient = x.DreamDaemonSettings.AllowWebClient, + PrimaryPort = x.DreamDaemonSettings.PrimaryPort, + SecondaryPort = x.DreamDaemonSettings.SecondaryPort, + SecurityLevel = x.DreamDaemonSettings.SecurityLevel + }).FirstAsync(cancellationToken).ConfigureAwait(false); + + await jobManager.RegisterOperation(new Models.Job + { + Description = "Launch DreamDaemon", + CancelRight = (int)DreamDaemonRights.Shutdown, + CancelRightsType = RightsType.DreamDaemon, + Instance = Instance, + StartedBy = AuthenticationContext.User + }, (job, serviceProvider, innerCt) => instance.DreamDaemon.Launch(launchParams, innerCt), cancellationToken).ConfigureAwait(false); + return Ok(); + } + + /// + [TgsAuthorize(DreamDaemonRights.ReadMetadata | DreamDaemonRights.ReadRevision)] + public override async Task Read(CancellationToken cancellationToken) + { + var dd = instanceManager.GetInstance(Instance).DreamDaemon; + + var metadata = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (int)DreamDaemonRights.ReadMetadata) != 0; + var revision = (AuthenticationContext.GetRight(RightsType.DreamDaemon) & (int)DreamDaemonRights.ReadRevision) != 0; + + var settings = metadata ? await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstAsync(cancellationToken).ConfigureAwait(false) : null; + Api.Models.DreamDaemon result = new Api.Models.DreamDaemon(); + if(metadata) + { + result.AutoStart = settings.AutoStart; + result.CurrentPort = dd.CurrentPort; + result.CurrentSecurity = dd.CurrentSecurity; + result.PrimaryPort = dd.LastLaunchParameters.PrimaryPort; + result.AllowWebClient = dd.LastLaunchParameters.AllowWebClient; + result.Running = dd.Running; + result.SecondaryPort = dd.LastLaunchParameters.SecondaryPort; + result.SecurityLevel = dd.LastLaunchParameters.SecurityLevel; + result.SoftRestart = dd.SoftRebooting; + result.SoftShutdown = dd.SoftStopping; + }; + if (revision) + result.CompileJob = dd.LastCompileJob.ToApi(); + + return Json(result); + } + + /// + [TgsAuthorize(DreamDaemonRights.Shutdown)] + public override async Task Delete([FromBody] Api.Models.DreamDaemon model, CancellationToken cancellationToken) + { + var instance = instanceManager.GetInstance(Instance); + + if (!instance.DreamDaemon.Running) + return StatusCode(HttpStatusCode.Gone); + + await instance.DreamDaemon.Terminate(false, cancellationToken).ConfigureAwait(false); + return Ok(); + } + + /// + [TgsAuthorize(DreamDaemonRights.SetAutoStart | DreamDaemonRights.SetPorts | DreamDaemonRights.SetSecurity | DreamDaemonRights.SetWebClient | DreamDaemonRights.SoftRestart | DreamDaemonRights.SoftShutdown | DreamDaemonRights.Start)] + public override async Task Update([FromBody] Api.Models.DreamDaemon model, CancellationToken cancellationToken) + { + var current = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstAsync(cancellationToken).ConfigureAwait(false); + + var userRights = (DreamDaemonRights)AuthenticationContext.GetRight(RightsType.DreamDaemon); + + bool CheckModified(Expression> expression, DreamDaemonRights requiredRight) + { + var memberSelectorExpression = (MemberExpression)expression.Body; + var property = (PropertyInfo)memberSelectorExpression.Member; + + var newVal = property.GetValue(model); + if (newVal == null) + return false; + if (!userRights.HasFlag(requiredRight) && property.GetValue(current) != newVal) + return true; + + property.SetValue(current, newVal); + return false; + }; + + if (!CheckModified(x => x.AllowWebClient, DreamDaemonRights.SetWebClient) + || !CheckModified(x => x.AutoStart, DreamDaemonRights.SetAutoStart) + || !CheckModified(x => x.PrimaryPort, DreamDaemonRights.SetPorts) + || !CheckModified(x => x.SecondaryPort, DreamDaemonRights.SetPorts) + || !CheckModified(x => x.SecurityLevel, DreamDaemonRights.SetSecurity) + || !CheckModified(x => x.SoftRestart, DreamDaemonRights.SoftRestart)) + return Forbid(); + + //interaction with soft stop is a bit different + if (model.SoftShutdown.HasValue) + { + if (current.SoftShutdown != model.SoftShutdown && ((!current.SoftShutdown.Value && !userRights.HasFlag(DreamDaemonRights.SoftShutdown)) || (current.SoftShutdown.Value && !userRights.HasFlag(DreamDaemonRights.Start)))) + return Forbid(); + current.SoftShutdown = model.SoftShutdown; + } + + await instanceManager.GetInstance(Instance).DreamDaemon.ChangeSettings(current, cancellationToken).ConfigureAwait(false); + await DatabaseContext.Save(default).ConfigureAwait(false); + + return Ok(); + } + } +} diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index f94a51537e..c13cb31e11 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -17,7 +17,7 @@ namespace Tgstation.Server.Host.Controllers /// /// Controller for managing the compiler /// - [Route("/DreamMaker")] + [Route("/" + nameof(DreamMaker))] public sealed class DreamMakerController : ModelController { /// @@ -30,7 +30,7 @@ namespace Tgstation.Server.Host.Controllers readonly IInstanceManager instanceManager; /// - /// Construct a + /// Construct a /// /// The for the /// The for the @@ -66,7 +66,8 @@ namespace Tgstation.Server.Host.Controllers Description = "Compile active repository code", StartedBy = AuthenticationContext.User, CancelRightsType = RightsType.DreamMaker, - CancelRight = (int)DreamMakerRights.CancelCompile + CancelRight = (int)DreamMakerRights.CancelCompile, + Instance = Instance }; await jobManager.RegisterOperation(job, (paramJob, serviceProvider, ct) => RunCompile(paramJob, serviceProvider, Instance, ct), cancellationToken).ConfigureAwait(false); return Json(job); diff --git a/src/Tgstation.Server.Host/Controllers/JobController.cs b/src/Tgstation.Server.Host/Controllers/JobController.cs index 982fc72cd8..1dcab2f526 100644 --- a/src/Tgstation.Server.Host/Controllers/JobController.cs +++ b/src/Tgstation.Server.Host/Controllers/JobController.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Controllers /// /// for s /// - [Route("/Job")] + [Route("/" + nameof(Job))] public sealed class JobController : ModelController { /// @@ -60,11 +60,24 @@ namespace Tgstation.Server.Host.Controllers if (job == default(Job)) return NotFound(); + if (job.CancelRight.HasValue && job.CancelRightsType.HasValue && (AuthenticationContext.GetRight(job.CancelRightsType.Value) & job.CancelRight.Value) == 0) + return Forbid(); + if(job.StoppedAt != null) return StatusCode(HttpStatusCode.Gone); await jobManager.CancelJob(job, AuthenticationContext.User, cancellationToken).ConfigureAwait(false); return Ok(); } + + /// + [TgsAuthorize] + public override async Task GetId(long id, CancellationToken cancellationToken) + { + var job = await DatabaseContext.Jobs.Where(x => x.Id == id).Include(x => x.StartedBy).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + if (job == default(Job)) + return NotFound(); + return Json(job.ToApi()); + } } } diff --git a/src/Tgstation.Server.Host/Controllers/ModelController.cs b/src/Tgstation.Server.Host/Controllers/ModelController.cs index 59d2b5c19a..aec3cf3769 100644 --- a/src/Tgstation.Server.Host/Controllers/ModelController.cs +++ b/src/Tgstation.Server.Host/Controllers/ModelController.cs @@ -43,6 +43,15 @@ namespace Tgstation.Server.Host.Controllers [HttpGet] public virtual Task Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); + /// + /// Attempt to get a specific a + /// + /// The for the operation + /// The ID of the model to get + /// A resulting in the of the operation + [HttpGet("/{0}")] + public virtual Task GetId(long id, CancellationToken cancellationToken) => Task.FromResult((IActionResult)NotFound()); + /// /// Attempt to update a ///