Merge pull request #561 from Cyberboss/TooMuchStuff

Various things
This commit is contained in:
Jordan Brown
2018-08-07 22:37:31 -04:00
committed by GitHub
58 changed files with 840 additions and 398 deletions
-1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+31 -17
View File
@@ -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
}
}
+2 -1
View File
@@ -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"
-10
View File
@@ -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