diff --git a/build/Version.props b/build/Version.props
index 75afbdc50b..3ec014ea32 100644
--- a/build/Version.props
+++ b/build/Version.props
@@ -3,12 +3,12 @@
- 5.6.0
+ 5.7.0
4.4.0
- 9.8.1
- 10.2.0
- 11.2.1
- 6.1.0
+ 9.9.0
+ 10.3.0
+ 11.3.0
+ 6.2.0
5.4.0
1.2.1
1.2.1
diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm
index 54fc1c8b0a..3744a95a0f 100644
--- a/src/DMAPI/tgs.dm
+++ b/src/DMAPI/tgs.dm
@@ -1,6 +1,6 @@
// tgstation-server DMAPI
-#define TGS_DMAPI_VERSION "6.1.0"
+#define TGS_DMAPI_VERSION "6.2.0"
// All functions and datums outside this document are subject to change with any version and should not be relied on.
@@ -258,6 +258,8 @@
var/help_text = ""
/// If this command should be available to game administrators only
var/admin_only = FALSE
+ /// A subtype of [/datum/tgs_chat_command] that is ignored when enumerating available commands. Use this to create shared base /datums for commands.
+ var/ignore_type
/**
* Process command activation. Should return a [/datum/tgs_message_content] to respond to the issuer with.
diff --git a/src/DMAPI/tgs/v3210/commands.dm b/src/DMAPI/tgs/v3210/commands.dm
index ad698808e2..d9bd287465 100644
--- a/src/DMAPI/tgs/v3210/commands.dm
+++ b/src/DMAPI/tgs/v3210/commands.dm
@@ -10,9 +10,12 @@
var/warned_about_the_dangers_of_robutussin = !warnings_only
for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command)
if(!warned_about_the_dangers_of_robutussin)
- TGS_ERROR_LOG("Custom chat commands in [ApiVersion()] lacks the /datum/tgs_chat_user/sender.channel field!")
+ TGS_WARNING_LOG("Custom chat commands in [ApiVersion()] lacks the /datum/tgs_chat_user/sender.channel field!")
warned_about_the_dangers_of_robutussin = TRUE
var/datum/tgs_chat_command/stc = I
+ if(stc.ignore_type == I)
+ continue
+
var/command_name = initial(stc.name)
if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\""))
if(warnings_only && !warned_command_names[command_name])
diff --git a/src/DMAPI/tgs/v4/commands.dm b/src/DMAPI/tgs/v4/commands.dm
index 8c16da8e23..d6d3d718d4 100644
--- a/src/DMAPI/tgs/v4/commands.dm
+++ b/src/DMAPI/tgs/v4/commands.dm
@@ -3,6 +3,9 @@
custom_commands = list()
for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command)
var/datum/tgs_chat_command/stc = new I
+ if(stc.ignore_type == I)
+ continue
+
var/command_name = stc.name
if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\""))
TGS_ERROR_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!")
diff --git a/src/DMAPI/tgs/v5/commands.dm b/src/DMAPI/tgs/v5/commands.dm
index 48dfd1c4d7..71ede42c3b 100644
--- a/src/DMAPI/tgs/v5/commands.dm
+++ b/src/DMAPI/tgs/v5/commands.dm
@@ -3,14 +3,17 @@
custom_commands = list()
for(var/I in typesof(/datum/tgs_chat_command) - /datum/tgs_chat_command)
var/datum/tgs_chat_command/stc = new I
+ if(stc.ignore_type == I)
+ continue
+
var/command_name = stc.name
if(!command_name || findtext(command_name, " ") || findtext(command_name, "'") || findtext(command_name, "\""))
- TGS_WARNING_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!")
+ TGS_ERROR_LOG("Custom command [command_name] ([I]) can't be used as it is empty or contains illegal characters!")
continue
if(results[command_name])
var/datum/other = custom_commands[command_name]
- TGS_WARNING_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!")
+ TGS_ERROR_LOG("Custom commands [other.type] and [I] have the same name (\"[command_name]\"), only [other.type] will be available!")
continue
results += list(list(DMAPI5_CUSTOM_CHAT_COMMAND_NAME = command_name, DMAPI5_CUSTOM_CHAT_COMMAND_HELP_TEXT = stc.help_text, DMAPI5_CUSTOM_CHAT_COMMAND_ADMIN_ONLY = stc.admin_only))
custom_commands[command_name] = stc
diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs
index 4890b211a1..7693ecf961 100644
--- a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs
+++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs
@@ -84,6 +84,13 @@ namespace Tgstation.Server.Api.Models.Internal
[StringLength(Limits.MaximumStringLength)]
public string? AdditionalParameters { get; set; }
+ ///
+ /// If process output/error text should be logged.
+ ///
+ [Required]
+ [ResponseOptions]
+ public bool? LogOutput { get; set; }
+
///
/// Check if we match a given set of . is excluded.
///
@@ -100,7 +107,8 @@ namespace Tgstation.Server.Api.Models.Internal
&& Port == otherParameters.Port
&& TopicRequestTimeout == otherParameters.TopicRequestTimeout
&& AdditionalParameters == otherParameters.AdditionalParameters
- && StartProfiler == otherParameters.StartProfiler; // We intentionally don't check StartupTimeout, heartbeat seconds, or heartbeat dump as they don't matter in terms of the watchdog
+ && StartProfiler == otherParameters.StartProfiler
+ && LogOutput == otherParameters.LogOutput; // We intentionally don't check StartupTimeout, heartbeat seconds, or heartbeat dump as they don't matter in terms of the watchdog
}
}
}
diff --git a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs
index 5fc6b54394..a89f20d9e0 100644
--- a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs
+++ b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs
@@ -102,5 +102,10 @@ namespace Tgstation.Server.Api.Rights
/// User can change
///
SetProfiler = 131072,
+
+ ///
+ /// User can change
+ ///
+ SetLogOutput = 262144,
}
}
diff --git a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs
index 03d6b9389f..76d47e26f6 100644
--- a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs
+++ b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs
@@ -187,7 +187,7 @@ namespace Tgstation.Server.Host.Components.Byond
try
{
// noShellExecute because we aren't doing runas shennanigans
- using var directXInstaller = processExecutor.LaunchProcess(
+ await using var directXInstaller = await processExecutor.LaunchProcess(
IOManager.ConcatPath(rbdx, "DXSETUP.exe"),
rbdx,
"/silent",
@@ -228,13 +228,12 @@ namespace Tgstation.Server.Host.Components.Byond
Logger.LogInformation("Adding Windows Firewall exception for {path}...", dreamDaemonPath);
try
{
- using var netshProcess = processExecutor.LaunchProcess(
+ await using var netshProcess = await processExecutor.LaunchProcess(
"netsh.exe",
IOManager.ResolvePath(),
$"advfirewall firewall add rule name=\"TGS DreamDaemon\" program=\"{dreamDaemonPath}\" protocol=tcp dir=in enable=yes action=allow",
- true,
- true,
- true);
+ readStandardHandles: true,
+ noShellExecute: true);
int exitCode;
using (cancellationToken.Register(() => netshProcess.Terminate()))
diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
index ff50fdcc81..8db34d95a8 100644
--- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
+++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs
@@ -235,6 +235,7 @@ namespace Tgstation.Server.Host.Components.Deployment
.Select(x => new Models.DreamDaemonSettings
{
StartupTimeout = x.StartupTimeout,
+ LogOutput = x.LogOutput,
})
.FirstOrDefaultAsync(cancellationToken)
;
@@ -325,7 +326,7 @@ namespace Tgstation.Server.Host.Components.Deployment
compileJob = await Compile(
revInfo,
dreamMakerSettings,
- ddSettings.StartupTimeout.Value,
+ ddSettings,
repo,
remoteDeploymentManager,
progressReporter,
@@ -462,7 +463,7 @@ namespace Tgstation.Server.Host.Components.Deployment
///
/// The .
/// The .
- /// The API validation timeout.
+ /// The .
/// The .
/// The .
/// The to report progress of the operation.
@@ -473,7 +474,7 @@ namespace Tgstation.Server.Host.Components.Deployment
async Task Compile(
Models.RevisionInformation revisionInformation,
Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
- uint apiValidateTimeout,
+ DreamDaemonLaunchParameters launchParameters,
IRepository repository,
IRemoteDeploymentManager remoteDeploymentManager,
JobProgressReporter progressReporter,
@@ -525,10 +526,10 @@ namespace Tgstation.Server.Host.Components.Deployment
await RunCompileJob(
job,
dreamMakerSettings,
+ launchParameters,
byondLock,
repository,
remoteDeploymentManager,
- apiValidateTimeout,
combinedTokenSource.Token)
;
}
@@ -559,19 +560,19 @@ namespace Tgstation.Server.Host.Components.Deployment
///
/// The to run and populate.
/// The to use.
+ /// The to use.
/// The to use.
/// The to use.
/// The to use.
- /// The timeout for validating the DMAPI.
/// The for the operation.
/// A representing the running operation.
async Task RunCompileJob(
Models.CompileJob job,
Api.Models.Internal.DreamMakerSettings dreamMakerSettings,
+ DreamDaemonLaunchParameters launchParameters,
IByondExecutableLock byondLock,
IRepository repository,
IRemoteDeploymentManager remoteDeploymentManager,
- uint apiValidateTimeout,
CancellationToken cancellationToken)
{
var outputDirectory = job.DirectoryName.ToString();
@@ -655,14 +656,14 @@ namespace Tgstation.Server.Host.Components.Deployment
currentStage = "Validating DMAPI";
await VerifyApi(
- apiValidateTimeout,
+ launchParameters.StartupTimeout.Value,
dreamMakerSettings.ApiValidationSecurityLevel.Value,
job,
byondLock,
dreamMakerSettings.ApiValidationPort.Value,
dreamMakerSettings.RequireDMApiValidation.Value,
- cancellationToken)
- ;
+ launchParameters.LogOutput.Value,
+ cancellationToken);
}
catch (JobException)
{
@@ -717,9 +718,8 @@ namespace Tgstation.Server.Host.Components.Deployment
/// A representing the running operation.
async Task ProgressTask(JobProgressReporter progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
{
- var noEstimate = !estimatedDuration.HasValue;
progressReporter.StageName = currentStage;
- double? lastReport = noEstimate ? null : 0;
+ double? lastReport = estimatedDuration.HasValue ? 0 : null;
progressReporter.ReportProgress(lastReport);
var minimumSleepInterval = TimeSpan.FromMilliseconds(250);
@@ -738,25 +738,31 @@ namespace Tgstation.Server.Host.Components.Deployment
{
for (var iteration = 0; iteration < (estimatedDuration.HasValue ? 99 : Int32.MaxValue); ++iteration)
{
- var nextInterval = DateTimeOffset.UtcNow + sleepInterval;
- do
+ if (estimatedDuration.HasValue)
{
- var remainingSleepThisInterval = nextInterval - DateTimeOffset.UtcNow;
- var nextSleepSpan = remainingSleepThisInterval < minimumSleepInterval ? remainingSleepThisInterval : minimumSleepInterval;
+ var nextInterval = DateTimeOffset.UtcNow + sleepInterval;
+ do
+ {
+ var remainingSleepThisInterval = nextInterval - DateTimeOffset.UtcNow;
+ var nextSleepSpan = remainingSleepThisInterval < minimumSleepInterval ? remainingSleepThisInterval : minimumSleepInterval;
- await Task.Delay(nextSleepSpan, cancellationToken);
- progressReporter.StageName = currentStage;
- progressReporter.ReportProgress(lastReport);
+ await Task.Delay(nextSleepSpan, cancellationToken);
+ progressReporter.StageName = currentStage;
+ progressReporter.ReportProgress(lastReport);
+ }
+ while (DateTimeOffset.UtcNow < nextInterval);
}
- while (DateTimeOffset.UtcNow < nextInterval);
+ else
+ await Task.Delay(minimumSleepInterval, cancellationToken);
progressReporter.StageName = currentStage;
- lastReport = noEstimate ? null : sleepInterval * (iteration + 1) / estimatedDuration.Value;
+ lastReport = estimatedDuration.HasValue ? sleepInterval * (iteration + 1) / estimatedDuration.Value : null;
progressReporter.ReportProgress(lastReport);
}
}
- catch (OperationCanceledException)
+ catch (OperationCanceledException ex)
{
+ logger.LogTrace(ex, "ProgressTask aborted.");
}
}
@@ -769,6 +775,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// The current .
/// The port to use for API validation.
/// If the API validation is required to complete the deployment.
+ /// If output should be logged to the DreamDaemon Diagnostics folder.
/// The for the operation.
/// A representing the running operation.
async Task VerifyApi(
@@ -778,6 +785,7 @@ namespace Tgstation.Server.Host.Components.Deployment
IByondExecutableLock byondLock,
ushort portToUse,
bool requireValidate,
+ bool logOutput,
CancellationToken cancellationToken)
{
logger.LogTrace("Verifying {0}DMAPI...", requireValidate ? "required " : String.Empty);
@@ -791,6 +799,7 @@ namespace Tgstation.Server.Host.Components.Deployment
TopicRequestTimeout = 0, // not used
HeartbeatSeconds = 0, // not used
StartProfiler = false,
+ LogOutput = logOutput,
};
job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider
@@ -854,14 +863,13 @@ namespace Tgstation.Server.Host.Components.Deployment
/// A representing the running operation.
async Task RunDreamMaker(string dreamMakerPath, Models.CompileJob job, CancellationToken cancellationToken)
{
- using var dm = processExecutor.LaunchProcess(
+ await using var dm = await processExecutor.LaunchProcess(
dreamMakerPath,
ioManager.ResolvePath(
job.DirectoryName.ToString()),
$"-clean {job.DmeName}.{DmeExtension}",
- true,
- true,
- true);
+ readStandardHandles: true,
+ noShellExecute: true);
if (sessionConfiguration.LowPriorityDeploymentProcesses)
dm.AdjustPriority(false);
diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs
index 1bb8121c9d..b1d9764321 100644
--- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs
+++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs
@@ -271,6 +271,7 @@ namespace Tgstation.Server.Host.Components
cryptographySuite,
assemblyInformationProvider,
gameIoManager,
+ diagnosticsIOManager,
chatManager,
networkPromptReaper,
platformIdentifier,
@@ -308,10 +309,11 @@ namespace Tgstation.Server.Host.Components
remoteDeploymentManagerFactory,
metadata,
metadata.DreamDaemonSettings);
- eventConsumer.SetWatchdog(watchdog);
- commandFactory.SetWatchdog(watchdog);
try
{
+ eventConsumer.SetWatchdog(watchdog);
+ commandFactory.SetWatchdog(watchdog);
+
Instance instance = null;
var dreamMaker = new DreamMaker(
byond,
diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
index 21bdc758e2..37a0a6ac64 100644
--- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs
+++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs
@@ -275,7 +275,7 @@ namespace Tgstation.Server.Host.Components.Session
byondLock.Dispose();
}
- process.Dispose();
+ await process.DisposeAsync();
bridgeRegistration?.Dispose();
ReattachInformation.Dmb?.Dispose(); // will be null when released
chatTrackingContext.Dispose();
@@ -656,8 +656,8 @@ namespace Tgstation.Server.Host.Components.Session
var result = new LaunchResult
{
- ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime : null,
- StartupTime = startupTask.IsCompleted ? (TimeSpan?)(DateTimeOffset.UtcNow - startTime) : null,
+ ExitCode = process.Lifetime.IsCompleted ? await process.Lifetime : null,
+ StartupTime = startupTask.IsCompleted ? (DateTimeOffset.UtcNow - startTime) : null,
};
logger.LogTrace("Launch result: {0}", result);
diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs
index e938adeb17..8b95c3dc05 100644
--- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs
+++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs
@@ -6,6 +6,8 @@ using System.Text;
using System.Threading;
using System.Threading.Tasks;
+using Byond.TopicSender;
+
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api;
@@ -30,6 +32,11 @@ namespace Tgstation.Server.Host.Components.Session
///
sealed class SessionControllerFactory : ISessionControllerFactory
{
+ ///
+ /// Path in Diagnostics folder to DreamDaemon logs.
+ ///
+ const string DreamDaemonLogsPath = "DreamDaemonLogs";
+
///
/// The for the .
///
@@ -56,9 +63,14 @@ namespace Tgstation.Server.Host.Components.Session
readonly IAssemblyInformationProvider assemblyInformationProvider;
///
- /// The for the .
+ /// The for the Game directory.
///
- readonly IIOManager ioManager;
+ readonly IIOManager gameIOManager;
+
+ ///
+ /// The for the Diagnostics directory.
+ ///
+ readonly IIOManager diagnosticsIOManager;
///
/// The for the .
@@ -168,7 +180,8 @@ namespace Tgstation.Server.Host.Components.Session
/// The value of .
/// The value of .
/// The value of .
- /// The value of .
+ /// The value of .
+ /// The value of .
/// The value of .
/// The value of .
/// The value of .
@@ -184,7 +197,8 @@ namespace Tgstation.Server.Host.Components.Session
ITopicClientFactory topicClientFactory,
ICryptographySuite cryptographySuite,
IAssemblyInformationProvider assemblyInformationProvider,
- IIOManager ioManager,
+ IIOManager gameIOManager,
+ IIOManager diagnosticsIOManager,
IChatManager chat,
INetworkPromptReaper networkPromptReaper,
IPlatformIdentifier platformIdentifier,
@@ -201,7 +215,8 @@ namespace Tgstation.Server.Host.Components.Session
this.topicClientFactory = topicClientFactory ?? throw new ArgumentNullException(nameof(topicClientFactory));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
- this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
+ this.gameIOManager = gameIOManager ?? throw new ArgumentNullException(nameof(gameIOManager));
+ this.diagnosticsIOManager = diagnosticsIOManager ?? throw new ArgumentNullException(nameof(diagnosticsIOManager));
this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
@@ -215,7 +230,6 @@ namespace Tgstation.Server.Host.Components.Session
}
///
-#pragma warning disable CA1506 // TODO: Decomplexify
public async Task LaunchNew(
IDmbProvider dmbProvider,
IByondExecutableLock currentByondLock,
@@ -248,125 +262,65 @@ namespace Tgstation.Server.Host.Components.Session
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel));
}
- var chatTrackingContext = chat.CreateTrackingContext();
+ // get the byond lock
+ var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken);
try
{
- // get the byond lock
- var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken);
+ logger.LogDebug(
+ "Launching session with CompileJob {compileJobId}...",
+ dmbProvider.CompileJob.Id);
+
+ if (launchParameters.SecurityLevel == DreamDaemonSecurity.Trusted)
+ await byondLock.TrustDmbPath(gameIOManager.ConcatPath(dmbProvider.Directory, dmbProvider.DmbName), cancellationToken);
+
+ PortBindTest(launchParameters.Port.Value);
+ await CheckPagerIsNotRunning(cancellationToken);
+
+ string outputFilePath = null;
+ if (launchParameters.LogOutput.Value)
+ {
+ await diagnosticsIOManager.CreateDirectory(DreamDaemonLogsPath, cancellationToken);
+ var now = DateTimeOffset.UtcNow;
+ outputFilePath = diagnosticsIOManager.ResolvePath(
+ diagnosticsIOManager.ConcatPath(
+ DreamDaemonLogsPath,
+ $"dd-utc-{DateTimeOffset.UtcNow.ToString("yyyy-MM-dd-HH-mm-ss", CultureInfo.InvariantCulture)}{(apiValidate ? "-dmapi" : String.Empty)}.log"));
+
+ logger.LogInformation("Logging DreamDaemon output to {path}...", outputFilePath);
+ }
+ else if (!byondLock.SupportsCli)
+ outputFilePath = gameIOManager.ConcatPath(dmbProvider.Directory, $"{Guid.NewGuid()}.dd.log");
+
+ var accessIdentifier = cryptographySuite.GetSecureString();
+
+ var byondTopicSender = topicClientFactory.CreateTopicClient(
+ TimeSpan.FromMilliseconds(
+ launchParameters.TopicRequestTimeout.Value));
+
+ if (!apiValidate && dmbProvider.CompileJob.DMApiVersion == null)
+ logger.LogDebug("Session will have no DMAPI support!");
+
+ // launch dd
+ var process = await CreateDreamDaemonProcess(
+ dmbProvider,
+ byondTopicSender,
+ byondLock,
+ launchParameters,
+ accessIdentifier,
+ outputFilePath,
+ apiValidate,
+ cancellationToken);
+
try
{
- logger.LogDebug(
- "Launching session with CompileJob {0}...",
- dmbProvider.CompileJob.Id);
-
- if (launchParameters.SecurityLevel == DreamDaemonSecurity.Trusted)
- await byondLock.TrustDmbPath(ioManager.ConcatPath(dmbProvider.Directory, dmbProvider.DmbName), cancellationToken);
-
- PortBindTest(launchParameters.Port.Value);
- await CheckPagerIsNotRunning(cancellationToken);
-
- var accessIdentifier = cryptographySuite.GetSecureString();
-
- var byondTopicSender = topicClientFactory.CreateTopicClient(
- TimeSpan.FromMilliseconds(
- launchParameters.TopicRequestTimeout.Value));
-
- // set command line options
- // more sanitization here cause it uses the same scheme
- var parameters = $"{DMApiConstants.ParamApiVersion}={byondTopicSender.SanitizeString(DMApiConstants.InteropVersion.Semver().ToString())}&{byondTopicSender.SanitizeString(DMApiConstants.ParamServerPort)}={serverPortProvider.HttpApiPort}&{byondTopicSender.SanitizeString(DMApiConstants.ParamAccessIdentifier)}={byondTopicSender.SanitizeString(accessIdentifier)}";
-
- if (!String.IsNullOrEmpty(launchParameters.AdditionalParameters))
- parameters = $"{parameters}&{launchParameters.AdditionalParameters}";
-
- // important to run on all ports to allow port changing
- Guid? logFileGuid = null;
- var arguments = String.Format(
- CultureInfo.InvariantCulture,
- "{0} -port {1} -ports 1-65535 {2}-close -verbose -{3} -{4}{5}{6} -params \"{7}\"",
- dmbProvider.DmbName,
- launchParameters.Port.Value,
- launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
- SecurityWord(launchParameters.SecurityLevel.Value),
- VisibilityWord(launchParameters.Visibility.Value),
- !byondLock.SupportsCli
- ? $" -logself -log {logFileGuid = Guid.NewGuid()}"
- : !platformIdentifier.IsWindows // Just use stdout on if CLI is supported
- ? " -logself"
- : String.Empty, // Windows doesn't output anything to dd.exe if -logself is set?
- launchParameters.StartProfiler.Value
- ? " -profile"
- : String.Empty,
- parameters);
-
- if (!apiValidate && dmbProvider.CompileJob.DMApiVersion == null)
- logger.LogDebug("Session will have no DMAPI support!");
-
- // launch dd
- var process = processExecutor.LaunchProcess(
- byondLock.DreamDaemonPath,
- dmbProvider.Directory,
- arguments,
- byondLock.SupportsCli,
- byondLock.SupportsCli,
- byondLock.SupportsCli);
-
- var cliSupported = byondLock.SupportsCli;
- async Task GetDDOutput()
- {
- // DCT x2: None available
- if (cliSupported)
- return await process.GetCombinedOutput(default);
-
- var logFilePath = ioManager.ConcatPath(dmbProvider.Directory, logFileGuid.ToString());
- try
- {
- var dreamDaemonLogBytes = await ioManager.ReadAllBytes(
- logFilePath,
- default)
- ;
-
- return Encoding.UTF8.GetString(dreamDaemonLogBytes);
- }
- finally
- {
- try
- {
- // DCT: No token available
- await ioManager.DeleteFile(logFilePath, default);
- }
- catch (Exception ex)
- {
- logger.LogWarning(ex, "Failed to delete DreamDaemon log file {0}!", logFilePath);
- }
- }
- }
-
- // Log DD output
- async Task PostLifetime()
- {
- try
- {
- var ddOutput = await GetDDOutput();
- logger.LogTrace(
- "DreamDaemon Output:{0}{1}",
- Environment.NewLine,
- ddOutput);
- }
- catch (Exception ex)
- {
- logger.LogWarning(ex, "Error reading DreamDaemon output!");
- }
- }
+ var chatTrackingContext = chat.CreateTrackingContext();
try
{
- networkPromptReaper.RegisterProcess(process);
-
var runtimeInformation = CreateRuntimeInformation(
dmbProvider,
chatTrackingContext,
- launchParameters.SecurityLevel.Value,
- launchParameters.Visibility.Value,
+ launchParameters,
apiValidate);
var reattachInformation = new ReattachInformation(
@@ -387,56 +341,38 @@ namespace Tgstation.Server.Host.Components.Session
chat,
assemblyInformationProvider,
loggerFactory.CreateLogger(),
- PostLifetime,
+ () => !launchParameters.LogOutput.Value
+ ? LogDDOutput(process, outputFilePath, byondLock.SupportsCli, default) // DCT: None available
+ : Task.CompletedTask,
launchParameters.StartupTimeout,
false,
apiValidate);
- if (apiValidate)
- {
- if (sessionConfiguration.HighPriorityLiveDreamDaemon)
- process.AdjustPriority(true);
- }
- else if (sessionConfiguration.LowPriorityDeploymentProcesses)
- process.AdjustPriority(false);
-
- // If this isnt a staging DD (From a Deployment), fire off an event
- if (!apiValidate)
- await eventConsumer.HandleEvent(
- EventType.DreamDaemonLaunch,
- new List
- {
- process.Id.ToString(CultureInfo.InvariantCulture),
- },
- cancellationToken)
- ;
-
return sessionController;
}
catch
{
- using (process)
- {
- process.Terminate();
- await process.Lifetime;
- throw;
- }
+ chatTrackingContext.Dispose();
+ throw;
}
}
catch
{
- if (currentByondLock == null)
- byondLock.Dispose();
- throw;
+ await using (process)
+ {
+ process.Terminate();
+ await process.Lifetime;
+ throw;
+ }
}
}
catch
{
- chatTrackingContext.Dispose();
+ if (currentByondLock == null)
+ byondLock.Dispose();
throw;
}
}
-#pragma warning restore CA1506
///
public async Task Reattach(
@@ -448,30 +384,30 @@ namespace Tgstation.Server.Host.Components.Session
logger.LogTrace("Begin session reattach...");
var byondTopicSender = topicClientFactory.CreateTopicClient(reattachInformation.TopicRequestTimeout);
- var chatTrackingContext = chat.CreateTrackingContext();
+ var byondLock = await byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), cancellationToken);
+
try
{
- var byondLock = await byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), cancellationToken);
+ logger.LogDebug(
+ "Attaching to session PID: {0}, CompileJob: {1}...",
+ reattachInformation.ProcessId,
+ reattachInformation.Dmb.CompileJob.Id);
+
+ var process = processExecutor.GetProcess(reattachInformation.ProcessId);
+ if (process == null)
+ return null;
try
{
- logger.LogDebug(
- "Attaching to session PID: {0}, CompileJob: {1}...",
- reattachInformation.ProcessId,
- reattachInformation.Dmb.CompileJob.Id);
-
- var process = processExecutor.GetProcess(reattachInformation.ProcessId);
- if (process == null)
- return null;
+ networkPromptReaper.RegisterProcess(process);
+ var chatTrackingContext = chat.CreateTrackingContext();
try
{
- networkPromptReaper.RegisterProcess(process);
var runtimeInformation = CreateRuntimeInformation(
reattachInformation.Dmb,
chatTrackingContext,
null,
- null,
false);
reattachInformation.SetRuntimeInformation(runtimeInformation);
@@ -497,19 +433,160 @@ namespace Tgstation.Server.Host.Components.Session
return controller;
}
- finally
+ catch
{
- process?.Dispose();
+ chatTrackingContext.Dispose();
+ throw;
}
}
- finally
+ catch
{
- byondLock?.Dispose();
+ await process.DisposeAsync();
+ throw;
}
}
- finally
+ catch
{
- chatTrackingContext?.Dispose();
+ byondLock.Dispose();
+ throw;
+ }
+ }
+
+ ///
+ /// Creates the DreamDaemon .
+ ///
+ /// The .
+ /// The to use for sanitization.
+ /// The .
+ /// The .
+ /// The secure string to use for the session.
+ /// The path to log DreamDaemon output to.
+ /// If we are only validating the DMAPI then exiting.
+ /// The for the operation.
+ /// A resulting in the DreamDaemon .
+ async Task CreateDreamDaemonProcess(
+ IDmbProvider dmbProvider,
+ ITopicClient byondTopicSender,
+ IByondExecutableLock byondLock,
+ DreamDaemonLaunchParameters launchParameters,
+ string accessIdentifier,
+ string logFilePath,
+ bool apiValidate,
+ CancellationToken cancellationToken)
+ {
+ // set command line options
+ // more sanitization here cause it uses the same scheme
+ var parameters = $"{DMApiConstants.ParamApiVersion}={byondTopicSender.SanitizeString(DMApiConstants.InteropVersion.Semver().ToString())}&{byondTopicSender.SanitizeString(DMApiConstants.ParamServerPort)}={serverPortProvider.HttpApiPort}&{byondTopicSender.SanitizeString(DMApiConstants.ParamAccessIdentifier)}={byondTopicSender.SanitizeString(accessIdentifier)}";
+
+ if (!String.IsNullOrEmpty(launchParameters.AdditionalParameters))
+ parameters = $"{parameters}&{launchParameters.AdditionalParameters}";
+
+ // important to run on all ports to allow port changing
+ var arguments = String.Format(
+ CultureInfo.InvariantCulture,
+ "{0} -port {1} -ports 1-65535 {2}-close -verbose -{3} -{4}{5}{6} -params \"{7}\"",
+ dmbProvider.DmbName,
+ launchParameters.Port.Value,
+ launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
+ SecurityWord(launchParameters.SecurityLevel.Value),
+ VisibilityWord(launchParameters.Visibility.Value),
+ !byondLock.SupportsCli
+ ? $" -logself -log {logFilePath}"
+ : !platformIdentifier.IsWindows // Just use stdout on if CLI is supported
+ ? " -logself"
+ : String.Empty, // Windows doesn't output anything to dd.exe if -logself is set?
+ launchParameters.StartProfiler.Value
+ ? " -profile"
+ : String.Empty,
+ parameters);
+
+ var process = await processExecutor.LaunchProcess(
+ byondLock.DreamDaemonPath,
+ dmbProvider.Directory,
+ arguments,
+ logFilePath,
+ byondLock.SupportsCli,
+ true);
+
+ try
+ {
+ if (apiValidate)
+ {
+ if (sessionConfiguration.HighPriorityLiveDreamDaemon)
+ process.AdjustPriority(true);
+ }
+ else if (sessionConfiguration.LowPriorityDeploymentProcesses)
+ process.AdjustPriority(false);
+
+ networkPromptReaper.RegisterProcess(process);
+
+ // If this isnt a staging DD (From a Deployment), fire off an event
+ if (!apiValidate)
+ await eventConsumer.HandleEvent(
+ EventType.DreamDaemonLaunch,
+ new List
+ {
+ process.Id.ToString(CultureInfo.InvariantCulture),
+ },
+ cancellationToken);
+
+ return process;
+ }
+ catch
+ {
+ await using (process)
+ {
+ process.Terminate();
+ await process.Lifetime;
+ throw;
+ }
+ }
+ }
+
+ ///
+ /// Attempts to log DreamDaemon output.
+ ///
+ /// The DreamDaemon .
+ /// The path to the DreamDaemon log file. Will be deleted.
+ /// If DreamDaemon was launched with CLI capabilities.
+ /// The for the operation.
+ /// A representing the running operation.
+ async Task LogDDOutput(IProcess process, string outputFilePath, bool cliSupported, CancellationToken cancellationToken)
+ {
+ try
+ {
+ string ddOutput;
+ if (cliSupported)
+ ddOutput = await process.GetCombinedOutput(cancellationToken);
+ else
+ try
+ {
+ var dreamDaemonLogBytes = await gameIOManager.ReadAllBytes(
+ outputFilePath,
+ cancellationToken);
+
+ ddOutput = Encoding.UTF8.GetString(dreamDaemonLogBytes);
+ }
+ finally
+ {
+ try
+ {
+ await gameIOManager.DeleteFile(outputFilePath, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Failed to delete DreamDaemon log file {outputFilePath}!", outputFilePath);
+ }
+ }
+
+ logger.LogTrace(
+ "DreamDaemon Output:{newLine}{output}",
+ Environment.NewLine,
+ ddOutput);
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Error reading DreamDaemon output!");
}
}
@@ -518,23 +595,21 @@ namespace Tgstation.Server.Host.Components.Session
///
/// The .
/// The .
- /// The level if any.
- /// The if any.
+ /// The if any.
/// The value of .
/// A new class.
RuntimeInformation CreateRuntimeInformation(
IDmbProvider dmbProvider,
IChatTrackingContext chatTrackingContext,
- DreamDaemonSecurity? securityLevel,
- DreamDaemonVisibility? visibility,
+ DreamDaemonLaunchParameters launchParameters,
bool apiValidateOnly)
=> new RuntimeInformation(
chatTrackingContext,
dmbProvider,
assemblyInformationProvider.Version,
instance.Name,
- securityLevel,
- visibility,
+ launchParameters?.SecurityLevel,
+ launchParameters?.Visibility,
serverPortProvider.HttpApiPort,
apiValidateOnly);
@@ -548,12 +623,12 @@ namespace Tgstation.Server.Host.Components.Session
if (!platformIdentifier.IsWindows)
return;
- using var otherProcess = processExecutor.GetProcessByName("byond");
+ await using var otherProcess = processExecutor.GetProcessByName("byond");
if (otherProcess == null)
return;
var otherUsernameTask = otherProcess.GetExecutingUsername(cancellationToken);
- using var ourProcess = processExecutor.GetCurrentProcess();
+ await using var ourProcess = processExecutor.GetCurrentProcess();
var ourUserName = await ourProcess.GetExecutingUsername(cancellationToken);
var otherUserName = await otherUsernameTask;
diff --git a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs
index d51bf8da5c..c6701c4ef0 100644
--- a/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs
+++ b/src/Tgstation.Server.Host/Components/Session/SessionPersistor.cs
@@ -99,7 +99,7 @@ namespace Tgstation.Server.Host.Components.Session
{
try
{
- using var process = processExecutor.GetProcess(reattachInfo.ProcessId);
+ await using var process = processExecutor.GetProcess(reattachInfo.ProcessId);
if (process != null)
{
if (reattachInfo == result)
diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
index 7b84ce8ebf..9ed5959137 100644
--- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
+++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
@@ -562,7 +562,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
foreach (var scriptFile in scriptFiles)
{
logger.LogTrace("Running event script {scriptFile}...", scriptFile);
- using (var script = processExecutor.LaunchProcess(
+ await using (var script = await processExecutor.LaunchProcess(
ioManager.ConcatPath(resolvedScriptsDir, scriptFile),
resolvedScriptsDir,
String.Join(
@@ -576,9 +576,8 @@ namespace Tgstation.Server.Host.Components.StaticFiles
return $"\"{arg}\"";
})),
- true,
- true,
- true))
+ readStandardHandles: true,
+ noShellExecute: true))
using (cancellationToken.Register(() => script.Terminate()))
{
var exitCode = await script.Lifetime;
diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
index 60abece7b8..c4af8f06d1 100644
--- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
+++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs
@@ -150,7 +150,8 @@ namespace Tgstation.Server.Host.Controllers
| DreamDaemonRights.SetTopicTimeout
| DreamDaemonRights.SetAdditionalParameters
| DreamDaemonRights.SetVisibility
- | DreamDaemonRights.SetProfiler)]
+ | DreamDaemonRights.SetProfiler
+ | DreamDaemonRights.SetLogOutput)]
[ProducesResponseType(typeof(DreamDaemonResponse), 200)]
[ProducesResponseType(typeof(ErrorMessageResponse), 410)]
#pragma warning disable CA1502 // TODO: Decomplexify
@@ -224,7 +225,8 @@ namespace Tgstation.Server.Host.Controllers
|| CheckModified(x => x.DumpOnHeartbeatRestart, DreamDaemonRights.CreateDump)
|| CheckModified(x => x.TopicRequestTimeout, DreamDaemonRights.SetTopicTimeout)
|| CheckModified(x => x.AdditionalParameters, DreamDaemonRights.SetAdditionalParameters)
- || CheckModified(x => x.StartProfiler, DreamDaemonRights.SetProfiler))
+ || CheckModified(x => x.StartProfiler, DreamDaemonRights.SetProfiler)
+ || CheckModified(x => x.LogOutput, DreamDaemonRights.SetLogOutput))
return Forbid();
await DatabaseContext.Save(cancellationToken);
@@ -365,6 +367,7 @@ namespace Tgstation.Server.Host.Controllers
result.TopicRequestTimeout = settings.TopicRequestTimeout.Value;
result.AdditionalParameters = settings.AdditionalParameters;
result.StartProfiler = settings.StartProfiler;
+ result.LogOutput = settings.LogOutput;
}
if (revision)
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
index b10390820b..359ed590fe 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
@@ -725,6 +725,7 @@ namespace Tgstation.Server.Host.Controllers
TopicRequestTimeout = generalConfiguration.ByondTopicTimeout,
AdditionalParameters = String.Empty,
StartProfiler = false,
+ LogOutput = false,
},
DreamMakerSettings = new DreamMakerSettings
{
diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs
index 93a68f8c8c..b7a197143d 100644
--- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs
+++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs
@@ -379,22 +379,22 @@ namespace Tgstation.Server.Host.Database
///
/// Used by unit tests to remind us to setup the correct MSSQL migration downgrades.
///
- internal static readonly Type MSLatestMigration = typeof(MSAddProfiler);
+ internal static readonly Type MSLatestMigration = typeof(MSAddDreamDaemonLogOutput);
///
/// Used by unit tests to remind us to setup the correct MYSQL migration downgrades.
///
- internal static readonly Type MYLatestMigration = typeof(MYAddProfiler);
+ internal static readonly Type MYLatestMigration = typeof(MYAddDreamDaemonLogOutput);
///
/// Used by unit tests to remind us to setup the correct PostgresSQL migration downgrades.
///
- internal static readonly Type PGLatestMigration = typeof(PGAddProfiler);
+ internal static readonly Type PGLatestMigration = typeof(PGAddDreamDaemonLogOutput);
///
/// Used by unit tests to remind us to setup the correct SQLite migration downgrades.
///
- internal static readonly Type SLLatestMigration = typeof(SLAddProfiler);
+ internal static readonly Type SLLatestMigration = typeof(SLAddDreamDaemonLogOutput);
///
#pragma warning disable CA1502 // Cyclomatic complexity
@@ -425,6 +425,15 @@ namespace Tgstation.Server.Host.Database
string BadDatabaseType() => throw new ArgumentException($"Invalid DatabaseType: {currentDatabaseType}", nameof(currentDatabaseType));
+ if (targetVersion < new Version(5, 7, 0))
+ targetMigration = currentDatabaseType switch
+ {
+ DatabaseType.MySql => nameof(MYAddProfiler),
+ DatabaseType.PostgresSql => nameof(PGAddProfiler),
+ DatabaseType.SqlServer => nameof(MSAddProfiler),
+ DatabaseType.Sqlite => nameof(SLAddProfiler),
+ _ => BadDatabaseType(),
+ };
if (targetVersion < new Version(4, 19, 0))
targetMigration = currentDatabaseType switch
{
diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.Designer.cs
new file mode 100644
index 0000000000..02be0acad1
--- /dev/null
+++ b/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.Designer.cs
@@ -0,0 +1,1054 @@
+//
+using System;
+
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Tgstation.Server.Host.Database.Migrations
+{
+ [DbContext(typeof(SqlServerDatabaseContext))]
+ [Migration("20230331220749_MSAddDreamDaemonLogOutput")]
+ partial class MSAddDreamDaemonLogOutput
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.15")
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder, 1L, 1);
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("ChannelLimit")
+ .HasColumnType("int");
+
+ b.Property("ConnectionString")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Enabled")
+ .HasColumnType("bit");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Provider")
+ .HasColumnType("int");
+
+ b.Property("ReconnectionInterval")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId", "Name")
+ .IsUnique();
+
+ b.ToTable("ChatBots");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("ChatSettingsId")
+ .HasColumnType("bigint");
+
+ b.Property("DiscordChannelId")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("IrcChannel")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("IsAdminChannel")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("IsUpdatesChannel")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("IsWatchdogChannel")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("Tag")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChatSettingsId", "DiscordChannelId")
+ .IsUnique()
+ .HasFilter("[DiscordChannelId] IS NOT NULL");
+
+ b.HasIndex("ChatSettingsId", "IrcChannel")
+ .IsUnique()
+ .HasFilter("[IrcChannel] IS NOT NULL");
+
+ b.ToTable("ChatChannels");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("ByondVersion")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("DMApiMajorVersion")
+ .HasColumnType("int");
+
+ b.Property("DMApiMinorVersion")
+ .HasColumnType("int");
+
+ b.Property("DMApiPatchVersion")
+ .HasColumnType("int");
+
+ b.Property("DirectoryName")
+ .IsRequired()
+ .HasColumnType("uniqueidentifier");
+
+ b.Property("DmeName")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("GitHubDeploymentId")
+ .HasColumnType("int");
+
+ b.Property("GitHubRepoId")
+ .HasColumnType("bigint");
+
+ b.Property("JobId")
+ .HasColumnType("bigint");
+
+ b.Property("MinimumSecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("Output")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RepositoryOrigin")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RevisionInformationId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DirectoryName");
+
+ b.HasIndex("JobId")
+ .IsUnique();
+
+ b.HasIndex("RevisionInformationId");
+
+ b.ToTable("CompileJobs");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("AdditionalParameters")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AllowWebClient")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("AutoStart")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("DumpOnHeartbeatRestart")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("HeartbeatSeconds")
+ .HasColumnType("bigint");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("LogOutput")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("Port")
+ .HasColumnType("int");
+
+ b.Property("SecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("StartProfiler")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("StartupTimeout")
+ .HasColumnType("bigint");
+
+ b.Property("TopicRequestTimeout")
+ .HasColumnType("bigint");
+
+ b.Property("Visibility")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId")
+ .IsUnique();
+
+ b.ToTable("DreamDaemonSettings");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("ApiValidationPort")
+ .HasColumnType("int");
+
+ b.Property("ApiValidationSecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("ProjectName")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("RequireDMApiValidation")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("Timeout")
+ .IsRequired()
+ .HasColumnType("time");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId")
+ .IsUnique();
+
+ b.ToTable("DreamMakerSettings");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("AutoUpdateInterval")
+ .HasColumnType("bigint");
+
+ b.Property("ChatBotLimit")
+ .HasColumnType("int");
+
+ b.Property("ConfigurationType")
+ .HasColumnType("int");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Online")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasColumnType("nvarchar(450)");
+
+ b.Property("SwarmIdentifer")
+ .HasColumnType("nvarchar(450)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Path", "SwarmIdentifer")
+ .IsUnique()
+ .HasFilter("[SwarmIdentifer] IS NOT NULL");
+
+ b.ToTable("Instances");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("ByondRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("ChatBotRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("ConfigurationRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("DreamDaemonRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("DreamMakerRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("InstancePermissionSetRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("PermissionSetId")
+ .HasColumnType("bigint");
+
+ b.Property("RepositoryRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId");
+
+ b.HasIndex("PermissionSetId", "InstanceId")
+ .IsUnique();
+
+ b.ToTable("InstancePermissionSets");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("CancelRight")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("CancelRightsType")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("Cancelled")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("CancelledById")
+ .HasColumnType("bigint");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("ErrorCode")
+ .HasColumnType("bigint");
+
+ b.Property("ExceptionDetails")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("StartedAt")
+ .IsRequired()
+ .HasColumnType("datetimeoffset");
+
+ b.Property("StartedById")
+ .HasColumnType("bigint");
+
+ b.Property("StoppedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CancelledById");
+
+ b.HasIndex("InstanceId");
+
+ b.HasIndex("StartedById");
+
+ b.ToTable("Jobs");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("ExternalUserId")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("Provider")
+ .HasColumnType("int");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.HasIndex("Provider", "ExternalUserId")
+ .IsUnique();
+
+ b.ToTable("OAuthConnections");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("AdministrationRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("GroupId")
+ .HasColumnType("bigint");
+
+ b.Property("InstanceManagerRights")
+ .HasColumnType("decimal(20,0)");
+
+ b.Property("UserId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("GroupId")
+ .IsUnique()
+ .HasFilter("[GroupId] IS NOT NULL");
+
+ b.HasIndex("UserId")
+ .IsUnique()
+ .HasFilter("[UserId] IS NOT NULL");
+
+ b.ToTable("PermissionSets");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("AccessIdentifier")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CompileJobId")
+ .HasColumnType("bigint");
+
+ b.Property("LaunchSecurityLevel")
+ .HasColumnType("int");
+
+ b.Property("LaunchVisibility")
+ .HasColumnType("int");
+
+ b.Property("Port")
+ .HasColumnType("int");
+
+ b.Property("ProcessId")
+ .HasColumnType("int");
+
+ b.Property("RebootState")
+ .HasColumnType("int");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CompileJobId");
+
+ b.ToTable("ReattachInformations");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("AccessToken")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AccessUser")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("AutoUpdatesKeepTestMerges")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("AutoUpdatesSynchronize")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("CommitterEmail")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CommitterName")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("CreateGitHubDeployments")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("PostTestMergeComment")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("PushTestMergeCommits")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("ShowTestMergeCommitters")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("UpdateSubmodules")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId")
+ .IsUnique();
+
+ b.ToTable("RepositorySettings");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("RevisionInformationId")
+ .HasColumnType("bigint");
+
+ b.Property("TestMergeId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("RevisionInformationId");
+
+ b.HasIndex("TestMergeId");
+
+ b.ToTable("RevInfoTestMerges");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("CommitSha")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("nvarchar(40)");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("OriginCommitSha")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("nvarchar(40)");
+
+ b.Property("Timestamp")
+ .HasColumnType("datetimeoffset");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId", "CommitSha")
+ .IsUnique();
+
+ b.ToTable("RevisionInformations");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("Author")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("BodyAtMerge")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Comment")
+ .HasMaxLength(10000)
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("MergedAt")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("MergedById")
+ .HasColumnType("bigint");
+
+ b.Property("Number")
+ .HasColumnType("int");
+
+ b.Property("PrimaryRevisionInformationId")
+ .IsRequired()
+ .HasColumnType("bigint");
+
+ b.Property("TargetCommitSha")
+ .IsRequired()
+ .HasMaxLength(40)
+ .HasColumnType("nvarchar(40)");
+
+ b.Property("TitleAtMerge")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Url")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("MergedById");
+
+ b.HasIndex("PrimaryRevisionInformationId")
+ .IsUnique();
+
+ b.ToTable("TestMerges");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("CanonicalName")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("CreatedAt")
+ .IsRequired()
+ .HasColumnType("datetimeoffset");
+
+ b.Property("CreatedById")
+ .HasColumnType("bigint");
+
+ b.Property("Enabled")
+ .IsRequired()
+ .HasColumnType("bit");
+
+ b.Property("GroupId")
+ .HasColumnType("bigint");
+
+ b.Property("LastPasswordUpdate")
+ .HasColumnType("datetimeoffset");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.Property("PasswordHash")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("SystemIdentifier")
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CanonicalName")
+ .IsUnique();
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("GroupId");
+
+ b.HasIndex("SystemIdentifier")
+ .IsUnique()
+ .HasFilter("[SystemIdentifier] IS NOT NULL");
+
+ b.ToTable("Users");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"), 1L, 1);
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("nvarchar(100)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Name")
+ .IsUnique();
+
+ b.ToTable("Groups");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithMany("ChatSettings")
+ .HasForeignKey("InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings")
+ .WithMany("Channels")
+ .HasForeignKey("ChatSettingsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("ChatSettings");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Job", "Job")
+ .WithOne()
+ .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
+ .WithMany("CompileJobs")
+ .HasForeignKey("RevisionInformationId")
+ .OnDelete(DeleteBehavior.ClientNoAction)
+ .IsRequired();
+
+ b.Navigation("Job");
+
+ b.Navigation("RevisionInformation");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithOne("DreamDaemonSettings")
+ .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithOne("DreamMakerSettings")
+ .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithMany("InstancePermissionSets")
+ .HasForeignKey("InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.PermissionSet", "PermissionSet")
+ .WithMany("InstancePermissionSets")
+ .HasForeignKey("PermissionSetId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+
+ b.Navigation("PermissionSet");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy")
+ .WithMany()
+ .HasForeignKey("CancelledById");
+
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithMany("Jobs")
+ .HasForeignKey("InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy")
+ .WithMany()
+ .HasForeignKey("StartedById")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("CancelledBy");
+
+ b.Navigation("Instance");
+
+ b.Navigation("StartedBy");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.User", "User")
+ .WithMany("OAuthConnections")
+ .HasForeignKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
+ .WithOne("PermissionSet")
+ .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "GroupId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.HasOne("Tgstation.Server.Host.Models.User", "User")
+ .WithOne("PermissionSet")
+ .HasForeignKey("Tgstation.Server.Host.Models.PermissionSet", "UserId")
+ .OnDelete(DeleteBehavior.Cascade);
+
+ b.Navigation("Group");
+
+ b.Navigation("User");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob")
+ .WithMany()
+ .HasForeignKey("CompileJobId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("CompileJob");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithOne("RepositorySettings")
+ .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation")
+ .WithMany("ActiveTestMerges")
+ .HasForeignKey("RevisionInformationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge")
+ .WithMany("RevisonInformations")
+ .HasForeignKey("TestMergeId")
+ .OnDelete(DeleteBehavior.ClientNoAction)
+ .IsRequired();
+
+ b.Navigation("RevisionInformation");
+
+ b.Navigation("TestMerge");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance")
+ .WithMany("RevisionInformations")
+ .HasForeignKey("InstanceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("Instance");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy")
+ .WithMany("TestMerges")
+ .HasForeignKey("MergedById")
+ .OnDelete(DeleteBehavior.Restrict)
+ .IsRequired();
+
+ b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation")
+ .WithOne("PrimaryTestMerge")
+ .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired();
+
+ b.Navigation("MergedBy");
+
+ b.Navigation("PrimaryRevisionInformation");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
+ {
+ b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy")
+ .WithMany("CreatedUsers")
+ .HasForeignKey("CreatedById");
+
+ b.HasOne("Tgstation.Server.Host.Models.UserGroup", "Group")
+ .WithMany("Users")
+ .HasForeignKey("GroupId");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("Group");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
+ {
+ b.Navigation("Channels");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
+ {
+ b.Navigation("ChatSettings");
+
+ b.Navigation("DreamDaemonSettings");
+
+ b.Navigation("DreamMakerSettings");
+
+ b.Navigation("InstancePermissionSets");
+
+ b.Navigation("Jobs");
+
+ b.Navigation("RepositorySettings");
+
+ b.Navigation("RevisionInformations");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.PermissionSet", b =>
+ {
+ b.Navigation("InstancePermissionSets");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b =>
+ {
+ b.Navigation("ActiveTestMerges");
+
+ b.Navigation("CompileJobs");
+
+ b.Navigation("PrimaryTestMerge");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b =>
+ {
+ b.Navigation("RevisonInformations");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.User", b =>
+ {
+ b.Navigation("CreatedUsers");
+
+ b.Navigation("OAuthConnections");
+
+ b.Navigation("PermissionSet");
+
+ b.Navigation("TestMerges");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.UserGroup", b =>
+ {
+ b.Navigation("PermissionSet")
+ .IsRequired();
+
+ b.Navigation("Users");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.cs
new file mode 100644
index 0000000000..366ac06457
--- /dev/null
+++ b/src/Tgstation.Server.Host/Database/Migrations/20230331220749_MSAddDreamDaemonLogOutput.cs
@@ -0,0 +1,39 @@
+using System;
+
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Tgstation.Server.Host.Database.Migrations
+{
+ ///
+ /// Adds the DreamDaemon LogOutput column for MSSQL.
+ ///
+ public partial class MSAddDreamDaemonLogOutput : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ if (migrationBuilder == null)
+ throw new ArgumentNullException(nameof(migrationBuilder));
+
+ migrationBuilder.AddColumn(
+ name: "LogOutput",
+ table: "DreamDaemonSettings",
+ type: "bit",
+ nullable: false,
+ defaultValue: false);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ if (migrationBuilder == null)
+ throw new ArgumentNullException(nameof(migrationBuilder));
+
+ migrationBuilder.DropColumn(
+ name: "LogOutput",
+ table: "DreamDaemonSettings");
+ }
+ }
+}
diff --git a/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.Designer.cs b/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.Designer.cs
new file mode 100644
index 0000000000..c1bb3e23c5
--- /dev/null
+++ b/src/Tgstation.Server.Host/Database/Migrations/20230331221032_PGAddDreamDaemonLogOutput.Designer.cs
@@ -0,0 +1,1048 @@
+//
+using System;
+
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Tgstation.Server.Host.Database.Migrations
+{
+ [DbContext(typeof(PostgresSqlDatabaseContext))]
+ [Migration("20230331221032_PGAddDreamDaemonLogOutput")]
+ partial class PGAddDreamDaemonLogOutput
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.15")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ChannelLimit")
+ .HasColumnType("integer");
+
+ b.Property("ConnectionString")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("character varying(10000)");
+
+ b.Property("Enabled")
+ .HasColumnType("boolean");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Provider")
+ .HasColumnType("integer");
+
+ b.Property("ReconnectionInterval")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId", "Name")
+ .IsUnique();
+
+ b.ToTable("ChatBots");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ChatSettingsId")
+ .HasColumnType("bigint");
+
+ b.Property("DiscordChannelId")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("IrcChannel")
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("IsAdminChannel")
+ .IsRequired()
+ .HasColumnType("boolean");
+
+ b.Property("IsUpdatesChannel")
+ .IsRequired()
+ .HasColumnType("boolean");
+
+ b.Property("IsWatchdogChannel")
+ .IsRequired()
+ .HasColumnType("boolean");
+
+ b.Property("Tag")
+ .HasMaxLength(10000)
+ .HasColumnType("character varying(10000)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("ChatSettingsId", "DiscordChannelId")
+ .IsUnique();
+
+ b.HasIndex("ChatSettingsId", "IrcChannel")
+ .IsUnique();
+
+ b.ToTable("ChatChannels");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ByondVersion")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("DMApiMajorVersion")
+ .HasColumnType("integer");
+
+ b.Property("DMApiMinorVersion")
+ .HasColumnType("integer");
+
+ b.Property("DMApiPatchVersion")
+ .HasColumnType("integer");
+
+ b.Property("DirectoryName")
+ .IsRequired()
+ .HasColumnType("uuid");
+
+ b.Property("DmeName")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("GitHubDeploymentId")
+ .HasColumnType("integer");
+
+ b.Property("GitHubRepoId")
+ .HasColumnType("bigint");
+
+ b.Property("JobId")
+ .HasColumnType("bigint");
+
+ b.Property("MinimumSecurityLevel")
+ .HasColumnType("integer");
+
+ b.Property("Output")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("RepositoryOrigin")
+ .HasColumnType("text");
+
+ b.Property("RevisionInformationId")
+ .HasColumnType("bigint");
+
+ b.HasKey("Id");
+
+ b.HasIndex("DirectoryName");
+
+ b.HasIndex("JobId")
+ .IsUnique();
+
+ b.HasIndex("RevisionInformationId");
+
+ b.ToTable("CompileJobs");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdditionalParameters")
+ .IsRequired()
+ .HasMaxLength(10000)
+ .HasColumnType("character varying(10000)");
+
+ b.Property("AllowWebClient")
+ .IsRequired()
+ .HasColumnType("boolean");
+
+ b.Property("AutoStart")
+ .IsRequired()
+ .HasColumnType("boolean");
+
+ b.Property("DumpOnHeartbeatRestart")
+ .IsRequired()
+ .HasColumnType("boolean");
+
+ b.Property("HeartbeatSeconds")
+ .HasColumnType("bigint");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("LogOutput")
+ .IsRequired()
+ .HasColumnType("boolean");
+
+ b.Property("Port")
+ .HasColumnType("integer");
+
+ b.Property("SecurityLevel")
+ .HasColumnType("integer");
+
+ b.Property("StartProfiler")
+ .IsRequired()
+ .HasColumnType("boolean");
+
+ b.Property("StartupTimeout")
+ .HasColumnType("bigint");
+
+ b.Property("TopicRequestTimeout")
+ .HasColumnType("bigint");
+
+ b.Property("Visibility")
+ .HasColumnType("integer");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId")
+ .IsUnique();
+
+ b.ToTable("DreamDaemonSettings");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ApiValidationPort")
+ .HasColumnType("integer");
+
+ b.Property("ApiValidationSecurityLevel")
+ .HasColumnType("integer");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("ProjectName")
+ .HasMaxLength(10000)
+ .HasColumnType("character varying(10000)");
+
+ b.Property("RequireDMApiValidation")
+ .IsRequired()
+ .HasColumnType("boolean");
+
+ b.Property("Timeout")
+ .IsRequired()
+ .HasColumnType("interval");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId")
+ .IsUnique();
+
+ b.ToTable("DreamMakerSettings");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AutoUpdateInterval")
+ .HasColumnType("bigint");
+
+ b.Property("ChatBotLimit")
+ .HasColumnType("integer");
+
+ b.Property("ConfigurationType")
+ .HasColumnType("integer");
+
+ b.Property("Name")
+ .IsRequired()
+ .HasMaxLength(100)
+ .HasColumnType("character varying(100)");
+
+ b.Property("Online")
+ .IsRequired()
+ .HasColumnType("boolean");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("SwarmIdentifer")
+ .HasColumnType("text");
+
+ b.HasKey("Id");
+
+ b.HasIndex("Path", "SwarmIdentifer")
+ .IsUnique();
+
+ b.ToTable("Instances");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.InstancePermissionSet", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ByondRights")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("ChatBotRights")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("ConfigurationRights")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("DreamDaemonRights")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("DreamMakerRights")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("InstancePermissionSetRights")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("PermissionSetId")
+ .HasColumnType("bigint");
+
+ b.Property("RepositoryRights")
+ .HasColumnType("numeric(20,0)");
+
+ b.HasKey("Id");
+
+ b.HasIndex("InstanceId");
+
+ b.HasIndex("PermissionSetId", "InstanceId")
+ .IsUnique();
+
+ b.ToTable("InstancePermissionSets");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CancelRight")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("CancelRightsType")
+ .HasColumnType("numeric(20,0)");
+
+ b.Property("Cancelled")
+ .IsRequired()
+ .HasColumnType("boolean");
+
+ b.Property("CancelledById")
+ .HasColumnType("bigint");
+
+ b.Property("Description")
+ .IsRequired()
+ .HasColumnType("text");
+
+ b.Property("ErrorCode")
+ .HasColumnType("bigint");
+
+ b.Property("ExceptionDetails")
+ .HasColumnType("text");
+
+ b.Property("InstanceId")
+ .HasColumnType("bigint");
+
+ b.Property("StartedAt")
+ .IsRequired()
+ .HasColumnType("timestamp with time zone");
+
+ b.Property("StartedById")
+ .HasColumnType("bigint");
+
+ b.Property("StoppedAt")
+ .HasColumnType("timestamp with time zone");
+
+ b.HasKey("Id");
+
+ b.HasIndex("CancelledById");
+
+ b.HasIndex("InstanceId");
+
+ b.HasIndex("StartedById");
+
+ b.ToTable("Jobs");
+ });
+
+ modelBuilder.Entity("Tgstation.Server.Host.Models.OAuthConnection", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bigint");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property