diff --git a/build/Version.props b/build/Version.props index ef659a9af2..b169916252 100644 --- a/build/Version.props +++ b/build/Version.props @@ -2,10 +2,10 @@ - 4.2.0 + 4.2.1 6.2.0 6.1.0 - 5.1.0 + 5.1.1 0.4.0 1.1.0 diff --git a/build/prep_deployment.ps1 b/build/prep_deployment.ps1 index 2f50c5b2b7..358b72d8c6 100644 --- a/build/prep_deployment.ps1 +++ b/build/prep_deployment.ps1 @@ -1,5 +1,7 @@ $bf = $env:APPVEYOR_BUILD_FOLDER -[XML]$versionXML = Get-Content "$bf/build/Version.props" +$propsPath = "$bf/build/Version.props" + +[XML]$versionXML = Get-Content $propsPath $env:TGSVersion = $versionXML.Project.PropertyGroup.TgsCoreVersion $env:APIVersion = $versionXML.Project.PropertyGroup.TgsApiVersion $env:DMVersion = $versionXML.Project.PropertyGroup.TgsDmapiVersion @@ -11,7 +13,7 @@ if (($env:CONFIGURATION -match "Release") -And ($env:APPVEYOR_REPO_BRANCH -match $env:TGSDeploy = "Do it." Write-Host "Generating release notes..." - dotnet run -p "$bf/tools/ReleaseNotes" $env:TGSVersion + dotnet run -p "$bf/tools/ReleaseNotes" $env:TGSVersion $propsPath $env:TGSDraftNotes = !($?) $releaseNotesPath = "$bf/release_notes.md" Write-Host "Reading release notes from $releaseNotesPath..." diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index 9f84fdd3cb..e164a4ec96 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -1,6 +1,6 @@ //tgstation-server DMAPI -#define TGS_DMAPI_VERSION "5.1.0" +#define TGS_DMAPI_VERSION "5.1.1" //All functions and datums outside this document are subject to change with any version and should not be relied on diff --git a/src/DMAPI/tgs/core/datum.dm b/src/DMAPI/tgs/core/datum.dm index 3abf5f284f..ccc75fe344 100644 --- a/src/DMAPI/tgs/core/datum.dm +++ b/src/DMAPI/tgs/core/datum.dm @@ -6,7 +6,7 @@ TGS_DEFINE_AND_SET_GLOBAL(tgs, null) /datum/tgs_api/New(datum/tgs_event_handler/event_handler, datum/tgs_version/version) . = ..() - src.event_handler = version + src.event_handler = event_handler src.version = version /datum/tgs_api/latest diff --git a/src/DMAPI/tgs/v5/_defines.dm b/src/DMAPI/tgs/v5/_defines.dm index 4abd059677..2baf3e12d7 100644 --- a/src/DMAPI/tgs/v5/_defines.dm +++ b/src/DMAPI/tgs/v5/_defines.dm @@ -60,10 +60,10 @@ #define DMAPI5_TOPIC_COMMAND_CHANGE_PORT 2 #define DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE 3 #define DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED 4 -#define DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE 4 -#define DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE 5 -#define DMAPI5_TOPIC_COMMAND_HEARTBEAT 6 -#define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 7 +#define DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE 5 +#define DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE 6 +#define DMAPI5_TOPIC_COMMAND_HEARTBEAT 7 +#define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 8 #define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType" #define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand" diff --git a/src/DMAPI/tgs/v5/api.dm b/src/DMAPI/tgs/v5/api.dm index f0c38a7c24..eedefb2877 100644 --- a/src/DMAPI/tgs/v5/api.dm +++ b/src/DMAPI/tgs/v5/api.dm @@ -16,7 +16,7 @@ var/list/chat_channels /datum/tgs_api/v5/ApiVersion() - return new /datum/tgs_version("5.1.0") + return new /datum/tgs_version("5.1.1") /datum/tgs_api/v5/OnWorldNew(minimum_required_security_level) server_port = world.params[DMAPI5_PARAM_SERVER_PORT] diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 19c999d4f2..9de878067c 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -415,7 +415,7 @@ namespace Tgstation.Server.Api.Models /// /// Attempted to start the watchdog with a corrupted . /// - [Description("Cannot launch with active compile job as it is corrupted!")] + [Description("Cannot launch active compile job as it is missing or corrupted!")] WatchdogCompileJobCorrupted, /// diff --git a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs index c57ae5c352..eb249c99c7 100644 --- a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs @@ -119,7 +119,10 @@ namespace Tgstation.Server.Host.Components.Byond IProcess directXInstaller; try { - directXInstaller = processExecutor.LaunchProcess(IOManager.ConcatPath(rbdx, "DXSETUP.exe"), rbdx, "/silent", noShellExecute: true); + directXInstaller = processExecutor.LaunchProcess( + IOManager.ConcatPath(rbdx, "DXSETUP.exe"), + rbdx, "/silent", + noShellExecute: true); } catch (Exception e) { diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs index 2782f9aba1..94e01c8af5 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatManager.cs @@ -646,7 +646,8 @@ namespace Tgstation.Server.Host.Components.Chat public async Task StopAsync(CancellationToken cancellationToken) { handlerCts.Cancel(); - await chatHandler.ConfigureAwait(false); + if (chatHandler != null) + await chatHandler.ConfigureAwait(false); await Task.WhenAll(providers.Select(x => x.Value).Select(x => x.Disconnect(cancellationToken))).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index fd368e5ef3..62a03990dd 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -118,7 +118,7 @@ namespace Tgstation.Server.Host.Components.Deployment if (!jobLockCounts.TryGetValue(job.Id, out var currentVal) || currentVal == 1) { jobLockCounts.Remove(job.Id); - logger.LogDebug("Cleaning compile job {0} => {1}", job.Id, job.DirectoryName); + logger.LogDebug("Cleaning lock-free compile job {0} => {1}", job.Id, job.DirectoryName); cleanupTask = HandleCleanup(); } else @@ -247,7 +247,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// #pragma warning disable CA1506 // TODO: Decomplexify - public async Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken) + public async Task CleanUnusedCompileJobs(CancellationToken cancellationToken) { List jobIdsToSkip; @@ -260,12 +260,18 @@ namespace Tgstation.Server.Host.Components.Deployment // 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)).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false); + jobUidsToNotErase = (await db.CompileJobs.Where( + x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)) + .Select(x => x.DirectoryName.Value) + .ToListAsync(cancellationToken) + .ConfigureAwait(false)) + .Select(x => x.ToString()) + .ToList(); }).ConfigureAwait(false); - // add the other exemption - if (exceptThisOne != null) - jobUidsToNotErase.Add(exceptThisOne.DirectoryName.Value.ToString().ToUpperInvariant()); + jobUidsToNotErase.Add(WindowsSwappableDmbProvider.LiveGameDirectory); + + logger.LogTrace("We will not clean the following directories: {0}", String.Join(", ", jobUidsToNotErase)); // cleanup var gameDirectory = ioManager.ResolvePath(); @@ -275,8 +281,9 @@ namespace Tgstation.Server.Host.Components.Deployment var tasks = directories.Select(async x => { var nameOnly = ioManager.GetFileName(x); - if (jobUidsToNotErase.Contains(nameOnly.ToUpperInvariant())) + if (jobUidsToNotErase.Contains(nameOnly)) return; + logger.LogDebug("Cleaning unused game folder: {0}...", nameOnly); try { ++deleting; @@ -292,10 +299,7 @@ namespace Tgstation.Server.Host.Components.Deployment } }).ToList(); if (deleting > 0) - { - logger.LogDebug("Cleaning unused game folders: {0}...", String.Join(", ", directories)); - await Task.WhenAll().ConfigureAwait(false); - } + await Task.WhenAll(tasks).ConfigureAwait(false); } #pragma warning restore CA1506 } diff --git a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs index 8cceeb1dd4..3c2b21bb43 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DreamMaker.cs @@ -244,6 +244,7 @@ namespace Tgstation.Server.Host.Components.Deployment ADirectoryName)), $"-clean {job.DmeName}.{DmeExtension}", true, + true, true); int exitCode; using (cancellationToken.Register(() => dm.Terminate())) @@ -381,7 +382,7 @@ namespace Tgstation.Server.Host.Components.Deployment await chatManager.SendUpdateMessage( String.Format( CultureInfo.InvariantCulture, - "*Deployment Triggered*{0}Revision: {1}{2}{3}{0} BYOND Version: {4}.{5}", + "*Deployment Triggered*{0}Revision: {1}{2}{3}{0}BYOND Version: {4}.{5}", Environment.NewLine, commitInsert, testmergeInsert, diff --git a/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs index 266f3b41e8..4f5059c2be 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs @@ -37,11 +37,10 @@ namespace Tgstation.Server.Host.Components.Deployment Task FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken); /// - /// Deletes all compile jobs that are inactive in the Game folder + /// Deletes all compile jobs that are inactive in the Game folder. /// - /// An optional compile job to not delete /// The for the operation /// A representing the running operation - Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken); + Task CleanUnusedCompileJobs(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/WindowsSwappableDmbProvider.cs b/src/Tgstation.Server.Host/Components/Deployment/WindowsSwappableDmbProvider.cs index add8f65690..597333c54c 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/WindowsSwappableDmbProvider.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/WindowsSwappableDmbProvider.cs @@ -14,7 +14,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// The directory where the is symlinked to. /// - const string LiveGameDirectory = "Live"; + public const string LiveGameDirectory = "Live"; /// public string DmbName => baseProvider.DmbName; diff --git a/src/Tgstation.Server.Host/Components/EventConsumer.cs b/src/Tgstation.Server.Host/Components/EventConsumer.cs index e204cbcf8f..1cebdd1d2b 100644 --- a/src/Tgstation.Server.Host/Components/EventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/EventConsumer.cs @@ -30,14 +30,13 @@ namespace Tgstation.Server.Host.Components } /// - public async Task HandleEvent(EventType eventType, IEnumerable parameters, CancellationToken cancellationToken) + public async Task HandleEvent(EventType eventType, IEnumerable parameters, CancellationToken cancellationToken) { if (watchdog == null) throw new InvalidOperationException("EventConsumer used without watchdog set!"); - if (!await configuration.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false)) - return false; - return await watchdog.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false); + await configuration.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false); + await watchdog.HandleEvent(eventType, parameters, cancellationToken).ConfigureAwait(false); } /// diff --git a/src/Tgstation.Server.Host/Components/IEventConsumer.cs b/src/Tgstation.Server.Host/Components/IEventConsumer.cs index aa30d18827..f4dd616bd8 100644 --- a/src/Tgstation.Server.Host/Components/IEventConsumer.cs +++ b/src/Tgstation.Server.Host/Components/IEventConsumer.cs @@ -15,7 +15,7 @@ namespace Tgstation.Server.Host.Components /// The /// The parameters for /// The for the operation - /// A resulting in if more should run, otherwise - Task HandleEvent(EventType eventType, IEnumerable parameters, CancellationToken cancellationToken); + /// A representing the running operation. + Task HandleEvent(EventType eventType, IEnumerable parameters, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index 0e23c4cca1..87814d36c6 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -15,7 +15,6 @@ using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; -using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Models; @@ -623,12 +622,7 @@ namespace Tgstation.Server.Host.Components // 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.MostRecentCompletedCompileJobOrDefault(metadata, cancellationToken).ConfigureAwait(false); - }).ConfigureAwait(false); - await dmbFactory.CleanUnusedCompileJobs(latestCompileJob, cancellationToken).ConfigureAwait(false); + await dmbFactory.CleanUnusedCompileJobs(cancellationToken).ConfigureAwait(false); } /// diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index d9991f0044..7b841e1270 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -235,6 +235,7 @@ namespace Tgstation.Server.Host.Components bridgeRegistrar, serverPortProvider, loggerFactory, + loggerFactory.CreateLogger(), metadata.CloneMetadata()); var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger(), metadata.CloneMetadata()); diff --git a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs index f14967cf94..f7f4820dc7 100644 --- a/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs +++ b/src/Tgstation.Server.Host/Components/Interop/DMApiConstants.cs @@ -33,7 +33,7 @@ namespace Tgstation.Server.Host.Components.Interop /// /// The DMAPI being used. /// - public static readonly Version Version = new Version(5, 1, 0); + public static readonly Version Version = new Version(5, 1, 1); /// /// for use when communicating with the DMAPI. diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 43bf3baa27..4a6aef276e 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -163,8 +163,8 @@ namespace Tgstation.Server.Host.Components.Repository if (remote.EndsWith(item, StringComparison.OrdinalIgnoreCase)) remote = remote.Substring(0, remote.LastIndexOf(item, StringComparison.OrdinalIgnoreCase)); var splits = remote.Split('/'); - name = splits[splits.Length - 1]; - owner = splits[splits.Length - 2].Split('.')[0]; + name = splits.Last(); + owner = splits[^2].Split('.').First(); logger.LogTrace("GetRepositoryOwnerName({0}) => {1} / {2}", remote, owner, name); } @@ -622,18 +622,14 @@ namespace Tgstation.Server.Host.Components.Repository cancellationToken.ThrowIfCancellationRequested(); try { - if (!await eventConsumer.HandleEvent( + await eventConsumer.HandleEvent( EventType.RepoPreSynchronize, new List { ioMananger.ResolvePath() }, cancellationToken) - .ConfigureAwait(false)) - { - logger.LogDebug("Aborted synchronize due to event handler response!"); - return false; - } + .ConfigureAwait(false); } finally { diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index 51f33bf3df..0e8e1736bf 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -522,7 +522,6 @@ namespace Tgstation.Server.Host.Components.Session var json = JsonConvert.SerializeObject(parameters, DMApiConstants.SerializerSettings); logger.LogTrace("Topic request: {0}", json); - Exception caughtException; try { var commandString = String.Format(CultureInfo.InvariantCulture, @@ -558,19 +557,20 @@ namespace Tgstation.Server.Host.Components.Session return new CombinedTopicResponse(topicResponse, interopResponse); } - catch (OperationCanceledException e) + catch (OperationCanceledException) { + logger.LogTrace( + "Topic request {0}!", + cancellationToken.IsCancellationRequested + ? "aborted" + : "timed out"); cancellationToken.ThrowIfCancellationRequested(); - caughtException = e; } catch (Exception e) { - caughtException = e; + logger.LogWarning("Send command exception:{0}{1}", Environment.NewLine, e); } - if (caughtException == null) - logger.LogWarning("Send command exception:{0}{1}", Environment.NewLine, caughtException.Message); - return null; } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs index aef22ee773..92a76a973b 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionControllerFactory.cs @@ -15,6 +15,7 @@ using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; @@ -83,6 +84,11 @@ namespace Tgstation.Server.Host.Components.Session /// readonly ILoggerFactory loggerFactory; + /// + /// The for the + /// + readonly ILogger logger; + /// /// The for the /// @@ -120,6 +126,7 @@ namespace Tgstation.Server.Host.Components.Session /// The value of . /// The value of . /// The value of + /// The value of . public SessionControllerFactory( IProcessExecutor processExecutor, IByondManager byond, @@ -133,6 +140,7 @@ namespace Tgstation.Server.Host.Components.Session IBridgeRegistrar bridgeRegistrar, IServerPortProvider serverPortProvider, ILoggerFactory loggerFactory, + ILogger logger, Api.Models.Instance instance) { this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); @@ -148,6 +156,7 @@ namespace Tgstation.Server.Host.Components.Session this.bridgeRegistrar = bridgeRegistrar ?? throw new ArgumentNullException(nameof(bridgeRegistrar)); this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// @@ -192,6 +201,8 @@ namespace Tgstation.Server.Host.Components.Session if (launchParameters.SecurityLevel == DreamDaemonSecurity.Trusted) await byondLock.TrustDmbPath(ioManager.ConcatPath(basePath, dmbProvider.DmbName), cancellationToken).ConfigureAwait(false); + CheckPagerIsNotRunning(); + var accessIdentifier = cryptographySuite.GetSecureString(); // set command line options @@ -213,7 +224,24 @@ namespace Tgstation.Server.Host.Components.Session var noShellExecute = !platformIdentifier.IsWindows; // launch dd - var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments, noShellExecute: noShellExecute); + var process = processExecutor.LaunchProcess( + byondLock.DreamDaemonPath, + basePath, + arguments, + noShellExecute, + noShellExecute, + noShellExecute: noShellExecute); + + if (noShellExecute) + { + // Log DD output + _ = process.Lifetime.ContinueWith( + x => logger.LogTrace( + "DreamDaemon Output:{0}{1}", + Environment.NewLine, process.GetCombinedOutput()), + TaskScheduler.Current); + } + try { networkPromptReaper.RegisterProcess(process); @@ -371,5 +399,14 @@ namespace Tgstation.Server.Host.Components.Session securityLevel, apiValidateOnly); } + + /// + /// Make sure the BYOND pager is not running. + /// + void CheckPagerIsNotRunning() + { + if (platformIdentifier.IsWindows && processExecutor.IsProcessWithNameRunning("byond")) + throw new JobException("Cannot start DreamDaemon headless with the BYOND pager running!"); + } } } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index c2ce055042..17ebbe5a5e 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -11,6 +11,7 @@ using System.Threading.Tasks; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; @@ -442,12 +443,12 @@ namespace Tgstation.Server.Host.Components.StaticFiles public Task StopAsync(CancellationToken cancellationToken) => EnsureDirectories(cancellationToken); /// - public async Task HandleEvent(EventType eventType, IEnumerable parameters, CancellationToken cancellationToken) + public async Task HandleEvent(EventType eventType, IEnumerable parameters, CancellationToken cancellationToken) { await EnsureDirectories(cancellationToken).ConfigureAwait(false); if (!EventTypeScriptFileNameMap.TryGetValue(eventType, out var scriptName)) - return true; + return; // always execute in serial using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) @@ -456,17 +457,22 @@ namespace Tgstation.Server.Host.Components.StaticFiles var resolvedScriptsDir = ioManager.ResolvePath(EventScriptsSubdirectory); foreach (var I in files.Select(x => ioManager.GetFileName(x)).Where(x => x.StartsWith(scriptName, StringComparison.Ordinal))) - using (var script = processExecutor.LaunchProcess(ioManager.ConcatPath(resolvedScriptsDir, I), resolvedScriptsDir, String.Join(' ', parameters), noShellExecute: true)) + using (var script = processExecutor.LaunchProcess( + ioManager.ConcatPath(resolvedScriptsDir, I), + resolvedScriptsDir, + String.Join(' ', parameters), + true, + true, + true)) using (cancellationToken.Register(() => script.Terminate())) { var exitCode = await script.Lifetime.ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); + var scriptOutput = script.GetCombinedOutput(); if (exitCode != 0) - return false; + throw new JobException($"Script {I} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}"); } } - - return true; } /// diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index abbbb4598d..3f026305ce 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -727,19 +727,15 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public async Task HandleEvent(EventType eventType, IEnumerable parameters, CancellationToken cancellationToken) + public async Task HandleEvent(EventType eventType, IEnumerable parameters, CancellationToken cancellationToken) { - if (!Running) - return true; - - var notification = new EventNotification(eventType, parameters); - var activeServer = GetActiveController(); // Server may have ended if (activeServer == null) - return true; + return; + var notification = new EventNotification(eventType, parameters); var result = await activeServer.SendCommand( new TopicParameters(notification), cancellationToken) @@ -762,8 +758,6 @@ namespace Tgstation.Server.Host.Components.Watchdog .Select(nullableChannelId => nullableChannelId.Value), cancellationToken))) .ConfigureAwait(false); - - return true; } /// diff --git a/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs b/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs index 68fae14333..9699c86a16 100644 --- a/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs +++ b/src/Tgstation.Server.Host/Database/Design/DesignTimeDbContextFactoryHelpers.cs @@ -1,8 +1,5 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; +using Microsoft.Extensions.Options; using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.IO; -using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Database.Design { @@ -11,31 +8,21 @@ namespace Tgstation.Server.Host.Database.Design /// static class DesignTimeDbContextFactoryHelpers { - /// - /// Path to the json file to use for migrations configuration - /// - const string RootJson = "appsettings.json"; - - /// - /// Path to the development json file to use for migrations configuration - /// - const string DevJson = "appsettings.Development.json"; - /// /// Get the for the /// + /// The . + /// The . /// The for the - public static IOptions GetDbContextOptions() + public static IOptions GetDbContextOptions(DatabaseType databaseType, string connectionString) { - var builder = new ConfigurationBuilder(); - var assemblyInfoProvider = new AssemblyInformationProvider(); - var ioManager = new DefaultIOManager(); - builder.SetBasePath(ioManager.GetDirectoryName(assemblyInfoProvider.Path)); - builder.AddJsonFile(RootJson); - builder.AddJsonFile(DevJson); - var configuration = builder.Build(); - var dbConfig = configuration.GetSection(DatabaseConfiguration.Section).Get(); - dbConfig.DesignTime = true; + var dbConfig = new DatabaseConfiguration + { + DesignTime = true, + DatabaseType = databaseType, + ConnectionString = connectionString + }; + return Options.Create(dbConfig); } } diff --git a/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs index 4ac39129ac..6496f55009 100644 --- a/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs +++ b/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Logging; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; @@ -17,7 +18,9 @@ namespace Tgstation.Server.Host.Database.Design using var loggerFactory = new LoggerFactory(); return new MySqlDatabaseContext( new DbContextOptions(), - DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), + DesignTimeDbContextFactoryHelpers.GetDbContextOptions( + DatabaseType.MariaDB, + "Server=127.0.0.1;User Id=root;Password=fake;Database=TGS_Design"), new DatabaseSeeder( new CryptographySuite( new PasswordHasher()), diff --git a/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs index dc56d54d4b..483fc08b2b 100644 --- a/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs +++ b/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Logging; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; @@ -17,7 +18,9 @@ namespace Tgstation.Server.Host.Database.Design using var loggerFactory = new LoggerFactory(); return new SqlServerDatabaseContext( new DbContextOptions(), - DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), + DesignTimeDbContextFactoryHelpers.GetDbContextOptions( + DatabaseType.SqlServer, + "Data Source=fake;Initial Catalog=TGS_Design;Integrated Security=True;Application Name=tgstation-server"), new DatabaseSeeder( new CryptographySuite( new PasswordHasher()), diff --git a/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs index 65789a71c6..09e506711f 100644 --- a/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs +++ b/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs @@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Logging; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; @@ -17,7 +18,9 @@ namespace Tgstation.Server.Host.Database.Design using var loggerFactory = new LoggerFactory(); return new SqliteDatabaseContext( new DbContextOptions(), - DesignTimeDbContextFactoryHelpers.GetDbContextOptions(), + DesignTimeDbContextFactoryHelpers.GetDbContextOptions( + DatabaseType.Sqlite, + "Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate"), new DatabaseSeeder( new CryptographySuite( new PasswordHasher()), diff --git a/src/Tgstation.Server.Host/System/IProcessExecutor.cs b/src/Tgstation.Server.Host/System/IProcessExecutor.cs index 3d9a001b9e..6b732915dd 100644 --- a/src/Tgstation.Server.Host/System/IProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/IProcessExecutor.cs @@ -23,5 +23,12 @@ /// The /// The represented by on success, on failure IProcess GetProcess(int id); + + /// + /// Check if a with a given is running. + /// + /// The name of the process without the extension. + /// if the process is running, otherwise. + bool IsProcessWithNameRunning(string name); } } diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index 07b8d5fbe3..a722179341 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -117,7 +117,7 @@ namespace Tgstation.Server.Host.System { if (outputStringBuilder == null) throw new InvalidOperationException("Output reading was not enabled!"); - return errorStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray()); + return outputStringBuilder.ToString().TrimStart(Environment.NewLine.ToCharArray()); } /// diff --git a/src/Tgstation.Server.Host/System/ProcessExecutor.cs b/src/Tgstation.Server.Host/System/ProcessExecutor.cs index 9e66aedf85..772c754131 100644 --- a/src/Tgstation.Server.Host/System/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/ProcessExecutor.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using System; +using System.Linq; using System.Text; using System.Threading.Tasks; @@ -102,9 +103,30 @@ namespace Tgstation.Server.Host.System } /// - public IProcess LaunchProcess(string fileName, string workingDirectory, string arguments, bool readOutput, bool readError, bool noShellExecute) + public IProcess LaunchProcess( + string fileName, + string workingDirectory, + string arguments, + bool readOutput, + bool readError, + bool noShellExecute) { - logger.LogDebug("Launching process in {0}: {1} {2}", workingDirectory, fileName, arguments); + if (fileName == null) + throw new ArgumentNullException(nameof(fileName)); + if (workingDirectory == null) + throw new ArgumentNullException(nameof(workingDirectory)); + if (arguments == null) + throw new ArgumentNullException(nameof(arguments)); + + if (!noShellExecute && (readOutput || readError)) + throw new InvalidOperationException("Requesting output/error reading requires noShellExecute to be true!"); + + logger.LogDebug( + "{0}aunching process in {1}: {2} {3}", + noShellExecute ? "L" : "Shell l", + workingDirectory, + fileName, + arguments); var handle = new global::System.Diagnostics.Process(); try { @@ -112,9 +134,12 @@ namespace Tgstation.Server.Host.System handle.StartInfo.Arguments = arguments; handle.StartInfo.WorkingDirectory = workingDirectory; - handle.StartInfo.UseShellExecute = !(noShellExecute || readOutput || readError); + handle.StartInfo.UseShellExecute = !noShellExecute; StringBuilder outputStringBuilder = null, errorStringBuilder = null, combinedStringBuilder = null; + + TaskCompletionSource outputReadTcs = null; + TaskCompletionSource errorReadTcs = null; if (readOutput || readError) { combinedStringBuilder = new StringBuilder(); @@ -122,8 +147,15 @@ namespace Tgstation.Server.Host.System { outputStringBuilder = new StringBuilder(); handle.StartInfo.RedirectStandardOutput = true; + outputReadTcs = new TaskCompletionSource(); handle.OutputDataReceived += (sender, e) => { + if (e.Data == null) + { + outputReadTcs.SetResult(null); + return; + } + combinedStringBuilder.Append(Environment.NewLine); combinedStringBuilder.Append(e.Data); outputStringBuilder.Append(Environment.NewLine); @@ -135,8 +167,15 @@ namespace Tgstation.Server.Host.System { errorStringBuilder = new StringBuilder(); handle.StartInfo.RedirectStandardError = true; + errorReadTcs = new TaskCompletionSource(); handle.ErrorDataReceived += (sender, e) => { + if (e.Data == null) + { + errorReadTcs.SetResult(null); + return; + } + combinedStringBuilder.Append(Environment.NewLine); combinedStringBuilder.Append(e.Data); errorStringBuilder.Append(Environment.NewLine); @@ -148,16 +187,30 @@ namespace Tgstation.Server.Host.System var lifetimeTask = AttachExitHandler(handle); handle.Start(); + + static async Task AddToLifetimeTask(Task originalTask, TaskCompletionSource tcs) + { + var exitCode = await originalTask.ConfigureAwait(false); + await tcs.Task.ConfigureAwait(false); + return exitCode; + } + try { if (readOutput) + { handle.BeginOutputReadLine(); + lifetimeTask = AddToLifetimeTask(lifetimeTask, outputReadTcs); + } } catch (InvalidOperationException) { } try { if (readError) + { handle.BeginErrorReadLine(); + lifetimeTask = AddToLifetimeTask(lifetimeTask, errorReadTcs); + } } catch (InvalidOperationException) { } @@ -176,5 +229,15 @@ namespace Tgstation.Server.Host.System throw; } } + + /// + public bool IsProcessWithNameRunning(string name) + { + var procs = global::System.Diagnostics.Process.GetProcessesByName(name); + foreach (var proc in procs) + proc.Dispose(); + + return procs.Any(); + } } } diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index 4db31fc889..8de83ec181 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -48,7 +48,7 @@ - + @@ -112,19 +112,10 @@ - - - - PreserveNewest - - - PreserveNewest - - - + - Always + PreserveNewest PreserveNewest diff --git a/tests/DMAPI/BasicOperation/Test.dm b/tests/DMAPI/BasicOperation/Test.dm index 5f9b79e2f9..21ba9e6226 100644 --- a/tests/DMAPI/BasicOperation/Test.dm +++ b/tests/DMAPI/BasicOperation/Test.dm @@ -26,6 +26,11 @@ world.log << "You really shouldn't be able to read this" /world/Topic(T, Addr, Master, Keys) + world.log << "Topic: [T]" + . = HandleTopic(T) + world.log << "Response: [.]" + +/world/proc/HandleTopic(T) TGS_TOPIC /world/Reboot(reason) diff --git a/tests/DMAPI/LongRunning/Test.dm b/tests/DMAPI/LongRunning/Test.dm index 4c8bf5c7d1..631dcfccae 100644 --- a/tests/DMAPI/LongRunning/Test.dm +++ b/tests/DMAPI/LongRunning/Test.dm @@ -12,6 +12,11 @@ world.TgsInitializationComplete() /world/Topic(T, Addr, Master, Keys) + world.log << "Topic: [T]" + . = HandleTopic(T) + world.log << "Response: [.]" + +/world/proc/HandleTopic(T) TGS_TOPIC world.sleep_offline = FALSE diff --git a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs index 6c6de168bd..e19a840a3e 100644 --- a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs @@ -25,7 +25,7 @@ namespace Tgstation.Server.Tests.Instance public async Task Run(CancellationToken cancellationToken) { await TestNoVersion(cancellationToken).ConfigureAwait(false); - await TestInstall511(cancellationToken).ConfigureAwait(false); + await TestInstallStable(cancellationToken).ConfigureAwait(false); await TestInstallFakeVersion(cancellationToken).ConfigureAwait(false); } @@ -40,11 +40,11 @@ namespace Tgstation.Server.Tests.Instance await WaitForJob(test.InstallJob, 60, true, cancellationToken).ConfigureAwait(false); } - async Task TestInstall511(CancellationToken cancellationToken) + async Task TestInstallStable(CancellationToken cancellationToken) { var newModel = new Api.Models.Byond { - Version = new Version(511, 1385) + Version = new Version(513, 1514) }; var test = await byondClient.SetActiveVersion(newModel, cancellationToken).ConfigureAwait(false); Assert.IsNotNull(test.InstallJob); @@ -57,7 +57,7 @@ namespace Tgstation.Server.Tests.Instance if (new PlatformIdentifier().IsWindows) dreamMaker += ".exe"; - var dreamMakerDir = Path.Combine(metadata.Path, "Byond", "511.1385", "byond", "bin"); + var dreamMakerDir = Path.Combine(metadata.Path, "Byond", newModel.Version.ToString(), "byond", "bin"); Assert.IsTrue(Directory.Exists(dreamMakerDir), $"Directory {dreamMakerDir} does not exist!"); Assert.IsTrue(File.Exists(Path.Combine(dreamMakerDir, dreamMaker)), $"Missing DreamMaker executable! Dir contents: {String.Join(", ", Directory.GetFileSystemEntries(dreamMakerDir))}"); diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 268f7a17b3..7111026da4 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -42,11 +42,15 @@ namespace Tgstation.Server.Tests.Instance }, cancellationToken), ErrorCode.DreamDaemonDoubleSoft); await RunBasicTest(cancellationToken); - await RunHeartbeatTest(cancellationToken); // await RunLongRunningTestThenUpdate(cancellationToken); // await RunLongRunningTestThenUpdateWithByondVersionSwitch(cancellationToken); + // Remove this deploy when the above tests are reenabled + await DeployTestDme("LongRunning/long_running_test", DreamDaemonSecurity.Trusted, cancellationToken); + + await RunHeartbeatTest(cancellationToken); + await StartAndLeaveRunning(cancellationToken); } @@ -89,21 +93,29 @@ namespace Tgstation.Server.Tests.Instance await WaitForJob(startJob, 10, false, cancellationToken); - await instanceClient.DreamDaemon.Update(new DreamDaemon - { - SoftShutdown = true - }, cancellationToken); - // lock on to DD and pause it so it can't heartbeat var ddProcs = System.Diagnostics.Process.GetProcessesByName("DreamDaemon").ToList(); if (ddProcs.Count != 1) Assert.Inconclusive($"Incorrect number of DD processes: {ddProcs.Count}"); - var pid = ddProcs.Single().Id; + using var ddProc = ddProcs.Single(); using var ourProcessHandler = new ProcessExecutor( - new PlatformIdentifier().IsWindows ? (IProcessSuspender)new WindowsProcessSuspender(Mock.Of>()) : new PosixProcessSuspender(Mock.Of>()), + new PlatformIdentifier().IsWindows + ? (IProcessSuspender)new WindowsProcessSuspender(Mock.Of>()) + : new PosixProcessSuspender(Mock.Of>()), Mock.Of>(), - LoggerFactory.Create(x => { })).GetProcess(pid); + LoggerFactory.Create(x => { })) + .GetProcess(ddProc.Id); + + // Ensure it's responding to heartbeats + await Task.WhenAny(Task.Delay(20000), ourProcessHandler.Lifetime); + Assert.IsFalse(ddProc.HasExited); + + await instanceClient.DreamDaemon.Update(new DreamDaemon + { + SoftShutdown = true + }, cancellationToken); + ourProcessHandler.Suspend(); await Task.WhenAny(ourProcessHandler.Lifetime, Task.Delay(TimeSpan.FromSeconds(20))); @@ -121,6 +133,12 @@ namespace Tgstation.Server.Tests.Instance Assert.Fail("DreamDaemon didn't shutdown within the timeout!"); } while (timeout > 0); + + // disable heartbeats + await instanceClient.DreamDaemon.Update(new DreamDaemon + { + HeartbeatSeconds = 0, + }, cancellationToken); } async Task RunLongRunningTestThenUpdate(CancellationToken cancellationToken) diff --git a/tests/Tgstation.Server.Tests/IntegrationTest.cs b/tests/Tgstation.Server.Tests/IntegrationTest.cs index 2b0e580be7..63e61fe328 100644 --- a/tests/Tgstation.Server.Tests/IntegrationTest.cs +++ b/tests/Tgstation.Server.Tests/IntegrationTest.cs @@ -1,4 +1,3 @@ -using Discord.WebSocket; using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -16,7 +15,8 @@ using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Client; using Tgstation.Server.Host; -using Tgstation.Server.Host.Components.Chat.Providers; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.System; using Tgstation.Server.Tests.Instance; namespace Tgstation.Server.Tests @@ -28,13 +28,9 @@ namespace Tgstation.Server.Tests readonly IServerClientFactory clientFactory = new ServerClientFactory(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString())); [TestMethod] - public async Task TestServerUpdate() + public async Task TestUpdateProtocol() { using var server = new TestingServer(); - - if (server.DatabaseType == "Sqlite") - Assert.Inconclusive("Cannot run this test on SQLite yet!"); - using var serverCts = new CancellationTokenSource(); var cancellationToken = serverCts.Token; var serverTask = server.Run(cancellationToken); @@ -101,14 +97,22 @@ namespace Tgstation.Server.Tests static void TerminateAllDDs() { - foreach (var proc in Process.GetProcessesByName("DreamDaemon")) + foreach (var proc in System.Diagnostics.Process.GetProcessesByName("DreamDaemon")) using (proc) proc.Kill(); } [TestMethod] - public async Task TestFullStandardOperation() + public async Task TestServer() { + var procs = System.Diagnostics.Process.GetProcessesByName("byond"); + if(procs.Any()) + { + foreach (var proc in procs) + proc.Dispose(); + Assert.Inconclusive("Cannot run server test because DreamDaemon will not start headless while the BYOND pager is running!"); + } + using var server = new TestingServer(); using var serverCts = new CancellationTokenSource(); var cancellationToken = serverCts.Token; @@ -191,20 +195,35 @@ namespace Tgstation.Server.Tests await Task.WhenAny(serverTask, Task.Delay(30000, cancellationToken)); Assert.IsTrue(serverTask.IsCompleted); + var preStartupTime = DateTimeOffset.Now; + serverTask = server.Run(cancellationToken); using (var adminClient = await CreateAdminClient()) { var instanceClient = adminClient.Instances.CreateClient(instance); - // reattach job var jobs = await instanceClient.Jobs.ListActive(cancellationToken); - if (jobs.Any()) + if (!jobs.Any()) { - Assert.AreEqual(1, jobs.Count); + var entities = await instanceClient.Jobs.List(cancellationToken); + var getTasks = entities + .Select(e => instanceClient.Jobs.GetId(e, cancellationToken)) + .ToList(); - await new JobsRequiredTest(instanceClient.Jobs).WaitForJob(jobs.Single(), 40, false, cancellationToken); + await Task.WhenAll(getTasks); + jobs = getTasks + .Select(x => x.Result) + .Where(x => x.StartedAt.Value > preStartupTime) + .ToList(); } + Assert.AreEqual(1, jobs.Count); + + var reattachJob = jobs.Single(); + Assert.IsTrue(reattachJob.StartedAt.Value >= preStartupTime); + + await new JobsRequiredTest(instanceClient.Jobs).WaitForJob(reattachJob, 40, false, cancellationToken); + var dd = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.IsTrue(dd.Running.Value); @@ -220,20 +239,34 @@ namespace Tgstation.Server.Tests await Task.WhenAny(serverTask, Task.Delay(30000, cancellationToken)); Assert.IsTrue(serverTask.IsCompleted); + preStartupTime = DateTimeOffset.Now; serverTask = server.Run(cancellationToken); using (var adminClient = await CreateAdminClient()) { var instanceClient = adminClient.Instances.CreateClient(instance); - // launch job var jobs = await instanceClient.Jobs.ListActive(cancellationToken); - if (jobs.Any()) + if (!jobs.Any()) { - Assert.AreEqual(1, jobs.Count); + var entities = await instanceClient.Jobs.List(cancellationToken); + var getTasks = entities + .Select(e => instanceClient.Jobs.GetId(e, cancellationToken)) + .ToList(); - await new JobsRequiredTest(instanceClient.Jobs).WaitForJob(jobs.Single(), 40, false, cancellationToken); + await Task.WhenAll(getTasks); + jobs = getTasks + .Select(x => x.Result) + .Where(x => x.StartedAt.Value > preStartupTime) + .ToList(); } + Assert.AreEqual(1, jobs.Count); + + var launchJob = jobs.Single(); + Assert.IsTrue(launchJob.StartedAt.Value >= preStartupTime); + + await new JobsRequiredTest(instanceClient.Jobs).WaitForJob(launchJob, 40, false, cancellationToken); + var dd = await instanceClient.DreamDaemon.Read(cancellationToken); Assert.IsTrue(dd.Running.Value); @@ -262,5 +295,24 @@ namespace Tgstation.Server.Tests TerminateAllDDs(); } } + + [TestMethod] + public async Task TestScriptExecution() + { + var platformIdentifier = new PlatformIdentifier(); + var processExecutor = new ProcessExecutor( + Mock.Of(), + Mock.Of>(), + LoggerFactory.Create(x => { })); + + using var process = processExecutor.LaunchProcess("test." + platformIdentifier.ScriptFileExtension, ".", String.Empty, true, true, true); + using var cts = new CancellationTokenSource(); + cts.CancelAfter(3000); + var exitCode = await process.Lifetime.WithToken(cts.Token); + + Assert.AreEqual(0, exitCode); + Assert.AreEqual(String.Empty, process.GetErrorOutput().Trim()); + Assert.AreEqual("Hello World!", process.GetStandardOutput().Trim()); + } } } diff --git a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj index 67ce0f1a7d..ff83e2f565 100644 --- a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj +++ b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj @@ -26,4 +26,13 @@ + + + PreserveNewest + + + PreserveNewest + + + diff --git a/tests/Tgstation.Server.Tests/test.bat b/tests/Tgstation.Server.Tests/test.bat new file mode 100644 index 0000000000..7ac29630a7 --- /dev/null +++ b/tests/Tgstation.Server.Tests/test.bat @@ -0,0 +1,3 @@ +@echo off + +echo Hello World! diff --git a/tests/Tgstation.Server.Tests/test.sh b/tests/Tgstation.Server.Tests/test.sh new file mode 100755 index 0000000000..982e2cd73f --- /dev/null +++ b/tests/Tgstation.Server.Tests/test.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +echo Hello World! diff --git a/tools/ReleaseNotes/Program.cs b/tools/ReleaseNotes/Program.cs index 7d12f64ff2..5e205c6b8a 100644 --- a/tools/ReleaseNotes/Program.cs +++ b/tools/ReleaseNotes/Program.cs @@ -1,7 +1,6 @@ using Octokit; using System; using System.Collections.Generic; -using System.Globalization; using System.IO; using System.Linq; using System.Text; @@ -33,7 +32,11 @@ namespace ReleaseNotes return 2; } - var doNotCloseMilestone = args.Length >= 2 && args[1].ToUpperInvariant() == "--NO-CLOSE"; + var doNotCloseMilestone = args.Length > 1 && args[1].ToUpperInvariant() == "--NO-CLOSE"; + + string propsPath = "../../../../../build/Version.props"; + if (args.Length > 1 && !doNotCloseMilestone) + propsPath = args[1]; const string ReleaseNotesEnvVar = "TGS4_RELEASE_NOTES_TOKEN"; var githubToken = Environment.GetEnvironmentVariable(ReleaseNotesEnvVar); @@ -73,7 +76,7 @@ namespace ReleaseNotes Task milestoneTask = null; var milestoneTaskLock = new object(); - var releaseDictionary = new Dictionary>(); + var releaseDictionary = new Dictionary>>(StringComparer.OrdinalIgnoreCase); var authorizedUsers = new Dictionary>(); bool postControlPanelMessage = false; @@ -94,74 +97,84 @@ namespace ReleaseNotes if (milestoneTask == null) milestoneTask = GetMilestone(); - if (!fullPR.Merged) - return; + // if (!fullPR.Merged) + //return; async Task BuildNotesFromComment(string comment, User user) { + async Task CommitNotes(string component, List notes) + { + Task authTask; + TaskCompletionSource ourTcs = null; + lock (authorizedUsers) + { + if (!authorizedUsers.TryGetValue(user.Id, out authTask)) + { + ourTcs = new TaskCompletionSource(); + authTask = ourTcs.Task; + authorizedUsers.Add(user.Id, authTask); + } + } + + if (ourTcs != null) + try + { + //check if the user has access + var perm = String.IsNullOrWhiteSpace(githubToken) + ? PermissionLevel.Write + : (await client.Repository.Collaborator.ReviewPermission(RepoOwner, RepoName, user.Login).ConfigureAwait(false)).Permission; + ourTcs.SetResult(perm == PermissionLevel.Write || perm == PermissionLevel.Admin); + } + catch + { + ourTcs.SetResult(false); + throw; + } + + var authorized = await authTask.ConfigureAwait(false); + if (!authorized) + return; + + lock (releaseDictionary) + { + foreach (var I in notes) + Console.WriteLine(component + " #" + fullPR.Number + " - " + I + " (@" + user.Login + ")"); + + var tupleSelector = notes.Select(note => Tuple.Create(note, fullPR.Number)); + if (releaseDictionary.TryGetValue(component, out var currentValues)) + currentValues.AddRange(tupleSelector); + else + releaseDictionary.Add(component, tupleSelector.ToList()); + } + } + var commentSplits = comment.Split('\n'); - var notesOpen = false; + string targetComponent = null; var notes = new List(); foreach (var line in commentSplits) { var trimmedLine = line.Trim(); - if (!notesOpen) + if (targetComponent == null) { - notesOpen = trimmedLine.StartsWith(":cl:", StringComparison.Ordinal); + if (trimmedLine.StartsWith(":cl:", StringComparison.Ordinal)) + { + targetComponent = trimmedLine.Substring(4).Trim(); + if (targetComponent.Length == 0) + targetComponent = "Core"; + } continue; } if (trimmedLine.StartsWith("/:cl:", StringComparison.Ordinal)) { - notesOpen = false; + await CommitNotes(targetComponent, notes); + targetComponent = null; + notes.Clear(); continue; } if (trimmedLine.Length == 0) continue; notes.Add(trimmedLine); } - if (notesOpen || notes.Count == 0) - return; - - Task authTask; - TaskCompletionSource ourTcs = null; - lock (authorizedUsers) - { - if (!authorizedUsers.TryGetValue(user.Id, out authTask)) - { - ourTcs = new TaskCompletionSource(); - authTask = ourTcs.Task; - authorizedUsers.Add(user.Id, authTask); - } - } - - if (ourTcs != null) - try - { - //check if the user has access - var perm = String.IsNullOrWhiteSpace(githubToken) - ? PermissionLevel.Write - : (await client.Repository.Collaborator.ReviewPermission(RepoOwner, RepoName, user.Login).ConfigureAwait(false)).Permission; - ourTcs.SetResult(perm == PermissionLevel.Write || perm == PermissionLevel.Admin); - } - catch - { - ourTcs.SetResult(false); - throw; - } - - var authorized = await authTask.ConfigureAwait(false); - if (!authorized) - return; - - lock (releaseDictionary) - { - foreach (var I in notes) - Console.WriteLine("#" + fullPR.Number + " - " + I + " (@" + user.Login + ")"); - if (releaseDictionary.TryGetValue(fullPR.Number, out var currentValues)) - currentValues.AddRange(notes); - else - releaseDictionary.Add(fullPR.Number, notes); - } } var comments = await client.Issue.Comment.GetAllForIssue(RepoOwner, RepoName, fullPR.Number).ConfigureAwait(false); @@ -205,7 +218,7 @@ namespace ReleaseNotes //trim away all the lines that don't start with # string keepThisRelease; - if (version.Build == 0) + if (version.Build <= 1) keepThisRelease = "# "; else keepThisRelease = "## "; @@ -223,7 +236,7 @@ namespace ReleaseNotes switch (releasingSuite) { case 4: - var doc = XDocument.Load("../../../../../build/Version.props"); + var doc = XDocument.Load(propsPath); var project = doc.Root; var xmlNamespace = project.GetDefaultNamespace(); var versionsPropertyGroup = project.Elements().First(); @@ -294,17 +307,25 @@ namespace ReleaseNotes } foreach (var I in releaseDictionary.OrderBy(kvp => kvp.Key)) - foreach (var note in I.Value) + { + newNotes.Append(Environment.NewLine); + newNotes.Append("#### "); + newNotes.Append(I.Key); + + + foreach (var noteTuple in I.Value) { newNotes.Append(Environment.NewLine); newNotes.Append("- "); - newNotes.Append(note); + newNotes.Append(noteTuple.Item1); newNotes.Append(" (#"); - newNotes.Append(I.Key); + newNotes.Append(noteTuple.Item2); newNotes.Append(')'); } - newNotes.Append(Environment.NewLine); + newNotes.Append(Environment.NewLine); + } + newNotes.Append(Environment.NewLine); if (version != new Version(4, 1, 0))