mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-08-24 21:46:52 +01:00
@@ -11,7 +11,6 @@ artifacts/
|
||||
*DS_Store
|
||||
*.sln.ide
|
||||
/TestResults
|
||||
/src/Tgstation.Server.Host/appsettings.Development.json
|
||||
/tests/DMAPI/travistester.lk
|
||||
/tests/DMAPI/travistester.int
|
||||
/tests/DMAPI/travistester.dmb
|
||||
|
||||
+54
-1
@@ -137,7 +137,7 @@ I DELETE "/InstanceUser/{UserId}" => OK
|
||||
|
||||
Users with the permission to modify @ref Tgstation.Server.Api.Models.Instance objects can also gain user editing rights for any Instance. See @ref api_instance
|
||||
|
||||
@section api_ver Getting the server version
|
||||
@section api_ver Version
|
||||
|
||||
The version of TGS running can be retireved with this request
|
||||
|
||||
@@ -196,6 +196,24 @@ Instances can be detached which will delete all meta knowledge of the instance (
|
||||
|
||||
DELETE "/Instance/{InstanceId}" => OK
|
||||
|
||||
@subsection api_job Jobs
|
||||
|
||||
Some requests return @ref Tgstation.Server.Api.Models.Job objects. These are long running tasks the server will perform asyncronously and can be polled for status.
|
||||
|
||||
To list all jobs in an Instance use the following request
|
||||
|
||||
I GET "/Job" => Array of @ref Tgstation.Server.Api.Models.Job
|
||||
|
||||
Note that the response for this request will only have the @ref Tgstation.Server.Api.Models.Job.Id field populated
|
||||
|
||||
To get full details of a job use the following request:
|
||||
|
||||
I GET "/Job/{JobId}" => @ref Tgstation.Server.Api.Models.Job
|
||||
|
||||
To cancel a running job (If the job can be cancelled and you have sufficient rights) use the following request:
|
||||
|
||||
I DELETE "/Job/{JobId}" => OK
|
||||
|
||||
@subsection api_chat Chat Bots
|
||||
|
||||
Each chat bot is represented by a @ref Tgstation.Server.Api.Models.ChatSettings object
|
||||
@@ -219,4 +237,39 @@ I GET "/Chat/{ChatSettingsId}" => @ref Tgstation.Server.Api.Models.ChatSettings
|
||||
|
||||
Also note that if the @ref Tgstation.Server.Api.Models.ChatSettings.Channels is present in a POST request, the list will fully replace any active channels
|
||||
|
||||
@subsection api_byond Byond Version Management
|
||||
|
||||
To get the Byond version used for new compilations use the following request:
|
||||
|
||||
I GET "/Byond" => @ref Tgstation.Server.Api.Models.Byond
|
||||
|
||||
To set the active Byond version:
|
||||
|
||||
I POST "/Byond" @ref Tgstation.Server.Api.Models.Byond => @ref Tgstation.Server.Api.Models.Byond
|
||||
|
||||
To list all installed Byond versions use the following request:
|
||||
|
||||
I GET "/Byond/List" => Array of @ref Tgstation.Server.Api.Models.Byond
|
||||
|
||||
@subsection Git Repository Management
|
||||
|
||||
To read the current repository state use the following request:
|
||||
|
||||
I GET "/Repository" => @ref Tgstation.Server.Api.Models.Repository
|
||||
|
||||
To clone the repository if it doesn't yet exist use the following request:
|
||||
|
||||
I PUT "/Repository" => @ref Tgstation.Server.Api.Models.Repository
|
||||
|
||||
The clone job will be represented by the @ref Tgstation.Server.Api.Models.Repository.ActiveJob field. Specify the @ref Tgstation.Server.Api.Models.Origin URL. Optionally specify the initial @ref Tgstation.Server.Api.Models.Repository.Reference as a git tag or branch. Be sure to specify the authentication fields if necessary to access your repository
|
||||
|
||||
To delete an existing repository make the following request:
|
||||
|
||||
I DELETE "/Repository" => OK
|
||||
|
||||
Modifications to the repository are done with the following request:
|
||||
|
||||
I POST "/Repository" => @ref Tgstation.Server.Api.Models.Repository => @ref Tgstation.Server.Api.Models.Repository
|
||||
|
||||
Each update creates a job specified in the @ref Tgstation.Server.Api.Models.Repository.ActiveJob field jobs will be queued in succession.
|
||||
*/
|
||||
|
||||
+2
-2
@@ -47,14 +47,14 @@
|
||||
#define TGS_EVENT_PORT_SWAP -2 //before a port change is about to happen, extra parameter is new port
|
||||
#define TGS_EVENT_REBOOT_MODE_CHANGE -1 //before a reboot mode change, extras parameters are the current and new reboot mode enums
|
||||
|
||||
//TODO
|
||||
|
||||
//OTHER ENUMS
|
||||
|
||||
#define TGS_REBOOT_MODE_NORMAL 0
|
||||
#define TGS_REBOOT_MODE_SHUTDOWN 1
|
||||
#define TGS_REBOOT_MODE_RESTART 2
|
||||
|
||||
//TODO
|
||||
|
||||
//REQUIRED HOOKS
|
||||
|
||||
//Call this somewhere in /world/New() that is always run
|
||||
|
||||
@@ -33,6 +33,10 @@
|
||||
switch(major)
|
||||
if(2)
|
||||
return /datum/tgs_api/v3210
|
||||
if(4)
|
||||
switch(major)
|
||||
if(0)
|
||||
return /datum/tgs_api/v4
|
||||
|
||||
if(super != null && major != null && minor != null && patch != null && tgs_version > TgsMaximumAPIVersion())
|
||||
TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.")
|
||||
|
||||
+20
-17
@@ -11,7 +11,7 @@
|
||||
|
||||
#define TGS4_COMM_ONLINE "tgs_on"
|
||||
#define TGS4_COMM_IDENTIFY "tgs_ident"
|
||||
#define TGS4_COMM_VALIDATE "tgs_vali"
|
||||
#define TGS4_COMM_VALIDATE "tgs_validate"
|
||||
#define TGS4_COMM_SERVER_PRIMED "tgs_prime"
|
||||
#define TGS4_COMM_WORLD_REBOOT "tgs_reboot"
|
||||
#define TGS4_COMM_END_PROCESS "tgs_kill"
|
||||
@@ -58,39 +58,42 @@
|
||||
TGS_ERROR_LOG("Failed to decode info json: [json_file]")
|
||||
return
|
||||
|
||||
access_identifier = cached_json["access_identifier"]
|
||||
instance_name = text2num(cached_json["instance_name"])
|
||||
host_path = cached_json["host_path"]
|
||||
if(cached_json["api_validate_only"])
|
||||
access_identifier = cached_json["accessIdentifier"]
|
||||
instance_name = text2num(cached_json["instanceName"])
|
||||
host_path = cached_json["hostPath"]
|
||||
if(cached_json["apiValidateOnly"])
|
||||
TGS_INFO_LOG("Validating API and exiting...")
|
||||
Export(TGS4_COMM_VALIDATE)
|
||||
del(world)
|
||||
|
||||
chat_channels_json_path = cached_json["chat_channels_json"]
|
||||
chat_commands_json_path = cached_json["chat_commands_json"]
|
||||
chat_channels_json_path = cached_json["chatChannelsJson"]
|
||||
chat_commands_json_path = cached_json["chatCommandsJson"]
|
||||
src.event_handler = event_handler
|
||||
instance_name = cached_json["instance_name"]
|
||||
instance_name = cached_json["instanceName"]
|
||||
|
||||
cached_test_merges = list()
|
||||
var/json = cached_json["test_merges"]
|
||||
var/json = cached_json["testMerges"]
|
||||
for(var/I in json)
|
||||
var/datum/tgs_revision_information/test_merge/tm = new
|
||||
tm.number = text2num(I)
|
||||
var/list/entry = json[I]
|
||||
tm.pull_request_commit = entry["pr_commit"]
|
||||
tm.pull_request_commit = entry["prCommit"]
|
||||
tm.author = entry["author"]
|
||||
tm.title = entry["title"]
|
||||
tm.commit = entry["commit"]
|
||||
tm.origin_commit = entry["origin_commit"]
|
||||
tm.time_merged = text2num(entry["time_merged"])
|
||||
tm.origin_commit = entry["originCommit"]
|
||||
tm.time_merged = text2num(entry["timeMerged"])
|
||||
tm.comment = entry["comment"]
|
||||
tm.url = entry["url"]
|
||||
|
||||
cached_revision = new
|
||||
cached_revision.commit = cached_json["commit"]
|
||||
cached_revision.origin_commit = cached_json["origin_commit"]
|
||||
cached_revision.origin_commit = cached_json["originCommit"]
|
||||
|
||||
ListCustomCommands()
|
||||
|
||||
return TRUE
|
||||
|
||||
/datum/tgs_api/v4/OnInitializationComplete()
|
||||
Export(TGS4_COMM_SERVER_PRIMED)
|
||||
var/tgs4_secret_sleep_offline_sauce = 24051994
|
||||
@@ -208,10 +211,10 @@
|
||||
/datum/tgs_api/v4/proc/DecodeChannel(channel_json)
|
||||
var/datum/tgs_chat_channel/channel = new
|
||||
channel.id = channel_json["id"]
|
||||
channel.friendly_name = channel_json["friendly_name"]
|
||||
channel.connection_name = channel_json["connection_name"]
|
||||
channel.is_admin_channel = channel_json["is_admin_channel"]
|
||||
channel.is_private_channel = channel_json["is_private_channel"] || FALSE
|
||||
channel.friendly_name = channel_json["friendlyName"]
|
||||
channel.connection_name = channel_json["connectionName"]
|
||||
channel.is_admin_channel = channel_json["isAdminChannel"]
|
||||
channel.is_private_channel = channel_json["isPrivateChannel"] || FALSE
|
||||
return channel
|
||||
|
||||
#undef TGS4_TOPIC_COMMAND
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
var/datum/tgs_chat_user/u = new
|
||||
u.id = user["id"]
|
||||
u.friendly_name = user["friendly_name"]
|
||||
u.friendly_name = user["friendlyName"]
|
||||
u.mention = user["mention"]
|
||||
u.channel = DecodeChannel(user["channel"])
|
||||
|
||||
|
||||
@@ -1,33 +1,41 @@
|
||||
namespace Tgstation.Server.Api.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Status of the <see cref="DreamMaker"/> for an <see cref="Instance"/>
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Status of the <see cref="DreamMaker"/> for an <see cref="Instance"/>
|
||||
/// </summary>
|
||||
#pragma warning disable CA1717 // Only FlagsAttribute enums should have plural names
|
||||
public enum CompilerStatus
|
||||
#pragma warning restore CA1717 // Only FlagsAttribute enums should have plural names
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="DreamMaker"/> is idle
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// The <see cref="DreamMaker"/> is idle
|
||||
/// </summary>
|
||||
Idle,
|
||||
/// <summary>
|
||||
/// The <see cref="Repository"/> is being copied
|
||||
/// </summary>
|
||||
Copying,
|
||||
/// <summary>
|
||||
/// The <see cref="Repository"/> is being copied
|
||||
/// </summary>
|
||||
Copying,
|
||||
/// <summary>
|
||||
/// Pre-compile scripts are running
|
||||
/// </summary>
|
||||
PreCompile,
|
||||
/// <summary>
|
||||
/// The .dme is having it's server side modifications applied
|
||||
/// </summary>
|
||||
Modifying,
|
||||
/// <summary>
|
||||
/// DreamMaker is running
|
||||
/// </summary>
|
||||
Compiling,
|
||||
/// <summary>
|
||||
/// DreamMaker is running
|
||||
/// </summary>
|
||||
Compiling,
|
||||
/// <summary>
|
||||
/// The DMAPI is being verified
|
||||
/// </summary>
|
||||
Verifying,
|
||||
/// <summary>
|
||||
/// Post-compile scripts are running
|
||||
/// </summary>
|
||||
PostCompile,
|
||||
/// <summary>
|
||||
/// The compile results are being duplicated
|
||||
/// </summary>
|
||||
Duplicating,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
/// </summary>
|
||||
Safe,
|
||||
/// <summary>
|
||||
/// Server will not be able to run shell commands or access anything but temporary files
|
||||
/// Server will not be able to run shell commands or access anything but temporary files. Currently unsupported!
|
||||
/// </summary>
|
||||
Ultrasafe
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
var versionToUse = requiredVersion ?? ActiveVersion;
|
||||
if (versionToUse == null)
|
||||
throw new InvalidOperationException("No BYOND versions installed!");
|
||||
await InstallVersion(requiredVersion, cancellationToken).ConfigureAwait(false);
|
||||
await InstallVersion(versionToUse, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var versionKey = VersionKey(versionToUse);
|
||||
|
||||
|
||||
@@ -127,23 +127,22 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
p.StartInfo.WorkingDirectory = rbdx;
|
||||
p.EnableRaisingEvents = true;
|
||||
var tcs = new TaskCompletionSource<object>();
|
||||
p.Exited += (a, b) => tcs.SetResult(null);
|
||||
p.Exited += (a, b) => tcs.TrySetResult(null);
|
||||
try
|
||||
{
|
||||
p.Start();
|
||||
using (cancellationToken.Register(() =>
|
||||
{
|
||||
p.Kill();
|
||||
tcs.SetCanceled();
|
||||
}))
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled()))
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
try
|
||||
{
|
||||
p.Kill();
|
||||
p.WaitForExit();
|
||||
if (!p.HasExited)
|
||||
{
|
||||
p.Kill();
|
||||
p.WaitForExit();
|
||||
}
|
||||
}
|
||||
catch (InvalidOperationException) { }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
@@ -58,7 +59,13 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
public async Task SetChannels(IEnumerable<Channel> channels, CancellationToken cancellationToken)
|
||||
{
|
||||
using (await SemaphoreSlimContext.Lock(channelsSemaphore, cancellationToken).ConfigureAwait(false))
|
||||
await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(channels)), cancellationToken).ConfigureAwait(false);
|
||||
await ioManager.WriteAllBytes(channelsPath, Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(channels, Formatting.Indented, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new CamelCaseNamingStrategy()
|
||||
}
|
||||
})), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -34,15 +35,19 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
/// </summary>
|
||||
readonly IIOManager ioManager;
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationTokenSource"/> for <see cref="cleanupTask"/>
|
||||
/// The <see cref="ILogger"/> for the <see cref="DmbFactory"/>
|
||||
/// </summary>
|
||||
readonly CancellationTokenSource cleanupCts;
|
||||
readonly ILogger<DmbFactory> logger;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Api.Models.Instance"/> for the <see cref="DmbFactory"/>
|
||||
/// </summary>
|
||||
readonly Api.Models.Instance instance;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationTokenSource"/> for <see cref="cleanupTask"/>
|
||||
/// </summary>
|
||||
readonly CancellationTokenSource cleanupCts;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Task"/> representing calls to <see cref="CleanJob(CompileJob)"/>
|
||||
/// </summary>
|
||||
@@ -50,7 +55,7 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
/// <summary>
|
||||
/// <see cref="TaskCompletionSource{TResult}"/> resulting in the latest <see cref="DmbProvider"/> yet to exist
|
||||
/// </summary>
|
||||
TaskCompletionSource<IDmbProvider> newerDmbTcs;
|
||||
TaskCompletionSource<object> newerDmbTcs;
|
||||
/// <summary>
|
||||
/// The latest <see cref="DmbProvider"/>
|
||||
/// </summary>
|
||||
@@ -63,20 +68,23 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
/// </summary>
|
||||
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
|
||||
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
/// <param name="instance">The value of <see cref="instance"/></param>
|
||||
public DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, Api.Models.Instance instance)
|
||||
public DmbFactory(IDatabaseContextFactory databaseContextFactory, IIOManager ioManager, ILogger<DmbFactory> logger, Api.Models.Instance instance)
|
||||
{
|
||||
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
|
||||
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
|
||||
|
||||
cleanupTask = Task.CompletedTask;
|
||||
newerDmbTcs = new TaskCompletionSource<object>();
|
||||
cleanupCts = new CancellationTokenSource();
|
||||
jobLockCounts = new Dictionary<long, int>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => cleanupCts.Dispose();
|
||||
public void Dispose() => cleanupCts.Dispose(); //we don't dispose nextDmbProvider here, since it might be the only thing we have
|
||||
|
||||
/// <summary>
|
||||
/// Delete the <see cref="Api.Models.Internal.CompileJob.DirectoryName"/> of <paramref name="job"/>
|
||||
@@ -84,84 +92,74 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
/// <param name="job">The <see cref="CompileJob"/> to clean</param>
|
||||
void CleanJob(CompileJob job)
|
||||
{
|
||||
logger.LogTrace("Cleaning compile job {0} => {1}", job.Id, job.DirectoryName);
|
||||
async Task HandleCleanup()
|
||||
{
|
||||
var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token);
|
||||
Task otherTask;
|
||||
lock (this)
|
||||
otherTask = cleanupTask;
|
||||
//lock (this) //already locked below
|
||||
otherTask = cleanupTask;
|
||||
await Task.WhenAll(otherTask, deleteJob).ConfigureAwait(false);
|
||||
}
|
||||
lock (this)
|
||||
{
|
||||
var currentVal = jobLockCounts[job.Id];
|
||||
if (--jobLockCounts[job.Id] == 0)
|
||||
if (!jobLockCounts.TryGetValue(job.Id, out var currentVal) || --jobLockCounts[job.Id] == 0)
|
||||
{
|
||||
jobLockCounts.Remove(job.Id);
|
||||
cleanupTask = HandleCleanup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken) => LoadCompileJob(job, true, cancellationToken);
|
||||
|
||||
async Task LoadCompileJob(CompileJob job, bool setAsStagedInDb, CancellationToken cancellationToken)
|
||||
public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
if (job == null)
|
||||
throw new ArgumentNullException(nameof(job));
|
||||
if (job.DMApiValidated != true || job.Job.Cancelled.Value || job.Job.ExceptionDetails != null || job.Job.StoppedAt == null)
|
||||
if (job.DMApiValidated != true || job.Job.Cancelled == true || job.Job.ExceptionDetails != null)
|
||||
throw new InvalidOperationException("Cannot load incomplete compile job!");
|
||||
if (setAsStagedInDb)
|
||||
await databaseContextFactory.UseContext(async db =>
|
||||
{
|
||||
var ddsettings = new DreamDaemonSettings
|
||||
{
|
||||
InstanceId = instance.Id
|
||||
};
|
||||
db.DreamDaemonSettings.Attach(ddsettings);
|
||||
ddsettings.StagedCompileJob = job;
|
||||
await db.Save(cancellationToken).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
var newProvider = await FromCompileJob(job, cancellationToken).ConfigureAwait(false);
|
||||
if (newProvider == null)
|
||||
return;
|
||||
lock (this)
|
||||
{
|
||||
var oldDmbProvider = nextDmbProvider;
|
||||
if (oldDmbProvider != null && oldDmbProvider.CompileJob.Job.StoppedAt < oldDmbProvider.CompileJob.Job.StoppedAt)
|
||||
throw new InvalidOperationException("Loaded compile job older than current job!");
|
||||
nextDmbProvider = FromCompileJob(job);
|
||||
nextDmbProvider?.Dispose();
|
||||
nextDmbProvider = newProvider;
|
||||
newerDmbTcs.SetResult(nextDmbProvider);
|
||||
newerDmbTcs = new TaskCompletionSource<IDmbProvider>();
|
||||
newerDmbTcs = new TaskCompletionSource<object>();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IDmbProvider> LockNextDmb(CancellationToken cancellationToken)
|
||||
{
|
||||
Task<IDmbProvider> task;
|
||||
lock (this)
|
||||
if (nextDmbProvider != null)
|
||||
return nextDmbProvider;
|
||||
else
|
||||
if (nextDmbProvider == null)
|
||||
{
|
||||
Task task;
|
||||
lock (this)
|
||||
task = newerDmbTcs.Task;
|
||||
|
||||
var result = await task.ConfigureAwait(false);
|
||||
//so there's currently a race condition in DreamMakerController where the setting of CompileJob.RevisionInformation and thus IDmbProvider.RevisionInformation can be delayed to after this if someone tries to start the server instantly after compiling
|
||||
//This is a terrible terrible hack to get around that
|
||||
//I'm sorry future me, I can't think of any other way to fix this other than giving DreamMaker an IDatabaseContext or having the controller load the CompileJob
|
||||
await Task.Delay(new TimeSpan(0, 0, 10), cancellationToken).ConfigureAwait(false);
|
||||
return result;
|
||||
await task.ConfigureAwait(false);
|
||||
}
|
||||
lock (this)
|
||||
{
|
||||
++jobLockCounts[nextDmbProvider.CompileJob.Id];
|
||||
return nextDmbProvider;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
|
||||
{
|
||||
//where complete clause not necessary, only successful COMPILEjobs get in the db
|
||||
var cj = await db.Instances.Where(x => x.Id == instance.Id).SelectMany(x => x.RevisionInformations).SelectMany(x => x.CompileJobs).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
var cj = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && !x.Job.Cancelled.Value && x.Job.ExceptionDetails == null && x.Job.StoppedAt != null)
|
||||
.Include(x => x.Job).ThenInclude(x => x.StartedBy)
|
||||
.Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
|
||||
.Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
|
||||
.OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (cj == default(CompileJob))
|
||||
return;
|
||||
var directoriesTask = ioManager.GetDirectories(".", cancellationToken);
|
||||
var compileJobTask = LoadCompileJob(cj, false, cancellationToken);
|
||||
//delete all other compile jobs
|
||||
var directories = await directoriesTask.ConfigureAwait(false);
|
||||
await Task.WhenAll(directories.Where(x => x != cj.Job.ToString()).Select(x => ioManager.DeleteDirectory(x, cancellationToken))).ConfigureAwait(false);
|
||||
await compileJobTask.ConfigureAwait(false);
|
||||
await LoadCompileJob(cj, cancellationToken).ConfigureAwait(false);
|
||||
//we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
|
||||
});
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -172,15 +170,86 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IDmbProvider FromCompileJob(CompileJob compileJob)
|
||||
public async Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
|
||||
{
|
||||
lock (this)
|
||||
logger.LogTrace("Loading compile job {0}...", compileJob.Id);
|
||||
var providerSubmitted = false;
|
||||
var newProvider = new DmbProvider(compileJob, ioManager, () =>
|
||||
{
|
||||
if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
|
||||
jobLockCounts.Add(compileJob.Id, 1);
|
||||
else
|
||||
jobLockCounts[compileJob.Id] = ++value;
|
||||
return new DmbProvider(compileJob, ioManager, () => CleanJob(compileJob));
|
||||
if (providerSubmitted)
|
||||
CleanJob(compileJob);
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var primaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
|
||||
var secondaryCheckTask = ioManager.FileExists(ioManager.ConcatPath(newProvider.PrimaryDirectory, newProvider.DmbName), cancellationToken);
|
||||
|
||||
if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
|
||||
{
|
||||
logger.LogWarning("Error loading compile job, .dmb missing!");
|
||||
return null; //omae wa mou shinderu
|
||||
}
|
||||
|
||||
lock (this)
|
||||
{
|
||||
if (!jobLockCounts.TryGetValue(compileJob.Id, out int value))
|
||||
jobLockCounts.Add(compileJob.Id, 1);
|
||||
else
|
||||
jobLockCounts[compileJob.Id] = ++value;
|
||||
providerSubmitted = true;
|
||||
return newProvider;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (!providerSubmitted)
|
||||
newProvider.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken)
|
||||
{
|
||||
List<long> jobIdsToSkip;
|
||||
//don't clean locked directories
|
||||
lock (this)
|
||||
jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
|
||||
|
||||
List<string> jobUidsToNotErase = null;
|
||||
|
||||
//find the uids of locked directories
|
||||
await databaseContextFactory.UseContext(async db =>
|
||||
{
|
||||
jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id) && x.DirectoryName.HasValue).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
//add the other exemption
|
||||
if (exceptThisOne != null)
|
||||
jobUidsToNotErase.Add(exceptThisOne.DirectoryName.Value.ToString().ToUpperInvariant());
|
||||
|
||||
//cleanup
|
||||
var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
|
||||
int deleting = 0;
|
||||
var tasks = directories.Select(async x =>
|
||||
{
|
||||
var nameOnly = ioManager.GetFileName(x);
|
||||
if (jobUidsToNotErase.Contains(nameOnly.ToUpperInvariant()))
|
||||
return;
|
||||
try
|
||||
{
|
||||
++deleting;
|
||||
await ioManager.DeleteDirectory(x, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Error deleting directory {0}! Exception: {1}", x, e);
|
||||
}
|
||||
}).ToList();
|
||||
if (deleting > 0)
|
||||
{
|
||||
logger.LogDebug("Cleaning {0} unused game folders...", deleting);
|
||||
await Task.WhenAll().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,15 +46,10 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose));
|
||||
}
|
||||
|
||||
~DmbProvider() => Dispose();
|
||||
/// <inheritdoc />
|
||||
public void Dispose() => onDispose?.Invoke();
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
onDispose?.Invoke();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
public void KeepAlive() => onDispose = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
/// <summary>
|
||||
/// Extension for .dmes
|
||||
/// </summary>
|
||||
const string DmeExtension = ".dme";
|
||||
const string DmeExtension = "dme";
|
||||
|
||||
/// <inheritdoc />
|
||||
public CompilerStatus Status { get; private set; }
|
||||
@@ -100,34 +100,45 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
/// Run a quick DD instance to test the DMAPI is installed on the target code
|
||||
/// </summary>
|
||||
/// <param name="timeout">The timeout in seconds for validation</param>
|
||||
/// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to use to validate the API</param>
|
||||
/// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
|
||||
/// <param name="byondLock">The current <see cref="IByondExecutableLock"/></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(uint timeout, Models.CompileJob job, IByondExecutableLock byondLock, CancellationToken cancellationToken)
|
||||
async Task<bool> VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogTrace("Verifying DMAPI...");
|
||||
var launchParameters = new DreamDaemonLaunchParameters
|
||||
{
|
||||
AllowWebClient = false,
|
||||
PrimaryPort = 0, //pick any port
|
||||
SecurityLevel = DreamDaemonSecurity.Safe, //all it needs to read the file and exit
|
||||
SecurityLevel = securityLevel, //all it needs to read the file and exit
|
||||
StartupTimeout = timeout
|
||||
};
|
||||
|
||||
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
|
||||
var provider = new TemporaryDmbProvider(ioManager.ResolvePath(ioManager.GetDirectoryName(dirA)), ioManager.ResolvePath(ioManager.ConcatPath(dirA, String.Concat(job.DmeName, DmbExtension))));
|
||||
var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job);
|
||||
|
||||
var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout);
|
||||
using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
var timeoutTask = Task.Delay(timeoutAt - DateTimeOffset.Now, cancellationToken);
|
||||
var now = DateTimeOffset.Now;
|
||||
if (now < timeoutAt)
|
||||
{
|
||||
var timeoutTask = Task.Delay(timeoutAt - DateTimeOffset.Now, cancellationToken);
|
||||
|
||||
await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
if (!controller.Lifetime.IsCompleted)
|
||||
{
|
||||
logger.LogDebug("API validation timed out!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return controller.ApiValidated;
|
||||
var validated = controller.ApiValidated;
|
||||
logger.LogTrace("API valid: {0}", validated);
|
||||
return validated;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,10 +154,11 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
using (var dm = new Process())
|
||||
{
|
||||
dm.StartInfo.FileName = dreamMakerPath;
|
||||
dm.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "-clean {0}{1}", job.DmeName, DmeExtension);
|
||||
dm.StartInfo.Arguments = String.Format(CultureInfo.InvariantCulture, "-clean {0}.{1}", job.DmeName, DmeExtension);
|
||||
dm.StartInfo.WorkingDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
|
||||
dm.StartInfo.RedirectStandardOutput = true;
|
||||
dm.StartInfo.RedirectStandardError = true;
|
||||
dm.StartInfo.UseShellExecute = false;
|
||||
var OutputList = new StringBuilder();
|
||||
var eventHandler = new DataReceivedEventHandler(
|
||||
delegate (object sender, DataReceivedEventArgs e)
|
||||
@@ -160,12 +172,15 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
|
||||
dm.EnableRaisingEvents = true;
|
||||
var dmTcs = new TaskCompletionSource<object>();
|
||||
dm.Exited += (a, b) => dmTcs.SetResult(null);
|
||||
dm.Exited += (a, b) => dmTcs.TrySetResult(null);
|
||||
|
||||
logger.LogTrace("Running DreamMaker...");
|
||||
dm.Start();
|
||||
dm.BeginOutputReadLine();
|
||||
dm.BeginErrorReadLine();
|
||||
try
|
||||
{
|
||||
using (cancellationToken.Register(() => dmTcs.SetCanceled()))
|
||||
using (cancellationToken.Register(() => dmTcs.TrySetCanceled()))
|
||||
await dmTcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
@@ -177,8 +192,10 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
}
|
||||
}
|
||||
|
||||
job.Output = OutputList.ToString();
|
||||
job.ExitCode = dm.ExitCode;
|
||||
logger.LogDebug("DreamMaker exit code: {0}", job.ExitCode);
|
||||
job.Output = OutputList.ToString();
|
||||
logger.LogTrace("DreamMaker output: {0}", job.Output);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +208,7 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
async Task ModifyDme(Models.CompileJob job, CancellationToken cancellationToken)
|
||||
{
|
||||
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
|
||||
var dmePath = ioManager.ConcatPath(dirA, String.Concat(job.DmeName, DmeExtension));
|
||||
var dmePath = ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension));
|
||||
var dmeReadTask = ioManager.ReadAllBytes(dmePath, cancellationToken);
|
||||
|
||||
var dmeModificationsTask = configuration.CopyDMFilesTo(dmePath, ioManager.ResolvePath(dirA), cancellationToken);
|
||||
@@ -202,7 +219,18 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
var dmeModifications = await dmeModificationsTask.ConfigureAwait(false);
|
||||
|
||||
if (dmeModifications == null || dmeModifications.TotalDmeOverwrite)
|
||||
{
|
||||
if (dmeModifications != null)
|
||||
logger.LogDebug(".dme replacement configured!");
|
||||
else
|
||||
logger.LogTrace("No .dme modifications required.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (dmeModifications.HeadIncludeLine != null)
|
||||
logger.LogDebug("Head .dme include line: {0}", dmeModifications.HeadIncludeLine);
|
||||
if (dmeModifications.TailIncludeLine != null)
|
||||
logger.LogDebug("Tail .dme include line: {0}", dmeModifications.TailIncludeLine);
|
||||
|
||||
var dmeLines = new List<string>(dme.Split(new[] { Environment.NewLine }, StringSplitOptions.None));
|
||||
for (var I = 0; I < dmeLines.Count; ++I)
|
||||
@@ -225,51 +253,86 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Models.CompileJob> Compile(string projectName, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken)
|
||||
public async Task<Models.CompileJob> Compile(string projectName, DreamDaemonSecurity securityLevel, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken)
|
||||
{
|
||||
if (repository == null)
|
||||
throw new ArgumentNullException(nameof(repository));
|
||||
|
||||
if (securityLevel == DreamDaemonSecurity.Ultrasafe)
|
||||
throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, "Cannot compile with ultrasafe security!");
|
||||
|
||||
logger.LogTrace("Begin Compile");
|
||||
await eventConsumer.HandleEvent(EventType.CompileStart, new List<string>{ repository.Origin }, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var job = new Models.CompileJob
|
||||
{
|
||||
DirectoryName = Guid.NewGuid(),
|
||||
DmeName = projectName
|
||||
};
|
||||
|
||||
logger.LogTrace("Compile output GUID: {0}", job.DirectoryName);
|
||||
|
||||
lock (this)
|
||||
{
|
||||
if(Status != CompilerStatus.Idle)
|
||||
{
|
||||
job.Output = "There is already a compile in progress!";
|
||||
logger.LogInformation(job.Output);
|
||||
return job;
|
||||
}
|
||||
|
||||
Status = CompilerStatus.Copying;
|
||||
}
|
||||
try
|
||||
{
|
||||
Status = CompilerStatus.Copying;
|
||||
var job = new Models.CompileJob
|
||||
{
|
||||
DirectoryName = Guid.NewGuid(),
|
||||
DmeName = projectName
|
||||
};
|
||||
await ioManager.CreateDirectory(job.DirectoryName.ToString(), cancellationToken).ConfigureAwait(false);
|
||||
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
|
||||
var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName);
|
||||
|
||||
async Task CleanupFailedCompile()
|
||||
{
|
||||
logger.LogTrace("Cleaning compile directory...");
|
||||
Status = CompilerStatus.Cleanup;
|
||||
try
|
||||
{
|
||||
await ioManager.DeleteDirectory(job.DirectoryName.ToString(), CancellationToken.None).ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogWarning("Error cleaning up compile directory {0}! Exception: {1}", ioManager.ResolvePath(job.DirectoryName.ToString()), e);
|
||||
}
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
logger.LogTrace("Copying repository to game directory...");
|
||||
//copy the repository
|
||||
var fullDirA = ioManager.ResolvePath(dirA);
|
||||
var repoOrigin = repository.Origin;
|
||||
using (repository)
|
||||
await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Status = CompilerStatus.PreCompile;
|
||||
|
||||
await eventConsumer.HandleEvent(EventType.CompileStart, new List<string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)), repoOrigin }, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Status = CompilerStatus.Modifying;
|
||||
|
||||
if (job.DmeName == null)
|
||||
{
|
||||
job.DmeName = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault();
|
||||
if (job.DmeName == default)
|
||||
logger.LogTrace("Searching for available .dmes...");
|
||||
var path = (await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false)).FirstOrDefault();
|
||||
if (path == default)
|
||||
{
|
||||
job.Output = "Unable to find any .dme!";
|
||||
logger.LogWarning(job.Output);
|
||||
return job;
|
||||
}
|
||||
var dmeWithExtension = ioManager.GetFileName(path);
|
||||
job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1);
|
||||
}
|
||||
|
||||
logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName);
|
||||
|
||||
await ModifyDme(job, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Status = CompilerStatus.Compiling;
|
||||
@@ -284,7 +347,7 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
|
||||
Status = CompilerStatus.Verifying;
|
||||
|
||||
ddVerified = job.ExitCode == 0 && await VerifyApi(apiValidateTimeout, job, byondLock, cancellationToken).ConfigureAwait(false);
|
||||
ddVerified = job.ExitCode == 0 && await VerifyApi(apiValidateTimeout, securityLevel, job, byondLock, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!ddVerified)
|
||||
@@ -297,11 +360,17 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
{
|
||||
job.DMApiValidated = true;
|
||||
|
||||
logger.LogTrace("Running post compile event...");
|
||||
Status = CompilerStatus.PostCompile;
|
||||
await eventConsumer.HandleEvent(EventType.CompileComplete, new List<string> { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
logger.LogTrace("Duplicating compiled game...");
|
||||
Status = CompilerStatus.Duplicating;
|
||||
|
||||
//duplicate the dmb et al
|
||||
await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
logger.LogTrace("Applying static game file symlinks...");
|
||||
Status = CompilerStatus.Symlinking;
|
||||
|
||||
//symlink in the static data
|
||||
@@ -309,9 +378,9 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
|
||||
|
||||
await Task.WhenAll(symATask, symBTask).ConfigureAwait(false);
|
||||
await eventConsumer.HandleEvent(EventType.CompileComplete, null, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
logger.LogDebug("Compile complete!");
|
||||
}
|
||||
await compileJobConsumer.LoadCompileJob(job, cancellationToken).ConfigureAwait(false);
|
||||
return job;
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
/// <summary>
|
||||
/// Sink for <see cref="CompileJob"/>s
|
||||
/// </summary>
|
||||
interface ICompileJobConsumer : IHostedService, IDisposable
|
||||
public interface ICompileJobConsumer : IHostedService, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Load a new <paramref name="job"/> into the <see cref="ICompileJobConsumer"/>
|
||||
|
||||
@@ -27,7 +27,17 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
/// Gets a <see cref="IDmbProvider"/> for a given <see cref="CompileJob"/>
|
||||
/// </summary>
|
||||
/// <param name="compileJob">The <see cref="CompileJob"/> to make the <see cref="IDmbProvider"/> for</param>
|
||||
/// <returns>A new <see cref="IDmbProvider"/></returns>
|
||||
IDmbProvider FromCompileJob(CompileJob compileJob);
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task{TResult}"/> resulting in a new <see cref="IDmbProvider"/> representing the <see cref="CompileJob"/> on success, <see langword="null"/> on failure</returns>
|
||||
Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all compile jobs that are inactive in the Game folder <paramref name="exceptThisOne"/>
|
||||
/// </summary>
|
||||
/// <param name="exceptThisOne">An optional compile job to not delete</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Host.Components.Repository;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Compiler
|
||||
{
|
||||
@@ -19,10 +19,11 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
/// Starts a compile
|
||||
/// </summary>
|
||||
/// <param name="projectName">The optional name of the .dme to compile without the extension if not pre</param>
|
||||
/// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level allowed for API validation</param>
|
||||
/// <param name="apiValidateTimeout">The time in seconds to wait while validating the API</param>
|
||||
/// <param name="repository">The <see cref="IRepository"/> to copy from</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. In particular, note the <see cref="CompileJob.RevisionInformation"/> field will only have it's <see cref="Api.Models.Internal.RevisionInformation.CommitSha"/> field populated</returns>
|
||||
Task<CompileJob> Compile(string projectName, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken);
|
||||
Task<Models.CompileJob> Compile(string projectName, DreamDaemonSecurity securityLevel, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -18,17 +18,19 @@ namespace Tgstation.Server.Host.Components.Compiler
|
||||
public string SecondaryDirectory => throw new NotSupportedException();
|
||||
|
||||
/// <inheritdoc />
|
||||
public CompileJob CompileJob => null;
|
||||
public CompileJob CompileJob { get; }
|
||||
|
||||
/// <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)
|
||||
/// <param name="compileJob">The value of <see cref="CompileJob"/></param>
|
||||
public TemporaryDmbProvider(string directory, string dmb, CompileJob compileJob)
|
||||
{
|
||||
DmbName = dmb ?? throw new ArgumentNullException(nameof(dmb));
|
||||
PrimaryDirectory = directory ?? throw new ArgumentNullException(nameof(directory));
|
||||
CompileJob = compileJob ?? throw new ArgumentNullException(nameof(compileJob));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -38,9 +38,8 @@
|
||||
/// No parameters
|
||||
/// </summary>
|
||||
ByondChangeComplete = 7,
|
||||
|
||||
/// <summary>
|
||||
/// Parameters: Origin commit sha
|
||||
/// Parameters: Game directory path, origin commit sha
|
||||
/// </summary>
|
||||
CompileStart = 8,
|
||||
/// <summary>
|
||||
@@ -52,7 +51,7 @@
|
||||
/// </summary>
|
||||
CompileFailure = 10,
|
||||
/// <summary>
|
||||
/// No parameters
|
||||
/// Parameters: Game directory path
|
||||
/// </summary>
|
||||
CompileComplete = 11,
|
||||
|
||||
|
||||
@@ -40,6 +40,11 @@ namespace Tgstation.Server.Host.Components
|
||||
/// </summary>
|
||||
IChat Chat { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICompileJobConsumer"/> for the <see cref="IInstance"/>
|
||||
/// </summary>
|
||||
ICompileJobConsumer CompileJobConsumer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="StaticFiles.IConfiguration"/> for the <see cref="IInstance"/>
|
||||
/// </summary>
|
||||
|
||||
@@ -35,10 +35,8 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <inheritdoc />
|
||||
public StaticFiles.IConfiguration Configuration { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ICompileJobConsumer"/> for the <see cref="Instance"/>
|
||||
/// </summary>
|
||||
readonly ICompileJobConsumer compileJobConsumer;
|
||||
/// <inheritdoc />
|
||||
public ICompileJobConsumer CompileJobConsumer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="Instance"/>
|
||||
@@ -79,7 +77,7 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <param name="watchdog">The value of <see cref="Watchdog"/></param>
|
||||
/// <param name="chat">The value of <see cref="Chat"/></param>
|
||||
/// <param name="configuration">The value of <see cref="Configuration"/></param>
|
||||
/// <param name="compileJobConsumer">The value of <see cref="compileJobConsumer"/></param>
|
||||
/// <param name="compileJobConsumer">The value of <see cref="CompileJobConsumer"/></param>
|
||||
/// <param name="databaseContextFactory">The value of <see cref="databaseContextFactory"/></param>
|
||||
/// <param name="dmbFactory">The value of <see cref="dmbFactory"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
@@ -92,7 +90,7 @@ namespace Tgstation.Server.Host.Components
|
||||
Watchdog = watchdog ?? throw new ArgumentNullException(nameof(watchdog));
|
||||
Chat = chat ?? throw new ArgumentNullException(nameof(chat));
|
||||
Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration));
|
||||
this.compileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
|
||||
CompileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer));
|
||||
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
|
||||
this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
@@ -102,7 +100,7 @@ namespace Tgstation.Server.Host.Components
|
||||
public void Dispose()
|
||||
{
|
||||
timerCts?.Dispose();
|
||||
compileJobConsumer.Dispose();
|
||||
CompileJobConsumer.Dispose();
|
||||
Configuration.Dispose();
|
||||
Chat.Dispose();
|
||||
Watchdog.Dispose();
|
||||
@@ -124,15 +122,19 @@ namespace Tgstation.Server.Host.Components
|
||||
|
||||
RepositorySettings repositorySettings = null;
|
||||
string projectName = null;
|
||||
uint timeout = 0;
|
||||
DreamDaemonSettings ddSettings = null;
|
||||
var dbTask = databaseContextFactory.UseContext(async (db) =>
|
||||
{
|
||||
var instanceQuery = db.Instances.Where(x => x.Id == metadata.Id);
|
||||
var timeoutTask = instanceQuery.Select(x => x.DreamDaemonSettings.StartupTimeout).FirstAsync(cancellationToken);
|
||||
var ddSettingsTask = instanceQuery.Select(x => x.DreamDaemonSettings).Select(x => new DreamDaemonSettings
|
||||
{
|
||||
StartupTimeout = x.StartupTimeout,
|
||||
SecurityLevel = x.SecurityLevel
|
||||
}).FirstAsync(cancellationToken);
|
||||
var projectNameTask = instanceQuery.Select(x => x.DreamMakerSettings.ProjectName).FirstOrDefaultAsync(cancellationToken);
|
||||
repositorySettings = await instanceQuery.Select(x => x.RepositorySettings).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
projectName = await projectNameTask.ConfigureAwait(false);
|
||||
timeout = (await timeoutTask.ConfigureAwait(false)).Value;
|
||||
ddSettings = await ddSettingsTask.ConfigureAwait(false);
|
||||
});
|
||||
using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
@@ -172,7 +174,7 @@ namespace Tgstation.Server.Host.Components
|
||||
if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
|
||||
await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, shouldSyncTracked, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var job = await DreamMaker.Compile(projectName, timeout, repo, cancellationToken).ConfigureAwait(false);
|
||||
var job = await DreamMaker.Compile(projectName, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
@@ -194,10 +196,23 @@ namespace Tgstation.Server.Host.Components
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(metadata.AutoUpdateInterval), Configuration.StartAsync(cancellationToken), ByondManager.StartAsync(cancellationToken), Watchdog.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), compileJobConsumer.StartAsync(cancellationToken));
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.WhenAll(SetAutoUpdateInterval(metadata.AutoUpdateInterval), Configuration.StartAsync(cancellationToken), ByondManager.StartAsync(cancellationToken), Chat.StartAsync(cancellationToken), CompileJobConsumer.StartAsync(cancellationToken)).ConfigureAwait(false);
|
||||
|
||||
//dependent on so many things, its just safer this way
|
||||
await Watchdog.StartAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
CompileJob latestCompileJob = null;
|
||||
await databaseContextFactory.UseContext(async db =>
|
||||
{
|
||||
latestCompileJob = await db.CompileJobs.Where(x => x.Job.Instance.Id == metadata.Id && x.Job.ExceptionDetails == null).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
}).ConfigureAwait(false);
|
||||
await dmbFactory.CleanUnusedCompileJobs(latestCompileJob, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(null), Configuration.StopAsync(cancellationToken), ByondManager.StopAsync(cancellationToken), Watchdog.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken), compileJobConsumer.StopAsync(cancellationToken));
|
||||
public Task StopAsync(CancellationToken cancellationToken) => Task.WhenAll(SetAutoUpdateInterval(null), Configuration.StopAsync(cancellationToken), ByondManager.StopAsync(cancellationToken), Watchdog.StopAsync(cancellationToken), Chat.StopAsync(cancellationToken), CompileJobConsumer.StopAsync(cancellationToken));
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SetAutoUpdateInterval(int? newInterval)
|
||||
|
||||
@@ -130,7 +130,7 @@ namespace Tgstation.Server.Host.Components
|
||||
var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, scriptExecutor, loggerFactory.CreateLogger<StaticFiles.Configuration>());
|
||||
var eventConsumer = new EventConsumer(configuration);
|
||||
|
||||
var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, metadata.CloneMetadata());
|
||||
var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger<DmbFactory>(), metadata.CloneMetadata());
|
||||
try
|
||||
{
|
||||
var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager, eventConsumer);
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Components.Watchdog;
|
||||
using Tgstation.Server.Host.Core;
|
||||
using Tgstation.Server.Host.IO;
|
||||
|
||||
@@ -16,11 +17,6 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <inheritdoc />
|
||||
sealed class InstanceManager : IInstanceManager, IHostedService, IInteropRegistrar, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// HTTP GET query key for interop access identifiers
|
||||
/// </summary>
|
||||
const string AccessIdentifierQueryKey = "access";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceFactory"/> for the <see cref="InstanceManager"/>
|
||||
/// </summary>
|
||||
@@ -178,10 +174,16 @@ namespace Tgstation.Server.Host.Components
|
||||
var tasks = new List<Task>();
|
||||
await dbInstances.ForEachAsync(metadata => tasks.Add(metadata.Online.Value ? OnlineInstance(metadata, cancellationToken) : Task.CompletedTask), cancellationToken).ConfigureAwait(false);
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
logger.LogInformation("Instance manager ready!");
|
||||
application.Ready(null);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
logger.LogInformation("Cancelled instance manager initialization!");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogCritical("Instance manager startup error! Exception: {0}", e);
|
||||
application.Ready(e);
|
||||
}
|
||||
});
|
||||
@@ -216,7 +218,7 @@ namespace Tgstation.Server.Host.Components
|
||||
if (query == null)
|
||||
throw new ArgumentNullException(nameof(query));
|
||||
|
||||
if (!query.TryGetValue(AccessIdentifierQueryKey, out StringValues values))
|
||||
if (!query.TryGetValue(InteropConstants.DMInteropAccessIdentifier, out StringValues values))
|
||||
return null;
|
||||
var accessIdentifier = values.FirstOrDefault();
|
||||
if (accessIdentifier == default)
|
||||
|
||||
@@ -80,7 +80,8 @@ namespace Tgstation.Server.Host.Components
|
||||
await databaseContextFactory.UseContext(async (db) =>
|
||||
result = await db.Instances.Where(x => x.Id == metadata.Id).Select(x => x.WatchdogReattachInformation).FirstAsync(cancellationToken).ConfigureAwait(false)
|
||||
).ConfigureAwait(false);
|
||||
return new WatchdogReattachInformation(result, dmbFactory);
|
||||
var bravoDmbTask = dmbFactory.FromCompileJob(result.Bravo.CompileJob, cancellationToken);
|
||||
return new WatchdogReattachInformation(result, await dmbFactory.FromCompileJob(result.Alpha.CompileJob, cancellationToken).ConfigureAwait(false), await bravoDmbTask.ConfigureAwait(false));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,10 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
|
||||
static readonly IReadOnlyDictionary<EventType, string> EventTypeScriptFileNameMap = new Dictionary<EventType, string>
|
||||
{
|
||||
{ EventType.CompileStart, "PreCompile" }
|
||||
};
|
||||
|
||||
static readonly string SystemScriptFileExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".bat" : ".sh";
|
||||
static readonly string SystemScriptFileExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "bat" : "sh";
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IIOManager"/> for <see cref="Configuration"/>
|
||||
@@ -223,7 +224,19 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
else
|
||||
task = ioManager.GetDirectories(GameStaticFilesSubdirectory, cancellationToken);
|
||||
var entries = await task.ConfigureAwait(false);
|
||||
await Task.WhenAll(entries.Select(x => symlinkFactory.CreateSymbolicLink(ioManager.ResolvePath(x), ioManager.ConcatPath(destination, x), cancellationToken))).ConfigureAwait(false);
|
||||
|
||||
await Task.WhenAll(task.Result.Select(async x =>
|
||||
{
|
||||
var destPath = ioManager.ConcatPath(destination, ioManager.GetFileName(x));
|
||||
logger.LogTrace("Symlinking {0} to {1}...", x, destPath);
|
||||
var fileExistsTask = ioManager.FileExists(destPath, cancellationToken);
|
||||
if (await ioManager.DirectoryExists(destPath, cancellationToken).ConfigureAwait(false))
|
||||
await ioManager.DeleteDirectory(destPath, cancellationToken).ConfigureAwait(false);
|
||||
var fileExists = await fileExistsTask.ConfigureAwait(false);
|
||||
if (fileExists)
|
||||
await ioManager.DeleteFile(destPath, cancellationToken).ConfigureAwait(false);
|
||||
await symlinkFactory.CreateSymbolicLink(ioManager.ResolvePath(x), ioManager.ResolvePath(destPath), cancellationToken).ConfigureAwait(false);
|
||||
})).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Task.WhenAll(SymlinkBase(true), SymlinkBase(false)).ConfigureAwait(false);
|
||||
@@ -298,7 +311,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
var files = await ioManager.GetFilesWithExtension(EventScriptsSubdirectory, SystemScriptFileExtension, cancellationToken).ConfigureAwait(false);
|
||||
var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory);
|
||||
|
||||
foreach (var I in files.Where(x => x.StartsWith(scriptName, StringComparison.Ordinal)))
|
||||
foreach (var I in files.Select(x => ioManager.GetFileName(x)).Where(x => x.StartsWith(scriptName, StringComparison.Ordinal)))
|
||||
if ((await scriptExecutor.ExecuteScript(ioManager.ConcatPath(resolvedScriptsDir, I), parameters, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -47,11 +47,11 @@ namespace Tgstation.Server.Host.Components.StaticFiles
|
||||
process.EnableRaisingEvents = true;
|
||||
|
||||
var tcs = new TaskCompletionSource<object>();
|
||||
process.Exited += (a, b) => tcs.SetResult(null);
|
||||
process.Exited += (a, b) => tcs.TrySetResult(null);
|
||||
try
|
||||
{
|
||||
process.Start();
|
||||
using (cancellationToken.Register(() => tcs.SetCanceled()))
|
||||
using (cancellationToken.Register(() => tcs.TrySetCanceled()))
|
||||
await tcs.Task.ConfigureAwait(false);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using Tgstation.Server.Api.Models;
|
||||
@@ -10,6 +11,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// <inheritdoc />
|
||||
sealed class Executor : IExecutor
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="Executor"/>
|
||||
/// </summary>
|
||||
readonly ILogger<Executor> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Change a given <paramref name="securityLevel"/> into the appropriate DreamDaemon command line word
|
||||
/// </summary>
|
||||
@@ -30,6 +36,15 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="Executor"/>
|
||||
/// </summary>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public Executor(ILogger<Executor> logger)
|
||||
{
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ISession AttachToDreamDaemon(int processId, IByondExecutableLock byondLock) => new Session(Process.GetProcessById(processId), byondLock);
|
||||
|
||||
@@ -57,6 +72,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
|
||||
SecurityWord(launchParameters.SecurityLevel.Value),
|
||||
parameters);
|
||||
|
||||
logger.LogTrace("Running DreamDaemon in {0}: {1} {2}", proc.StartInfo.WorkingDirectory, proc.StartInfo.FileName, proc.StartInfo.Arguments);
|
||||
|
||||
proc.Start();
|
||||
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
static class InteropConstants
|
||||
{
|
||||
|
||||
@@ -18,12 +18,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
public int? ExitCode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The peak virtual memory usage in bytes
|
||||
/// </summary>
|
||||
public long PeakMemory { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => String.Format(CultureInfo.InvariantCulture, "Exit Code: {0}, RAM: {1}, Time {2}ms", ExitCode, PeakMemory, StartupTime.TotalMilliseconds);
|
||||
public override string ToString() => String.Format(CultureInfo.InvariantCulture, "Exit Code: {0}, Time {1}ms", ExitCode, StartupTime.TotalMilliseconds);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Tgstation.Server.Host.Components.Compiler;
|
||||
using System;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
@@ -19,13 +19,13 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
public ReattachInformation() { }
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="ReattachInformation"/> from a given <paramref name="copy"/> and <paramref name="dmbFactory"/>
|
||||
/// Construct a <see cref="ReattachInformation"/> from a given <paramref name="copy"/> and <paramref name="dmb"/>
|
||||
/// </summary>
|
||||
/// <param name="copy">The <see cref="Models.ReattachInformation"/> to copy values from</param>
|
||||
/// <param name="dmbFactory">The <see cref="IDmbFactory"/> used to assign <see cref="Dmb"/></param>
|
||||
public ReattachInformation(Models.ReattachInformation copy, IDmbFactory dmbFactory) : base(copy)
|
||||
/// <param name="dmb">The value of <see cref="Dmb"/></param>
|
||||
public ReattachInformation(Models.ReattachInformation copy, IDmbProvider dmb) : base(copy)
|
||||
{
|
||||
Dmb = dmbFactory.FromCompileJob(copy.CompileJob);
|
||||
Dmb = dmb ?? throw new ArgumentNullException(nameof(dmb));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,16 +52,21 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
var result = new LaunchResult
|
||||
{
|
||||
ExitCode = process.HasExited ? (int?)process.ExitCode : null,
|
||||
PeakMemory = process.PeakWorkingSet64,
|
||||
StartupTime = DateTimeOffset.Now - startTime
|
||||
};
|
||||
if (result.PeakMemory == 0) //linux, best we can do honestly, test if this even works
|
||||
result.PeakMemory = process.WorkingSet64;
|
||||
return result;
|
||||
}, default, TaskCreationOptions.LongRunning, TaskScheduler.Current);
|
||||
lifetimeTask = new TaskCompletionSource<int>();
|
||||
process.EnableRaisingEvents = true;
|
||||
process.Exited += (a, b) => lifetimeTask.SetResult(process.ExitCode);
|
||||
try
|
||||
{
|
||||
process.EnableRaisingEvents = true;
|
||||
process.Exited += (a, b) => lifetimeTask.TrySetResult(process.ExitCode);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
//dead proccess
|
||||
lifetimeTask.TrySetResult(process.ExitCode);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -145,6 +145,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
bool apiValidated;
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="session"/> should be kept alive instead
|
||||
/// </summary>
|
||||
bool released;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="SessionController"/>
|
||||
/// </summary>
|
||||
@@ -171,22 +176,50 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
portClosed = false;
|
||||
disposed = false;
|
||||
apiValidated = false;
|
||||
released = false;
|
||||
|
||||
rebootTcs = new TaskCompletionSource<object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finalize the <see cref="SessionController"/>
|
||||
/// </summary>
|
||||
~SessionController() => Dispose(false);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void Dispose(bool disposing)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (disposed)
|
||||
return;
|
||||
session.Dispose();
|
||||
interopContext.Dispose();
|
||||
Dmb?.Dispose(); //will be null when released
|
||||
chatJsonTrackingContext.Dispose();
|
||||
disposed = true;
|
||||
if (disposing)
|
||||
{
|
||||
if (!released)
|
||||
session.Terminate();
|
||||
session.Dispose();
|
||||
interopContext.Dispose();
|
||||
Dmb?.Dispose(); //will be null when released
|
||||
chatJsonTrackingContext.Dispose();
|
||||
disposed = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (logger != null)
|
||||
logger.LogError("Being disposed via finalizer!");
|
||||
if (!released)
|
||||
if (session != null)
|
||||
session.Terminate();
|
||||
else if (logger != null)
|
||||
logger.LogCritical("Unable to terminate active DreamDaemon session due to finalizer ordering!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +274,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Throws an <see cref="ObjectDisposedException"/> if <see cref="Dispose"/> has been called
|
||||
/// Throws an <see cref="ObjectDisposedException"/> if <see cref="Dispose(bool)"/> has been called
|
||||
/// </summary>
|
||||
void CheckDisposed()
|
||||
{
|
||||
@@ -256,6 +289,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
//we still don't want to dispose the dmb yet, even though we're keeping it alive
|
||||
var tmpProvider = reattachInformation.Dmb;
|
||||
reattachInformation.Dmb = null;
|
||||
released = true;
|
||||
Dispose();
|
||||
Dmb.KeepAlive();
|
||||
reattachInformation.Dmb = tmpProvider;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Byond.TopicSender;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
@@ -116,26 +117,34 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
InstanceName = instance.Name,
|
||||
Revision = dmbProvider.CompileJob.RevisionInformation
|
||||
};
|
||||
interopInfo.TestMerges.AddRange(dmbProvider.CompileJob.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => new TestMerge
|
||||
{
|
||||
Author = x.Author,
|
||||
Body = x.BodyAtMerge,
|
||||
Comment = x.Comment,
|
||||
CommitSha = x.PrimaryRevisionInformation.CommitSha,
|
||||
Number = x.Number,
|
||||
OriginCommitSha = x.PrimaryRevisionInformation.OriginCommitSha,
|
||||
PullRequestCommit = x.PullRequestRevision,
|
||||
TimeMerged = x.MergedAt.Ticks,
|
||||
Title = x.TitleAtMerge,
|
||||
Url = x.Url
|
||||
}));
|
||||
|
||||
if (dmbProvider.CompileJob.RevisionInformation != null) //null while compiling
|
||||
interopInfo.TestMerges.AddRange(dmbProvider.CompileJob.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => new TestMerge
|
||||
{
|
||||
Author = x.Author,
|
||||
Body = x.BodyAtMerge,
|
||||
Comment = x.Comment,
|
||||
CommitSha = x.PrimaryRevisionInformation.CommitSha,
|
||||
Number = x.Number,
|
||||
OriginCommitSha = x.PrimaryRevisionInformation.OriginCommitSha,
|
||||
PullRequestCommit = x.PullRequestRevision,
|
||||
TimeMerged = x.MergedAt.Ticks,
|
||||
Title = x.TitleAtMerge,
|
||||
Url = x.Url
|
||||
}));
|
||||
|
||||
var interopJsonFile = GuidJsonFile();
|
||||
|
||||
var interopJson = JsonConvert.SerializeObject(interopInfo);
|
||||
var interopJson = JsonConvert.SerializeObject(interopInfo, Formatting.Indented, new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new DefaultContractResolver
|
||||
{
|
||||
NamingStrategy = new CamelCaseNamingStrategy()
|
||||
}
|
||||
});
|
||||
|
||||
var basePath = primaryDirectory ? dmbProvider.PrimaryDirectory : dmbProvider.SecondaryDirectory;
|
||||
var localIoManager = new ResolvingIOManager(ioManager, ioManager.ConcatPath(basePath, dmbProvider.DmbName));
|
||||
var localIoManager = new ResolvingIOManager(ioManager, basePath);
|
||||
|
||||
var chatJsonTrackingTask = chat.TrackJsons(basePath, interopInfo.ChatChannelsJson, interopInfo.ChatCommandsJson, cancellationToken);
|
||||
|
||||
|
||||
@@ -182,7 +182,6 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
/// </summary>
|
||||
void DisposeAndNullControllers()
|
||||
{
|
||||
logger.LogTrace("DisposeAndNullControllers");
|
||||
alphaServer?.Dispose();
|
||||
alphaServer = null;
|
||||
bravoServer?.Dispose();
|
||||
@@ -315,7 +314,12 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
//either way try to start it using the active server's dmb as a backup
|
||||
try
|
||||
{
|
||||
var dmbBackup = dmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob);
|
||||
var dmbBackup = await dmbFactory.FromCompileJob(monitorState.ActiveServer.Dmb.CompileJob, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (dmbBackup == null) //NANI!?
|
||||
//just give up, if THAT compile job is failing then the ActiveServer is gonna crash soon too or already has
|
||||
throw new Exception("Creating backup DMB provider failed!");
|
||||
|
||||
monitorState.InactiveServer = await sessionControllerFactory.LaunchNew(ActiveLaunchParameters, dmbBackup, null, false, !monitorState.ActiveServer.IsPrimary, false, cancellationToken).ConfigureAwait(false);
|
||||
usedMostRecentDmb = false;
|
||||
await chat.SendWatchdogMessage("Staging newest DMB on inactive server failed: {0} Falling back to previous dmb...", cancellationToken).ConfigureAwait(false);
|
||||
@@ -327,7 +331,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
catch (Exception e2)
|
||||
{
|
||||
//fuuuuucckkk
|
||||
logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! This Exception: {0}", e2.ToString());
|
||||
logger.LogError("Backup strategy failed! Monitor will restart when active server reboots! Exception: {0}", e2.ToString());
|
||||
monitorState.InactiveServerCritFail = true;
|
||||
await chat.SendWatchdogMessage("Attempted reboot of inactive server failed. Watchdog will reset when active server fails or exits", cancellationToken).ConfigureAwait(false);
|
||||
return true; //we didn't use the old dmb
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Tgstation.Server.Host.Components.Compiler;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Models;
|
||||
|
||||
namespace Tgstation.Server.Host.Components.Watchdog
|
||||
{
|
||||
@@ -24,16 +23,17 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
public WatchdogReattachInformation() { }
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="WatchdogReattachInformation"/> from a given <paramref name="copy"/> with a given <paramref name="dmbFactory"/>
|
||||
/// Construct a <see cref="WatchdogReattachInformation"/> from a given <paramref name="copy"/> with a given <paramref name="dmbAlpha"/> and <paramref name="dmbBravo"/>
|
||||
/// </summary>
|
||||
/// <param name="copy">The <see cref="WatchdogReattachInformationBase"/> to copy information from</param>
|
||||
/// <param name="dmbFactory">The <see cref="IDmbFactory"/> used to build the <see cref="ReattachInformation.Dmb"/>s</param>
|
||||
public WatchdogReattachInformation(Models.WatchdogReattachInformation copy, IDmbFactory dmbFactory): base(copy)
|
||||
/// <param name="dmbAlpha">The <see cref="IDmbProvider"/> used to build <see cref="Alpha"/></param>
|
||||
/// <param name="dmbBravo">The <see cref="IDmbProvider"/> used to build <see cref="Bravo"/></param>
|
||||
public WatchdogReattachInformation(Models.WatchdogReattachInformation copy, IDmbProvider dmbAlpha, IDmbProvider dmbBravo): base(copy)
|
||||
{
|
||||
if (copy.Alpha != null)
|
||||
Alpha = new ReattachInformation(copy.Alpha, dmbFactory);
|
||||
Alpha = new ReattachInformation(copy.Alpha, dmbAlpha);
|
||||
if (copy.Bravo != null)
|
||||
Bravo = new ReattachInformation(copy.Bravo, dmbFactory);
|
||||
Bravo = new ReattachInformation(copy.Bravo, dmbBravo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,5 +24,10 @@
|
||||
/// The connection string for the database
|
||||
/// </summary>
|
||||
public string ConnectionString { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If the database should use direct table creation instead of automatic migrations. Should not be used in production!
|
||||
/// </summary>
|
||||
public bool NoMigrations { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
Logger.LogInformation("Request made by User ID {0}. Api version: {1}. User-Agent: {2}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture) ?? "NULL", ApiHeaders.ApiVersion, ApiHeaders.UserAgent);
|
||||
Logger.LogTrace("Request made by User ID {0}. Api version: {1}. User-Agent: {2}. Type: {3}. Route {4}", AuthenticationContext?.User.Id.ToString(CultureInfo.InvariantCulture), ApiHeaders.ApiVersion, ApiHeaders.UserAgent, Request.Method, Request.Path);
|
||||
await base.OnActionExecutionAsync(context, next).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,9 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|| !CheckModified(x => x.SoftShutdown, DreamDaemonRights.SoftShutdown)
|
||||
|| !CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout))
|
||||
return Forbid();
|
||||
|
||||
if (current.SecurityLevel == DreamDaemonSecurity.Ultrasafe)
|
||||
return BadRequest(new ErrorMessage { Message = "TGS does not support the ultrasafe DreamDaemon configuration!" });
|
||||
|
||||
var wd = instanceManager.GetInstance(Instance).Watchdog;
|
||||
|
||||
|
||||
@@ -101,14 +101,18 @@ namespace Tgstation.Server.Host.Controllers
|
||||
var instanceManager = serviceProvider.GetRequiredService<IInstanceManager>();
|
||||
var databaseContext = serviceProvider.GetRequiredService<IDatabaseContext>();
|
||||
|
||||
var timeoutTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => x.StartupTimeout).FirstOrDefaultAsync(cancellationToken);
|
||||
var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => new DreamDaemonSettings{
|
||||
StartupTimeout = x.StartupTimeout,
|
||||
SecurityLevel = x.SecurityLevel
|
||||
}).FirstOrDefaultAsync(cancellationToken);
|
||||
var projectName = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => x.ProjectName).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
var timeout = await timeoutTask.ConfigureAwait(false);
|
||||
var ddSettings = await ddSettingsTask.ConfigureAwait(false);
|
||||
|
||||
var instance = instanceManager.GetInstance(instanceModel);
|
||||
|
||||
CompileJob compileJob;
|
||||
string repoSha = null;
|
||||
Task<RevisionInformation> revInfoTask;
|
||||
string repoSha;
|
||||
using (var repo = await instance.RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (repo == null)
|
||||
@@ -117,11 +121,15 @@ namespace Tgstation.Server.Host.Controllers
|
||||
return;
|
||||
}
|
||||
repoSha = repo.Head;
|
||||
compileJob = await instance.DreamMaker.Compile(projectName, timeout.Value, repo, cancellationToken).ConfigureAwait(false);
|
||||
revInfoTask = databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha).Select(x => new RevisionInformation { Id = x.Id }).FirstOrDefaultAsync();
|
||||
compileJob = await instance.DreamMaker.Compile(projectName, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (compileJob.DMApiValidated != true)
|
||||
return;
|
||||
|
||||
compileJob.Job = job;
|
||||
compileJob.RevisionInformation = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha).Select(x => new RevisionInformation { Id = x.Id }).FirstOrDefaultAsync().ConfigureAwait(false);
|
||||
compileJob.RevisionInformation = await revInfoTask.ConfigureAwait(false);
|
||||
|
||||
if (compileJob.RevisionInformation == default)
|
||||
{
|
||||
@@ -134,12 +142,26 @@ namespace Tgstation.Server.Host.Controllers
|
||||
Id = Instance.Id
|
||||
}
|
||||
};
|
||||
DatabaseContext.Instances.Attach(compileJob.RevisionInformation.Instance);
|
||||
databaseContext.Instances.Attach(compileJob.RevisionInformation.Instance);
|
||||
}
|
||||
else
|
||||
databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation);
|
||||
|
||||
databaseContext.CompileJobs.Add(compileJob);
|
||||
//default ct because we don't want to give up after getting this far
|
||||
await databaseContext.Save(default).ConfigureAwait(false);
|
||||
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
//now load the entire compile job tree into the consumer
|
||||
//default ct because we don't want to give up after getting this far since we already set this job as staged in the db
|
||||
var finalCompileJob = await databaseContext.CompileJobs.Where(x => x.Id == compileJob.Id)
|
||||
.Include(x => x.Job).ThenInclude(x => x.StartedBy)
|
||||
.Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
|
||||
.Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
|
||||
.FirstOrDefaultAsync().ConfigureAwait(false); //can't wait to see that query
|
||||
if (finalCompileJob == null)
|
||||
//lol git fucked
|
||||
return;
|
||||
|
||||
await instance.CompileJobConsumer.LoadCompileJob(finalCompileJob, default).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,7 +126,10 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
var token = tokenFactory.CreateToken(user, out var expiry);
|
||||
if (identity != null)
|
||||
identityCache.CacheSystemIdentity(user, identity, expiry.AddSeconds(10)); //expire the identity slightly after the auth token in case of lag
|
||||
identityCache.CacheSystemIdentity(user, identity, expiry.AddMinutes(1)); //expire the identity slightly after the auth token in case of lag
|
||||
|
||||
Logger.LogDebug("Successfully logged in user {0}!", user.Id);
|
||||
|
||||
return Json(token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
AutoStart = false,
|
||||
PrimaryPort = 1337,
|
||||
SecondaryPort = 1338,
|
||||
SecurityLevel = DreamDaemonSecurity.Ultrasafe,
|
||||
SecurityLevel = DreamDaemonSecurity.Safe,
|
||||
SoftRestart = false,
|
||||
SoftShutdown = false,
|
||||
StartupTimeout = 20
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Net.Http.Headers;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Components;
|
||||
using Tgstation.Server.Host.Models;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
@@ -13,23 +14,27 @@ namespace Tgstation.Server.Host.Controllers
|
||||
/// Handles requests from DreamDaemon
|
||||
/// </summary>
|
||||
[Route("/Interop")]
|
||||
public sealed class InteropController : ApiController
|
||||
public sealed class InteropController : Controller //not an ApiController because "lol im byond and who is headers?"
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceManager"/> for the <see cref="InteropController"/>
|
||||
/// </summary>
|
||||
readonly IInstanceManager instanceManager;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="InteropController"/>
|
||||
/// </summary>
|
||||
readonly ILogger<InteropController> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Construct an <see cref="InteropController"/>
|
||||
/// </summary>
|
||||
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="authenticationContextFactory">The <see cref="IAuthenticationContextFactory"/> for the <see cref="ApiController"/></param>
|
||||
/// <param name="instanceManager">The value of <see cref="instanceManager"/></param>
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="ApiController"/></param>
|
||||
public InteropController(IInstanceManager instanceManager, IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, ILogger<InteropController> logger) : base(databaseContext, authenticationContextFactory, logger, false)
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public InteropController(IInstanceManager instanceManager, ILogger<InteropController> logger)
|
||||
{
|
||||
this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -40,6 +45,14 @@ namespace Tgstation.Server.Host.Controllers
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> HandleInterop(CancellationToken cancellationToken)
|
||||
{
|
||||
//since this is the only identifying factor of a TGS server we want to pretend we don't exist unless it at least has the correct BYOND headers
|
||||
if (!Request.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues)
|
||||
|| !ProductInfoHeaderValue.TryParse(userAgentValues.FirstOrDefault(), out var clientUserAgent)
|
||||
|| clientUserAgent.Product.Name != "libbyond")
|
||||
return Unauthorized();
|
||||
|
||||
logger.LogDebug("Request from BYOND: {0}", Request.QueryString);
|
||||
|
||||
var result = await instanceManager.HandleWorldExport(Request.Query, cancellationToken).ConfigureAwait(false);
|
||||
//explain things in very simple terms dream daemon can understand
|
||||
//EXCEPT DREAMDAENEN BUGS
|
||||
|
||||
@@ -35,6 +35,16 @@ namespace Tgstation.Server.Host.Controllers
|
||||
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[TgsAuthorize]
|
||||
public override async Task<IActionResult> Read(CancellationToken cancellationToken)
|
||||
{
|
||||
var result = await DatabaseContext.Jobs.Where(x => x.Instance.Id == Instance.Id).OrderByDescending(x => x.StartedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (result == null)
|
||||
return StatusCode((int)HttpStatusCode.Gone);
|
||||
return Json(result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[TgsAuthorize]
|
||||
public override async Task<IActionResult> List(CancellationToken cancellationToken)
|
||||
|
||||
@@ -300,7 +300,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
|
||||
await jobManager.RegisterOperation(job, async (paramJob, serviceProvider, progressReporter, ct) =>
|
||||
{
|
||||
using (var repo = await instanceManager.GetInstance(Instance).RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
|
||||
using (var repo = await instanceManager.GetInstance(Instance).RepositoryManager.LoadRepository(ct).ConfigureAwait(false))
|
||||
{
|
||||
if (repo == null)
|
||||
throw new InvalidOperationException("Repository could not be loaded!");
|
||||
@@ -330,13 +330,13 @@ namespace Tgstation.Server.Host.Controllers
|
||||
};
|
||||
databaseContext.Instances.Attach(attachedInstance);
|
||||
|
||||
await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, cancellationToken).ConfigureAwait(false);
|
||||
await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false);
|
||||
|
||||
//apply new rev info, tracking applied test merges
|
||||
async Task UpdateRevInfo()
|
||||
{
|
||||
var last = lastRevisionInfo;
|
||||
await LoadRevisionInformation(repo, databaseContext, attachedInstance, last.OriginCommitSha, x => lastRevisionInfo = x, cancellationToken).ConfigureAwait(false);
|
||||
await LoadRevisionInformation(repo, databaseContext, attachedInstance, last.OriginCommitSha, x => lastRevisionInfo = x, ct).ConfigureAwait(false);
|
||||
lastRevisionInfo.ActiveTestMerges.AddRange(last.ActiveTestMerges);
|
||||
};
|
||||
|
||||
@@ -347,11 +347,11 @@ namespace Tgstation.Server.Host.Controllers
|
||||
{
|
||||
if (!repo.Tracking && model.Reference == null)
|
||||
throw new InvalidOperationException("Not on an updatable reference!");
|
||||
await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, x => progressReporter(x / numFetches), cancellationToken).ConfigureAwait(false);
|
||||
await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, x => progressReporter(x / numFetches), ct).ConfigureAwait(false);
|
||||
doneFetches = 1;
|
||||
if (!modelHasShaOrReference)
|
||||
{
|
||||
var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, cancellationToken).ConfigureAwait(false);
|
||||
var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, ct).ConfigureAwait(false);
|
||||
if (!fastForward.HasValue)
|
||||
throw new InvalidOperationException("Merge conflict occurred during origin update!");
|
||||
await UpdateRevInfo().ConfigureAwait(false);
|
||||
@@ -366,83 +366,106 @@ namespace Tgstation.Server.Host.Controllers
|
||||
if ((model.CheckoutSha != null && repo.Head.ToUpperInvariant() != model.CheckoutSha.ToUpperInvariant())
|
||||
|| (model.Reference != null && repo.Reference != model.Reference))
|
||||
{
|
||||
await repo.CheckoutObject(model.CheckoutSha ?? model.Reference, cancellationToken).ConfigureAwait(false);
|
||||
await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, cancellationToken).ConfigureAwait(false); //we've either seen origin before or what we're checking out is on origin
|
||||
await repo.CheckoutObject(model.CheckoutSha ?? model.Reference, ct).ConfigureAwait(false);
|
||||
await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); //we've either seen origin before or what we're checking out is on origin
|
||||
}
|
||||
|
||||
if (model.UpdateFromOrigin == true && model.Reference != null)
|
||||
{
|
||||
if (!repo.Tracking)
|
||||
throw new InvalidOperationException("Checked out reference does not track a remote object!");
|
||||
await repo.ResetToOrigin(cancellationToken).ConfigureAwait(false);
|
||||
await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, true, cancellationToken).ConfigureAwait(false);
|
||||
await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, cancellationToken).ConfigureAwait(false);
|
||||
await repo.ResetToOrigin(ct).ConfigureAwait(false);
|
||||
await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, true, ct).ConfigureAwait(false);
|
||||
await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false);
|
||||
//repo head is on origin so force this
|
||||
//will update the db if necessary
|
||||
lastRevisionInfo.OriginCommitSha = repo.Head;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//test merging
|
||||
if (newTestMerges)
|
||||
{
|
||||
var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) : gitHubClientFactory.CreateClient();
|
||||
var contextUser = new Models.User
|
||||
//optimization: if we've already merged these exact same commits in this fashion before, just find the rev info for it and check it out
|
||||
Models.RevisionInformation revInfoWereLookingFor = null;
|
||||
if(lastRevisionInfo.OriginCommitSha == lastRevisionInfo.CommitSha)
|
||||
{
|
||||
Id = AuthenticationContext.User.Id
|
||||
};
|
||||
databaseContext.Users.Attach(contextUser);
|
||||
foreach (var I in model.NewTestMerges)
|
||||
//normalize the shas to lowercase ala libgit2
|
||||
#pragma warning disable CA1308 // Normalize strings to uppercase
|
||||
I.PullRequestRevision = I.PullRequestRevision?.ToLowerInvariant();
|
||||
#pragma warning restore CA1308 // Normalize strings to uppercase
|
||||
|
||||
var repoOwner = repo.GitHubOwner;
|
||||
var repoName = repo.GitHubRepoName;
|
||||
foreach (var I in model.NewTestMerges)
|
||||
revInfoWereLookingFor = await databaseContext.RevisionInformations.Where(
|
||||
x => x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
|
||||
&& x.ActiveTestMerges.Count == model.NewTestMerges.Count)
|
||||
//split here cause this bit probably has to be done locally
|
||||
.Where(x => x.ActiveTestMerges.Select(y => y.TestMerge).All(y => model.NewTestMerges.Any(z => y.Number == z.Number && y.PullRequestRevision.StartsWith(z.PullRequestRevision, StringComparison.Ordinal)))).FirstOrDefaultAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
|
||||
if (revInfoWereLookingFor != null)
|
||||
await repo.ResetToSha(revInfoWereLookingFor.CommitSha, cancellationToken).ConfigureAwait(false);
|
||||
else
|
||||
{
|
||||
Octokit.PullRequest pr = null;
|
||||
string errorMessage = null;
|
||||
try
|
||||
var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) : gitHubClientFactory.CreateClient();
|
||||
var contextUser = new Models.User
|
||||
{
|
||||
pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false);
|
||||
}
|
||||
catch (Octokit.RateLimitExceededException)
|
||||
{
|
||||
//you look at your anonymous access and sigh
|
||||
errorMessage = "P.R.E. RATE LIMITED";
|
||||
}
|
||||
catch (Octokit.NotFoundException)
|
||||
{
|
||||
//you look at your shithub access and sigh
|
||||
errorMessage = "P.R.E. NOT FOUND";
|
||||
}
|
||||
|
||||
var mergeResult = await repo.AddTestMerge(I.Number, I.PullRequestRevision, committerName, currentModel.CommitterEmail, String.Format(CultureInfo.InvariantCulture, "Test merge of pull request #{0}{1}{2}", I.Number, I.Comment != null ? Environment.NewLine : null, I.Comment), currentModel.AccessUser, currentModel.AccessToken, x => progressReporter((x + 100 * doneFetches) / numFetches), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (!mergeResult.HasValue) //conflict, we don't care, dd already knows
|
||||
continue;
|
||||
|
||||
++doneFetches;
|
||||
|
||||
var revInfoUpdateTask = UpdateRevInfo();
|
||||
|
||||
var tm = new Models.TestMerge
|
||||
{
|
||||
Author = pr?.User.Login ?? errorMessage,
|
||||
BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty,
|
||||
MergedAt = DateTimeOffset.Now,
|
||||
TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty,
|
||||
Comment = I.Comment,
|
||||
Number = I.Number,
|
||||
MergedBy = contextUser,
|
||||
PullRequestRevision = I.PullRequestRevision,
|
||||
Url = pr?.HtmlUrl ?? errorMessage
|
||||
Id = AuthenticationContext.User.Id
|
||||
};
|
||||
databaseContext.Users.Attach(contextUser);
|
||||
|
||||
await revInfoUpdateTask.ConfigureAwait(false);
|
||||
|
||||
lastRevisionInfo.PrimaryTestMerge = tm;
|
||||
lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
|
||||
var repoOwner = repo.GitHubOwner;
|
||||
var repoName = repo.GitHubRepoName;
|
||||
foreach (var I in model.NewTestMerges)
|
||||
{
|
||||
TestMerge = tm
|
||||
});
|
||||
Octokit.PullRequest pr = null;
|
||||
string errorMessage = null;
|
||||
try
|
||||
{
|
||||
pr = await gitHubClient.PullRequest.Get(repoOwner, repoName, I.Number).ConfigureAwait(false);
|
||||
}
|
||||
catch (Octokit.RateLimitExceededException)
|
||||
{
|
||||
//you look at your anonymous access and sigh
|
||||
errorMessage = "P.R.E. RATE LIMITED";
|
||||
}
|
||||
catch (Octokit.NotFoundException)
|
||||
{
|
||||
//you look at your shithub access and sigh
|
||||
errorMessage = "P.R.E. NOT FOUND";
|
||||
}
|
||||
|
||||
var mergeResult = await repo.AddTestMerge(I.Number, I.PullRequestRevision, committerName, currentModel.CommitterEmail, String.Format(CultureInfo.InvariantCulture, "Test merge of pull request #{0}{1}{2}", I.Number, I.Comment != null ? Environment.NewLine : null, I.Comment), currentModel.AccessUser, currentModel.AccessToken, x => progressReporter((x + 100 * doneFetches) / numFetches), ct).ConfigureAwait(false);
|
||||
|
||||
if (!mergeResult.HasValue) //conflict, we don't care, dd already knows
|
||||
continue;
|
||||
|
||||
++doneFetches;
|
||||
|
||||
var revInfoUpdateTask = UpdateRevInfo();
|
||||
|
||||
var tm = new Models.TestMerge
|
||||
{
|
||||
Author = pr?.User.Login ?? errorMessage,
|
||||
BodyAtMerge = pr?.Body ?? errorMessage ?? String.Empty,
|
||||
MergedAt = DateTimeOffset.Now,
|
||||
TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty,
|
||||
Comment = I.Comment,
|
||||
Number = I.Number,
|
||||
MergedBy = contextUser,
|
||||
PullRequestRevision = I.PullRequestRevision,
|
||||
Url = pr?.HtmlUrl ?? errorMessage
|
||||
};
|
||||
|
||||
await revInfoUpdateTask.ConfigureAwait(false);
|
||||
|
||||
lastRevisionInfo.PrimaryTestMerge = tm;
|
||||
lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
|
||||
{
|
||||
TestMerge = tm
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -451,7 +474,7 @@ namespace Tgstation.Server.Host.Controllers
|
||||
await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, false, ct).ConfigureAwait(false);
|
||||
await UpdateRevInfo().ConfigureAwait(false);
|
||||
}
|
||||
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
await databaseContext.Save(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
|
||||
@@ -243,6 +243,8 @@ namespace Tgstation.Server.Host.Core
|
||||
|
||||
logger.LogInformation(VersionString);
|
||||
|
||||
logger.LogTrace("Configuring middleware...");
|
||||
|
||||
serverAddresses = applicationBuilder.ServerFeatures.Get<IServerAddressesFeature>();
|
||||
|
||||
applicationBuilder.UseDeveloperExceptionPage(); //it is not worth it to limit this, you should only ever get it if you're an authorized user
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -17,6 +17,12 @@ namespace Tgstation.Server.Host.Core
|
||||
/// The <see cref="IServiceProvider"/> for the <see cref="JobManager"/>
|
||||
/// </summary>
|
||||
readonly IServiceProvider serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="JobManager"/>
|
||||
/// </summary>
|
||||
readonly ILogger<JobManager> logger;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Dictionary{TKey, TValue}"/> of <see cref="Api.Models.Internal.Job.Id"/> to running <see cref="JobHandler"/>s
|
||||
/// </summary>
|
||||
@@ -26,9 +32,11 @@ namespace Tgstation.Server.Host.Core
|
||||
/// Construct a <see cref="JobManager"/>
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The value of <see cref="serviceProvider"/></param>
|
||||
public JobManager(IServiceProvider serviceProvider)
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public JobManager(IServiceProvider serviceProvider, ILogger<JobManager> logger)
|
||||
{
|
||||
this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
jobs = new Dictionary<long, JobHandler>();
|
||||
}
|
||||
|
||||
@@ -72,22 +80,21 @@ namespace Tgstation.Server.Host.Core
|
||||
{
|
||||
var oldJob = job;
|
||||
job = new Job { Id = oldJob.Id };
|
||||
try
|
||||
{
|
||||
await operation(job, scope.ServiceProvider, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
databaseContext = scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
|
||||
databaseContext.Jobs.Attach(job);
|
||||
}
|
||||
databaseContext = scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
|
||||
databaseContext.Jobs.Attach(job);
|
||||
|
||||
await operation(job, scope.ServiceProvider, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
logger.LogDebug("Job {0} completed!", job.Id);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
logger.LogDebug("Job {0} cancelled!", job.Id);
|
||||
job.Cancelled = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, e);
|
||||
job.ExceptionDetails = e.ToString();
|
||||
}
|
||||
job.StoppedAt = DateTimeOffset.Now;
|
||||
@@ -128,6 +135,7 @@ namespace Tgstation.Server.Host.Core
|
||||
}
|
||||
databaseContext.Jobs.Add(job);
|
||||
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description);
|
||||
var jobHandler = JobHandler.Create(x => RunJob(job, (jobParam, serviceProvider, ct) =>
|
||||
operation(jobParam, serviceProvider, y =>
|
||||
{
|
||||
@@ -144,20 +152,26 @@ namespace Tgstation.Server.Host.Core
|
||||
/// <inheritdoc />
|
||||
public async Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
logger.LogTrace("Starting job manager...");
|
||||
using (var scope = serviceProvider.CreateScope())
|
||||
{
|
||||
var databaseContext = scope.ServiceProvider.GetRequiredService<IDatabaseContext>();
|
||||
|
||||
//mark all jobs as cancelled
|
||||
var enumerator = await databaseContext.Jobs.Where(y => !y.Cancelled.Value && !y.StoppedAt.HasValue).Select(y => y.Id).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
foreach(var I in enumerator)
|
||||
var badJobs = await databaseContext.Jobs.Where(y => !y.Cancelled.Value && !y.StoppedAt.HasValue).Select(y => y.Id).ToListAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (badJobs.Count > 0)
|
||||
{
|
||||
var job = new Job { Id = I };
|
||||
databaseContext.Jobs.Attach(job);
|
||||
job.Cancelled = true;
|
||||
logger.LogTrace("Cleaning {0} unfinished jobs...", badJobs.Count);
|
||||
foreach (var I in badJobs)
|
||||
{
|
||||
var job = new Job { Id = I };
|
||||
databaseContext.Jobs.Attach(job);
|
||||
job.Cancelled = true;
|
||||
}
|
||||
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
logger.LogDebug("Job manager started!");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -34,7 +34,11 @@ namespace Tgstation.Server.Host.IO
|
||||
foreach (var subDir in dir.EnumerateDirectories())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
tasks.Add(NormalizeAndDelete(subDir, cancellationToken));
|
||||
if (!subDir.Attributes.HasFlag(FileAttributes.Directory) || subDir.Attributes.HasFlag(FileAttributes.ReparsePoint))
|
||||
//this is probably a symlink
|
||||
subDir.Delete();
|
||||
else
|
||||
tasks.Add(NormalizeAndDelete(subDir, cancellationToken));
|
||||
}
|
||||
foreach (var file in dir.EnumerateFiles())
|
||||
{
|
||||
@@ -61,34 +65,40 @@ namespace Tgstation.Server.Host.IO
|
||||
/// <param name="dest">The destination directory path</param>
|
||||
/// <param name="ignore">Files and folders to ignore at the root level</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
async Task CopyDirectoryImpl(string src, string dest, IEnumerable<string> ignore, CancellationToken cancellationToken)
|
||||
/// <returns>A <see cref="IEnumerable{T}"/> of <see cref="Task"/>s representing the running operation</returns>
|
||||
IEnumerable<Task> CopyDirectoryImpl(string src, string dest, IEnumerable<string> ignore, CancellationToken cancellationToken)
|
||||
{
|
||||
await CreateDirectory(dest, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var dir = new DirectoryInfo(src);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var dirs = dir.EnumerateDirectories();
|
||||
var files = dir.EnumerateFiles();
|
||||
|
||||
var fileCopyTasks = files.Select(x =>
|
||||
var atLeastOneSubDir = false;
|
||||
foreach (var I in dir.EnumerateDirectories())
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (ignore != null && ignore.Contains(x.Name))
|
||||
return Task.CompletedTask;
|
||||
return CopyFile(x.FullName, Path.Combine(dest, x.Name), cancellationToken);
|
||||
});
|
||||
if (ignore != null && ignore.Contains(I.Name))
|
||||
continue;
|
||||
foreach (var J in CopyDirectoryImpl(I.FullName, Path.Combine(dest, I.Name), null, cancellationToken))
|
||||
{
|
||||
atLeastOneSubDir = true;
|
||||
yield return J;
|
||||
}
|
||||
}
|
||||
|
||||
var directoryCopyTasks = dirs.Select(x =>
|
||||
async Task CopyThisDirectory()
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
if (ignore != null && ignore.Contains(x.Name))
|
||||
return Task.CompletedTask;
|
||||
return CopyDirectoryImpl(x.FullName, Path.Combine(dest, x.Name), null, cancellationToken);
|
||||
});
|
||||
if (!atLeastOneSubDir)
|
||||
await CreateDirectory(dest, cancellationToken).ConfigureAwait(false); //save on createdir calls
|
||||
|
||||
await Task.WhenAll(fileCopyTasks.Concat(directoryCopyTasks)).ConfigureAwait(false);
|
||||
var tasks = new List<Task>();
|
||||
|
||||
await dir.EnumerateFiles().ToAsyncEnumerable().ForEachAsync(I =>
|
||||
{
|
||||
if (ignore != null && ignore.Contains(I.Name))
|
||||
return;
|
||||
tasks.Add(CopyFile(I.FullName, Path.Combine(dest, I.Name), cancellationToken));
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
};
|
||||
|
||||
yield return CopyThisDirectory();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -101,7 +111,8 @@ namespace Tgstation.Server.Host.IO
|
||||
|
||||
src = ResolvePath(src);
|
||||
dest = ResolvePath(dest);
|
||||
await CopyDirectoryImpl(src, dest, ignore, cancellationToken).ConfigureAwait(false);
|
||||
foreach (var directoryCopy in CopyDirectoryImpl(src, dest, ignore, cancellationToken))
|
||||
await directoryCopy.ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System;
|
||||
#if !DEBUG
|
||||
using System.Linq;
|
||||
#endif
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
@@ -46,21 +45,26 @@ namespace Tgstation.Server.Host.Models
|
||||
/// <inheritdoc />
|
||||
public DbSet<Job> Jobs { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DbSet<ReattachInformation> ReattachInformations { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DbSet<WatchdogReattachInformation> WatchdogReattachInformations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="TestMerge"/>s in the <see cref="DatabaseContext{TParentContext}"/>
|
||||
/// </summary>
|
||||
public DbSet<TestMerge> TestMerges { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DbSet<ReattachInformation> ReattachInformations { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="RevInfoTestMerge"/>s om the <see cref="DatabaseContext{TParentContext}"/>
|
||||
/// </summary>
|
||||
public DbSet<RevInfoTestMerge> RevInfoTestMerges { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public DbSet<WatchdogReattachInformation> WatchdogReattachInformations { get; set; }
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="DatabaseContext{TParentContext}"/>
|
||||
/// </summary>
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The connection string for the <see cref="DatabaseContext{TParentContext}"/>
|
||||
@@ -71,6 +75,7 @@ namespace Tgstation.Server.Host.Models
|
||||
/// The <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext{TParentContext}"/>
|
||||
/// </summary>
|
||||
readonly DatabaseConfiguration databaseConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext{TParentContext}"/>
|
||||
/// </summary>
|
||||
@@ -82,15 +87,18 @@ namespace Tgstation.Server.Host.Models
|
||||
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TParentContext}"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
/// <param name="databaseConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="databaseConfiguration"/></param>
|
||||
/// <param name="databaseSeeder">The value of <see cref="databaseSeeder"/></param>
|
||||
public DatabaseContext(DbContextOptions<TParentContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder) : base(dbContextOptions)
|
||||
/// <param name="logger">The value of <see cref="Logger"/></param>
|
||||
public DatabaseContext(DbContextOptions<TParentContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions)
|
||||
{
|
||||
databaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions));
|
||||
this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder));
|
||||
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
Logger.LogTrace("Building entity framework context...");
|
||||
base.OnModelCreating(modelBuilder);
|
||||
|
||||
var userModel = modelBuilder.Entity<User>();
|
||||
@@ -125,27 +133,40 @@ namespace Tgstation.Server.Host.Models
|
||||
instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
base.OnConfiguring(optionsBuilder);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task Initialize(CancellationToken cancellationToken)
|
||||
{
|
||||
#if DEBUG
|
||||
await Database.EnsureCreatedAsync().ConfigureAwait(false);
|
||||
var wasEmpty = (await Users.CountAsync().ConfigureAwait(false)) == 0;
|
||||
#else
|
||||
var migrations = await Database.GetAppliedMigrationsAsync().ConfigureAwait(false);
|
||||
var wasEmpty = !migrations.Any();
|
||||
await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
|
||||
#endif
|
||||
Logger.LogInformation("Migrating database...");
|
||||
|
||||
var wasEmpty = false;
|
||||
if (!databaseConfiguration.NoMigrations)
|
||||
{
|
||||
Logger.LogWarning("Running in debug mode. Using all or nothing migration strategy!");
|
||||
await Database.EnsureCreatedAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false);
|
||||
wasEmpty = !migrations.Any();
|
||||
await Database.MigrateAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
wasEmpty |= (await Users.CountAsync(cancellationToken).ConfigureAwait(false)) == 0;
|
||||
|
||||
if (wasEmpty)
|
||||
{
|
||||
Logger.LogInformation("Seeding database...");
|
||||
await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false);
|
||||
else if(databaseConfiguration.ResetAdminPassword)
|
||||
await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.LogDebug("No migrations applied!");
|
||||
if (databaseConfiguration.ResetAdminPassword)
|
||||
{
|
||||
Logger.LogWarning("Enabling and resetting admin password due to configuration!");
|
||||
await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Models.Migrations
|
||||
@@ -9,6 +10,6 @@ namespace Tgstation.Server.Host.Models.Migrations
|
||||
sealed class MySqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MySqlDatabaseContext>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public MySqlDatabaseContext CreateDbContext(string[] args) => new MySqlDatabaseContext(new DbContextOptions<MySqlDatabaseContext>(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher<User>())));
|
||||
public MySqlDatabaseContext CreateDbContext(string[] args) => new MySqlDatabaseContext(new DbContextOptions<MySqlDatabaseContext>(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher<User>())), new LoggerFactory().CreateLogger<MySqlDatabaseContext>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Design;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Tgstation.Server.Host.Security;
|
||||
|
||||
namespace Tgstation.Server.Host.Models.Migrations
|
||||
@@ -9,6 +10,6 @@ namespace Tgstation.Server.Host.Models.Migrations
|
||||
sealed class SqlServerDesignTimeDbContextFactory : IDesignTimeDbContextFactory<SqlServerDatabaseContext>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public SqlServerDatabaseContext CreateDbContext(string[] args) => new SqlServerDatabaseContext(new DbContextOptions<SqlServerDatabaseContext>(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher<User>())));
|
||||
public SqlServerDatabaseContext CreateDbContext(string[] args) => new SqlServerDatabaseContext(new DbContextOptions<SqlServerDatabaseContext>(), DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), new DatabaseSeeder(new CryptographySuite(new PasswordHasher<User>())), new LoggerFactory().CreateLogger<SqlServerDatabaseContext>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
|
||||
@@ -15,7 +16,8 @@ namespace Tgstation.Server.Host.Models
|
||||
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
public MySqlDatabaseContext(DbContextOptions<MySqlDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder) : base(dbContextOptions, databaseConfiguration, databaseSeeder)
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
public MySqlDatabaseContext(DbContextOptions<MySqlDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<MySqlDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Tgstation.Server.Host.Configuration;
|
||||
|
||||
@@ -15,7 +16,8 @@ namespace Tgstation.Server.Host.Models
|
||||
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
public SqlServerDatabaseContext(DbContextOptions<SqlServerDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder) : base(dbContextOptions, databaseConfiguration, databaseSeeder)
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
public SqlServerDatabaseContext(DbContextOptions<SqlServerDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqlServerDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -11,22 +11,15 @@ namespace Tgstation.Server.Host.Models
|
||||
/// </summary>
|
||||
sealed class SqliteDatabaseContext : DatabaseContext<SqliteDatabaseContext>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="ILogger"/> for the <see cref="SqliteDatabaseContext"/>
|
||||
/// </summary>
|
||||
readonly ILogger<SqliteDatabaseContext> logger;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="SqliteDatabaseContext"/>
|
||||
/// </summary>
|
||||
/// <param name="dbContextOptions">The <see cref="DbContextOptions{TContext}"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
/// <param name="databaseConfiguration">The <see cref="IOptions{TOptions}"/> of <see cref="DatabaseConfiguration"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
/// <param name="databaseSeeder">The <see cref="IDatabaseSeeder"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public SqliteDatabaseContext(DbContextOptions<SqliteDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqliteDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder)
|
||||
{
|
||||
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="DatabaseContext{TParentContext}"/></param>
|
||||
public SqliteDatabaseContext(DbContextOptions<SqliteDatabaseContext> dbContextOptions, IOptions<DatabaseConfiguration> databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger<SqliteDatabaseContext> logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder options)
|
||||
@@ -35,7 +28,7 @@ namespace Tgstation.Server.Host.Models
|
||||
//on the off chance that connection string is null here we default to a db file next to the executable since this is the default database
|
||||
if (ConnectionString == null)
|
||||
{
|
||||
logger.LogWarning("No database configured! Defaulting to SQLite in the working directory!");
|
||||
Logger.LogWarning("No database configured! Defaulting to SQLite in the working directory!");
|
||||
options.UseSqlite("Data Source=TgsDatabase.db3");
|
||||
}
|
||||
else
|
||||
|
||||
@@ -31,7 +31,7 @@ namespace Tgstation.Server.Host.Security
|
||||
lock (cachedIdentities)
|
||||
{
|
||||
if (cachedIdentities.TryGetValue(user.Id, out var identCache))
|
||||
identCache.Dispose(); //also clears it out
|
||||
identCache.Dispose(); //also clears it out
|
||||
identCache = new IdentityCacheObject(systemIdentity.Clone(), () =>
|
||||
{
|
||||
lock (cachedIdentities)
|
||||
|
||||
@@ -54,6 +54,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="appsettings.Development.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"General": {
|
||||
"DisableFileLogging": true
|
||||
},
|
||||
"Database": {
|
||||
"NoMigrations": true
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
"Console": {
|
||||
"LogLevel": {
|
||||
"Default": "Trace",
|
||||
"Microsoft": "Information"
|
||||
"Microsoft": "Warning"
|
||||
}
|
||||
},
|
||||
"LogLevel": {
|
||||
@@ -29,6 +29,7 @@
|
||||
"UpdatePackageAssetName": "ServerUpdatePackage.zip"
|
||||
},
|
||||
"Database": {
|
||||
"NoMigrations": false,
|
||||
"DatabaseType": "SqlServer",
|
||||
"ResetAdminPassword": false,
|
||||
"ConnectionString": "Data Source=(local);Initial Catalog=TGS;Integrated Security=True"
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
Watchdog performance counters
|
||||
- Remember to catch PlatformNotSupportedException()
|
||||
|
||||
Install as network service instead of LocalSystem if win10 >= Creators update to allow for unprivileged symlinks
|
||||
|
||||
Verify the byond cache folder location on linux
|
||||
|
||||
Don't throw arg null exceptions with null [FromBody]'s, apparently that's allowed, return bad request instead
|
||||
|
||||
Test repo more
|
||||
Test byond
|
||||
Test compile
|
||||
Test watchdog
|
||||
test configuration
|
||||
|
||||
|
||||
Reference in New Issue
Block a user