diff --git a/src/Tgstation.Server.Api/Models/Byond.cs b/src/Tgstation.Server.Api/Models/Byond.cs
index 17f5b1456c..9eebf4ddd1 100644
--- a/src/Tgstation.Server.Api/Models/Byond.cs
+++ b/src/Tgstation.Server.Api/Models/Byond.cs
@@ -14,5 +14,11 @@ namespace Tgstation.Server.Api.Models
///
[Permissions(ReadRight = ByondRights.ReadActive, WriteRight = ByondRights.ChangeVersion)]
public Version Version { get; set; }
+
+ ///
+ /// The being used to install a new
+ ///
+ [Permissions(DenyWrite = true)]
+ public Job InstallJob { get; set; }
}
}
diff --git a/src/Tgstation.Server.Api/Models/Repository.cs b/src/Tgstation.Server.Api/Models/Repository.cs
index 3cec95577c..981c77aa94 100644
--- a/src/Tgstation.Server.Api/Models/Repository.cs
+++ b/src/Tgstation.Server.Api/Models/Repository.cs
@@ -32,6 +32,12 @@ namespace Tgstation.Server.Api.Models
[Permissions(DenyWrite = true)]
public bool? IsGitHub { get; set; }
+ ///
+ /// The started by the if any
+ ///
+ [Permissions(DenyWrite = true)]
+ public Job ActiveJob { get; set; }
+
///
/// Do the equivalent of a git pull. Will attempt to merge unless is also specified
///
diff --git a/src/Tgstation.Server.Api/Rights/ByondRights.cs b/src/Tgstation.Server.Api/Rights/ByondRights.cs
index ae650b69af..ed08ed282a 100644
--- a/src/Tgstation.Server.Api/Rights/ByondRights.cs
+++ b/src/Tgstation.Server.Api/Rights/ByondRights.cs
@@ -17,8 +17,16 @@ namespace Tgstation.Server.Api.Rights
///
ReadActive = 1,
///
- /// User may change to any BYOND version
+ /// User may list all installed BYOND versions
///
- ChangeVersion = 4
+ ListInstalled = 2,
+ ///
+ /// User may change the active BYOND version
+ ///
+ ChangeVersion = 4,
+ ///
+ /// User may cancel version installations
+ ///
+ CancelInstall = 8
}
}
diff --git a/src/Tgstation.Server.Api/Rights/RepositoryRights.cs b/src/Tgstation.Server.Api/Rights/RepositoryRights.cs
index fa86528d28..5eb33c11b7 100644
--- a/src/Tgstation.Server.Api/Rights/RepositoryRights.cs
+++ b/src/Tgstation.Server.Api/Rights/RepositoryRights.cs
@@ -55,6 +55,14 @@ namespace Tgstation.Server.Api.Rights
///
/// User may delete the
///
- Delete = 2048
+ Delete = 2048,
+ ///
+ /// User may cancel clone operations
+ ///
+ CancelClone = 4096,
+ ///
+ /// User may cancel synchronize operations
+ ///
+ CancelSynchronize = 8192
}
}
diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs
index f6f0c44790..6e5079e76c 100644
--- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs
+++ b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs
@@ -5,6 +5,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Byond
@@ -20,6 +21,16 @@ namespace Tgstation.Server.Host.Components.Byond
///
public Version ActiveVersion { get; private set; }
+ ///
+ public IReadOnlyList InstalledVersions
+ {
+ get
+ {
+ lock (installedVersions)
+ return installedVersions.Select(x => Version.Parse(x.Key)).ToList();
+ }
+ }
+
///
/// The for the
///
@@ -40,6 +51,13 @@ namespace Tgstation.Server.Host.Components.Byond
///
readonly Dictionary installedVersions;
+ ///
+ /// The for the
+ ///
+ readonly SemaphoreSlim semaphore;
+
+ static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
+
///
/// Construct a
///
@@ -53,9 +71,11 @@ namespace Tgstation.Server.Host.Components.Byond
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
installedVersions = new Dictionary();
+ semaphore = new SemaphoreSlim(1);
}
- static string VersionKey(Version version) => new Version(version.Major, version.Minor).ToString();
+ ///
+ public void Dispose() => semaphore.Dispose();
async Task InstallVersion(Version version, CancellationToken cancellationToken)
{
@@ -95,8 +115,11 @@ namespace Tgstation.Server.Host.Components.Byond
public async Task ChangeVersion(Version version, CancellationToken cancellationToken)
{
await InstallVersion(version, cancellationToken).ConfigureAwait(false);
- await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
- ActiveVersion = version;
+ using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
+ {
+ await ioManager.WriteAllBytes(ActiveVersionFileName, Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
+ ActiveVersion = version;
+ }
}
///
diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs
index 70fd934081..767d15537e 100644
--- a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs
+++ b/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Hosting;
using System;
+using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
@@ -8,13 +9,18 @@ namespace Tgstation.Server.Host.Components.Byond
///
/// For managing the BYOND installation
///
- public interface IByondManager : IHostedService
+ public interface IByondManager : IHostedService, IDisposable
{
///
/// The currently active BYOND version
///
Version ActiveVersion { get; }
+ ///
+ /// The installed BYOND versions
+ ///
+ IReadOnlyList InstalledVersions { get; }
+
///
/// Change the active BYOND version
///
diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs
index 13fdf97475..11e516ee2a 100644
--- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs
+++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs
@@ -12,6 +12,11 @@ namespace Tgstation.Server.Host.Components.Repository
///
sealed class Repository : IRepository
{
+ ///
+ /// Indication of a GitHub repository
+ ///
+ public const string GitHubUrl = "://github.com/";
+
const string UnknownReference = "";
///
@@ -86,7 +91,7 @@ namespace Tgstation.Server.Host.Components.Repository
this.ioMananger = ioMananger ?? throw new ArgumentNullException(nameof(ioMananger));
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
- IsGitHubRepository = Origin.ToUpperInvariant().Contains("://GITHUB.COM/");
+ IsGitHubRepository = Origin.ToUpperInvariant().Contains(GitHubUrl.ToUpperInvariant());
if (IsGitHubRepository)
{
GetRepositoryOwnerName(Origin, out var owner, out var name);
diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs
index ecaf3c5d98..25cd954457 100644
--- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs
+++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs
@@ -53,28 +53,38 @@ namespace Tgstation.Server.Host.Components.Repository
{
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false))
- {
- await DeleteRepository(cancellationToken).ConfigureAwait(false);
-
- await Task.Factory.StartNew(() =>
+ try
+ {
+ await DeleteRepository(cancellationToken).ConfigureAwait(false);
+
+ await Task.Factory.StartNew(() =>
+ {
+ string path = null;
+ try
+ {
+ path = LibGit2Sharp.Repository.Clone(Repository.GenerateAuthUrl(url.ToString(), accessString), ioManager.ResolvePath("."), new CloneOptions
+ {
+ OnProgress = (a) => !cancellationToken.IsCancellationRequested,
+ OnTransferProgress = (a) => !cancellationToken.IsCancellationRequested,
+ RecurseSubmodules = true,
+ OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
+ RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested,
+ BranchName = initialBranch
+ });
+ }
+ catch (UserCancelledException) { }
+ cancellationToken.ThrowIfCancellationRequested();
+ }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
+ }
+ catch
{
- string path = null;
try
{
- path = LibGit2Sharp.Repository.Clone(Repository.GenerateAuthUrl(url.ToString(), accessString), ioManager.ResolvePath("."), new CloneOptions
- {
- OnProgress = (a) => !cancellationToken.IsCancellationRequested,
- OnTransferProgress = (a) => !cancellationToken.IsCancellationRequested,
- RecurseSubmodules = true,
- OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested,
- RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested,
- BranchName = initialBranch
- });
+ await ioManager.DeleteDirectory(".", default).ConfigureAwait(false);
}
- catch (UserCancelledException) { }
- cancellationToken.ThrowIfCancellationRequested();
- }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false);
- }
+ catch { }
+ throw;
+ }
return await LoadRepository(cancellationToken).ConfigureAwait(false);
}
diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs
new file mode 100644
index 0000000000..5d731e6060
--- /dev/null
+++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs
@@ -0,0 +1,108 @@
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Tgstation.Server.Api.Models;
+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
+{
+ ///
+ /// Controller for managing s
+ ///
+ [Route("/" + nameof(Byond))]
+ public sealed class ByondController : ModelController
+ {
+ ///
+ /// The for the
+ ///
+ readonly IInstanceManager instanceManager;
+
+ ///
+ /// The for the
+ ///
+ readonly IJobManager jobManager;
+
+ ///
+ /// The for the
+ ///
+ readonly ILogger logger;
+
+ ///
+ /// Construct a
+ ///
+ /// The for the
+ /// The for the
+ /// The value of
+ /// The value of
+ /// The value of
+ public ByondController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, true)
+ {
+ this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
+ this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ ///
+ [TgsAuthorize(ByondRights.ReadActive)]
+ public override Task Read(CancellationToken cancellationToken) => Task.FromResult((IActionResult)
+ Json(new Api.Models.Byond
+ {
+ Version = instanceManager.GetInstance(Instance).ByondManager.ActiveVersion
+ }));
+
+ ///
+ [TgsAuthorize(ByondRights.ListInstalled)]
+ public override Task List(CancellationToken cancellationToken) => Task.FromResult((IActionResult)
+ Json(instanceManager.GetInstance(Instance).ByondManager.InstalledVersions.Select(x => new Api.Models.Byond
+ {
+ Version = x
+ })));
+
+ ///
+ [TgsAuthorize(ByondRights.ChangeVersion)]
+ public override async Task Update([FromBody] Api.Models.Byond model, CancellationToken cancellationToken)
+ {
+ if (model == null)
+ return BadRequest(new { message = "Missing request model!" });
+
+ if(model.Version == null)
+ return BadRequest(new { message = "Missing version!" });
+
+ var byondManager = instanceManager.GetInstance(Instance).ByondManager;
+
+ //remove cruff fields
+ var installingVersion = new Version(model.Version.Major, model.Version.Major);
+
+ var result = new Api.Models.Byond();
+
+ if (byondManager.InstalledVersions.Any(x => x == model.Version))
+ {
+ logger.LogInformation("User ID {0} changing instance ID {1} BYOND version to {2}", AuthenticationContext.User.Id, Instance.Id, installingVersion);
+ await byondManager.ChangeVersion(model.Version, cancellationToken).ConfigureAwait(false);
+ }
+ else
+ {
+ logger.LogInformation("User ID {0} installing BYOND version to {2} on instance ID {1}", AuthenticationContext.User.Id, Instance.Id, installingVersion);
+ //run the install through the job manager
+ var job = new Models.Job
+ {
+ StartedBy = AuthenticationContext.User,
+ CancelRightsType = RightsType.Byond,
+ CancelRight = (int)ByondRights.CancelInstall,
+ Instance = Instance
+ };
+ await jobManager.RegisterOperation(job, (paramJob, serviceProvicer, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false);
+ result.InstallJob = job.ToApi();
+ }
+ result.Version = byondManager.ActiveVersion;
+ return Json(result);
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
index 0167d49112..323e073e00 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
@@ -55,15 +55,16 @@ namespace Tgstation.Server.Host.Controllers
if (instance.Watchdog.Running)
return StatusCode((int)HttpStatusCode.Gone);
- await jobManager.RegisterOperation(new Models.Job
+ var job = new Models.Job
{
Description = "Launch DreamDaemon",
CancelRight = (int)DreamDaemonRights.Shutdown,
CancelRightsType = RightsType.DreamDaemon,
Instance = Instance,
StartedBy = AuthenticationContext.User
- },
- async (job, serviceProvider, innerCt) =>
+ };
+ await jobManager.RegisterOperation(job,
+ async (paramJob, serviceProvider, innerCt) =>
{
var result = await instance.Watchdog.Launch(innerCt).ConfigureAwait(false);
if (result == null)
@@ -72,7 +73,7 @@ namespace Tgstation.Server.Host.Controllers
throw new Exception("Failed to launch watchdog!");
},
cancellationToken).ConfigureAwait(false);
- return Ok();
+ return Json(job);
}
///
diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
index 11a20fa482..aeac86e77c 100644
--- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
+++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs
@@ -1,5 +1,6 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
@@ -38,6 +39,11 @@ namespace Tgstation.Server.Host.Controllers
///
readonly IJobManager jobManager;
+ ///
+ /// The for the
+ ///
+ readonly ILogger logger;
+
///
/// Construct a
///
@@ -46,11 +52,13 @@ namespace Tgstation.Server.Host.Controllers
/// The value of
/// The value of
/// The value of
- public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, Octokit.IGitHubClient gitHubClient, IJobManager jobManager) : base(databaseContext, authenticationContextFactory, true)
+ /// The value of
+ public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, Octokit.IGitHubClient gitHubClient, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, true)
{
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
this.gitHubClient = gitHubClient ?? throw new ArgumentNullException(nameof(gitHubClient));
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
+ this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
static string GetAccessString(Api.Models.Internal.RepositorySettings repositorySettings) => repositorySettings.AccessUser != null ? String.Concat(repositorySettings.AccessUser, '@', repositorySettings.AccessToken) : null;
@@ -115,6 +123,13 @@ namespace Tgstation.Server.Host.Controllers
if (currentModel == default)
return StatusCode((int)HttpStatusCode.Gone);
+ //normalize github urls
+ const string BadGitHubUrl = "://www.github.com/";
+ var uiOrigin = model.Origin.ToUpperInvariant();
+ var uiBad = BadGitHubUrl.ToUpperInvariant();
+ var uiGitHub = Components.Repository.Repository.GitHubUrl.ToUpperInvariant();
+ if (uiOrigin.Contains(uiBad))
+ model.Origin = uiOrigin.Replace(uiBad, uiGitHub);
currentModel.AccessToken = model.AccessToken;
currentModel.AccessUser = model.AccessUser; //intentionally only these fields, user not allowed to change anything else atm
@@ -122,15 +137,37 @@ namespace Tgstation.Server.Host.Controllers
var origin = model.Origin;
var repoManager = instanceManager.GetInstance(Instance).RepositoryManager;
- using (var repo = await repoManager.CloneRepository(new Uri(origin), cloneBranch, GetAccessString(currentModel), cancellationToken).ConfigureAwait(false))
+
+ using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
if (repo == null)
//clone conflict
return Conflict();
+
+ var job = new Models.Job
+ {
+ Description = "Clone repository",
+ StartedBy = AuthenticationContext.User,
+ CancelRightsType = RightsType.Repository,
+ CancelRight = (int)RepositoryRights.CancelClone,
+ Instance = Instance
+ };
var api = currentModel.ToApi();
- await PopulateApi(api, repo, null, null, cancellationToken).ConfigureAwait(false);
- currentModel.LastOriginCommitSha = repo.Head;
- await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
+ await jobManager.RegisterOperation(job, async (paramJob, serviceProvider, ct) =>
+ {
+ using (var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, GetAccessString(currentModel), cancellationToken).ConfigureAwait(false))
+ {
+ if (repos == null)
+ throw new Exception("Filesystem conflict while cloning repository!");
+ await PopulateApi(api, repo, null, null, cancellationToken).ConfigureAwait(false);
+ }
+ }, cancellationToken).ConfigureAwait(false);
+
+ api.Origin = model.Origin;
+ api.Reference = model.Reference;
+ api.IsGitHub = model.Origin.ToUpperInvariant().Contains(uiGitHub);
+ api.ActiveJob = job.ToApi();
+
return Json(api);
}
}
@@ -154,7 +191,17 @@ namespace Tgstation.Server.Host.Controllers
await DatabaseContext.Save(cancellationToken).ConfigureAwait(false);
- await instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken).ConfigureAwait(false);
+ logger.LogInformation("Instance {0} repository delete initiated by user {1}", Instance.Id, AuthenticationContext.User.Id);
+
+ var job = new Models.Job
+ {
+ Description = "Delete repository",
+ StartedBy = AuthenticationContext.User,
+ Instance = Instance
+ };
+ var api = currentModel.ToApi();
+ await jobManager.RegisterOperation(job, (paramJob, serviceProvider, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false);
+ api.ActiveJob = job.ToApi();
return Ok();
}
@@ -341,7 +388,9 @@ namespace Tgstation.Server.Host.Controllers
{
Description = "Synchronize repository changes",
StartedBy = AuthenticationContext.User,
- Instance = Instance
+ Instance = Instance,
+ CancelRightsType = RightsType.Repository,
+ CancelRight = (int)RepositoryRights.CancelSynchronize,
};
await jobManager.RegisterOperation(job, async (paramJob, serviceProvider, ct) =>
{
@@ -349,6 +398,8 @@ namespace Tgstation.Server.Host.Controllers
if (repos != null)
await repos.Sychronize(accessString, ct).ConfigureAwait(false);
}, cancellationToken).ConfigureAwait(false);
+
+ api.ActiveJob = job.ToApi();
}
return Json(api);
diff --git a/src/Tgstation.Server.Host/Core/JobManager.cs b/src/Tgstation.Server.Host/Core/JobManager.cs
index 2f0dcc2a94..c99672c654 100644
--- a/src/Tgstation.Server.Host/Core/JobManager.cs
+++ b/src/Tgstation.Server.Host/Core/JobManager.cs
@@ -104,6 +104,19 @@ namespace Tgstation.Server.Host.Core
{
var databaseContext = scope.ServiceProvider.GetRequiredService();
job.StartedAt = DateTimeOffset.Now;
+ job.Instance = new Instance
+ {
+ Id = job.Instance.Id
+ };
+ databaseContext.Instances.Attach(job.Instance);
+ if (job.StartedBy != null)
+ {
+ job.StartedBy = new User
+ {
+ Id = job.StartedBy.Id
+ };
+ databaseContext.Users.Attach(job.StartedBy);
+ }
databaseContext.Jobs.Add(job);
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
var jobHandler = JobHandler.Create(x => RunJob(job, operation, x));