diff --git a/build/package/nix/package.nix b/build/package/nix/package.nix index 191e9f68ed..7d04f757a4 100644 --- a/build/package/nix/package.nix +++ b/build/package/nix/package.nix @@ -78,12 +78,13 @@ stdenv.mkDerivation { }; buildInputs = with pkgs; [ - dotnetCorePackages.aspnetcore_8_0 + dotnetCorePackages.sdk_8_0 gdb systemd zlib gcc_multi glibc + bash ]; nativeBuildInputs = with pkgs; [ makeWrapper @@ -98,12 +99,13 @@ stdenv.mkDerivation { mkdir -p $out/bin unzip "${fixedOutput}/ServerConsole.zip" -d $out/bin rm -rf $out/bin/lib - makeWrapper ${pkgs.dotnetCorePackages.aspnetcore_8_0}/dotnet $out/bin/tgstation-server --suffix PATH : ${ + makeWrapper ${pkgs.dotnetCorePackages.sdk_8_0}/dotnet $out/bin/tgstation-server --suffix PATH : ${ lib.makeBinPath ( with pkgs; [ - dotnetCorePackages.aspnetcore_8_0 + dotnetCorePackages.sdk_8_0 gdb + bash ] ) } --suffix LD_LIBRARY_PATH : ${ diff --git a/build/package/nix/tgstation-server.nix b/build/package/nix/tgstation-server.nix index eb60c7c94c..745391341a 100644 --- a/build/package/nix/tgstation-server.nix +++ b/build/package/nix/tgstation-server.nix @@ -21,6 +21,12 @@ let ]; byond-patcher = pkgs-i686.writeShellScriptBin "EngineInstallComplete-050-TgsPatchELFByond.sh" '' + # If the file doesn't exist, assume OD + if [[ ! -f ../../Byond/$1/byond/bin/DreamDaemon ]] ; then + echo "DreamDaemon doesn't appear to exist. Assuming OD install" + exit + fi + BYOND_PATH=$(realpath ../../Byond/$1/byond/bin/) ${pkgs.patchelf}/bin/patchelf --set-interpreter "$(cat ${stdenv.cc}/nix-support/dynamic-linker)" \ @@ -109,6 +115,11 @@ in group = cfg.groupname; mode = "0640"; }; + "tgstation-server.d/EventScripts/README.txt" = { + text = "TGS event scripts placed here will be executed by all online instances"; + group = cfg.groupname; + mode = "0640"; + }; }; systemd.services.tgstation-server = { diff --git a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs index 40fbde1a40..e02c109e6b 100644 --- a/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/ByondInstallerBase.cs @@ -74,29 +74,30 @@ protected ByondInstallerBase(IIOManager ioManager, ILogger l } /// - public override IEngineInstallation CreateInstallation(EngineVersion version, string path, Task installationTask) + public override ValueTask CreateInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken) { CheckVersionValidity(version); var installationIOManager = new ResolvingIOManager(IOManager, path); var supportsMapThreads = version.Version >= MapThreadsVersion; - return new ByondInstallation( - installationIOManager, - installationTask, - version, - installationIOManager.ResolvePath( - installationIOManager.ConcatPath( - ByondBinPath, - GetDreamDaemonName( - version.Version!, - out var supportsCli))), - installationIOManager.ResolvePath( - installationIOManager.ConcatPath( - ByondBinPath, - DreamMakerName)), - supportsCli, - supportsMapThreads); + return ValueTask.FromResult( + new ByondInstallation( + installationIOManager, + installationTask, + version, + installationIOManager.ResolvePath( + installationIOManager.ConcatPath( + ByondBinPath, + GetDreamDaemonName( + version.Version!, + out var supportsCli))), + installationIOManager.ResolvePath( + installationIOManager.ConcatPath( + ByondBinPath, + DreamMakerName)), + supportsCli, + supportsMapThreads)); } /// diff --git a/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs index c24eee3e8b..5b3cc20c36 100644 --- a/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/DelegatingEngineInstaller.cs @@ -33,8 +33,8 @@ public Task CleanCache(CancellationToken cancellationToken) => Task.WhenAll(delegatedInstallers.Values.Select(installer => installer.CleanCache(cancellationToken))); /// - public IEngineInstallation CreateInstallation(EngineVersion version, string path, Task installationTask) - => DelegateCall(version, installer => installer.CreateInstallation(version, path, installationTask)); + public ValueTask CreateInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken) + => DelegateCall(version, installer => installer.CreateInstallation(version, path, installationTask, cancellationToken)); /// public ValueTask DownloadVersion(EngineVersion version, JobProgressReporter jobProgressReporter, CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs b/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs index 6ca2b94030..30329edd34 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineInstallerBase.cs @@ -40,7 +40,7 @@ protected EngineInstallerBase(IIOManager ioManager, ILogger } /// - public abstract IEngineInstallation CreateInstallation(EngineVersion version, string path, Task installationTask); + public abstract ValueTask CreateInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken); /// public abstract Task CleanCache(CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs b/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs index 113a0d249b..eeadebc71f 100644 --- a/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs +++ b/src/Tgstation.Server.Host/Components/Engine/EngineManager.cs @@ -337,7 +337,8 @@ async ValueTask ReadVersion(string path) try { - AddInstallationContainer(version, path, Task.CompletedTask); + var installation = await engineInstaller.CreateInstallation(version, path, Task.CompletedTask, cancellationToken); + AddInstallationContainer(installation); logger.LogDebug("Added detected BYOND version {versionKey}...", version); } catch (Exception ex) @@ -410,107 +411,121 @@ async ValueTask AssertAndLockVersion( IEngineInstallation installation; EngineExecutableLock installLock; bool installedOrInstalling; - lock (installedVersions) + + // loop is because of the race condition with potentialInstallation, installedVersions, and CustomIteration selection + while (true) { - if (customVersionStream != null) + lock (installedVersions) { - var customInstallationNumber = 1; - do + if (customVersionStream != null) { - version.CustomIteration = customInstallationNumber++; + var customInstallationNumber = 1; + do + { + version.CustomIteration = customInstallationNumber++; + } + while (installedVersions.ContainsKey(version)); } - while (installedVersions.ContainsKey(version)); } - installedOrInstalling = installedVersions.TryGetValue(version, out var installationContainerNullable); - ReferenceCountingContainer installationContainer; - if (!installedOrInstalling) - { - if (!allowInstallation) - throw new InvalidOperationException($"Engine version {version} not installed!"); - - installationContainer = AddInstallationContainer( - version, - ioManager.ResolvePath(version.ToString()), - ourTcs.Task); - } - else - installationContainer = installationContainerNullable!; - - installation = installationContainer.Instance; - installLock = installationContainer.AddReference(); - } + var potentialInstallation = await engineInstaller.CreateInstallation( + version, + ioManager.ResolvePath(version.ToString()), + ourTcs.Task, + cancellationToken); - var deploymentPipelineProcesses = !neededForLock; - try - { - if (installedOrInstalling) + lock (installedVersions) { - progressReporter.StageName = "Waiting for existing installation job..."; + if (customVersionStream != null && installedVersions.ContainsKey(version)) + continue; - if (neededForLock && !installation.InstallationTask.IsCompleted) - logger.LogWarning("The required engine version ({version}) is not readily available! We will have to wait for it to install.", version); + installedOrInstalling = installedVersions.TryGetValue(version, out var installationContainerNullable); + ReferenceCountingContainer installationContainer; + if (!installedOrInstalling) + { + if (!allowInstallation) + throw new InvalidOperationException($"Engine version {version} not installed!"); - await installation.InstallationTask.WaitAsync(cancellationToken); - return installLock; + installationContainer = AddInstallationContainer(potentialInstallation); + } + else + installationContainer = installationContainerNullable!; + + installation = installationContainer.Instance; + installLock = installationContainer.AddReference(); } - // okay up to us to install it then - string? installPath = null; + var deploymentPipelineProcesses = !neededForLock; try { - if (customVersionStream != null) - logger.LogInformation("Installing custom engine version as {version}...", version); - else if (neededForLock) + if (installedOrInstalling) { - if (version.CustomIteration.HasValue) - throw new JobException(ErrorCode.EngineNonExistentCustomVersion); + progressReporter.StageName = "Waiting for existing installation job..."; + + if (neededForLock && !installation.InstallationTask.IsCompleted) + logger.LogWarning("The required engine version ({version}) is not readily available! We will have to wait for it to install.", version); - logger.LogWarning("The required engine version ({version}) is not readily available! We will have to install it.", version); + await installation.InstallationTask.WaitAsync(cancellationToken); + return installLock; } - else - logger.LogInformation("Requested engine version {version} not currently installed. Doing so now...", version); - progressReporter.StageName = "Running event"; + // okay up to us to install it then + string? installPath = null; + try + { + if (customVersionStream != null) + logger.LogInformation("Installing custom engine version as {version}...", version); + else if (neededForLock) + { + if (version.CustomIteration.HasValue) + throw new JobException(ErrorCode.EngineNonExistentCustomVersion); + + logger.LogWarning("The required engine version ({version}) is not readily available! We will have to install it.", version); + } + else + logger.LogInformation("Requested engine version {version} not currently installed. Doing so now...", version); - var versionString = version.ToString(); - await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List { versionString }, deploymentPipelineProcesses, cancellationToken); + progressReporter.StageName = "Running event"; - installPath = await InstallVersionFiles(progressReporter, version, customVersionStream, deploymentPipelineProcesses, cancellationToken); - await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List { versionString }, deploymentPipelineProcesses, cancellationToken); + var versionString = version.ToString(); + await eventConsumer.HandleEvent(EventType.EngineInstallStart, new List { versionString }, deploymentPipelineProcesses, cancellationToken); - ourTcs.SetResult(); - } - catch (Exception ex) - { - if (installPath != null) + installPath = await InstallVersionFiles(progressReporter, version, customVersionStream, deploymentPipelineProcesses, cancellationToken); + await eventConsumer.HandleEvent(EventType.EngineInstallComplete, new List { versionString }, deploymentPipelineProcesses, cancellationToken); + + ourTcs.SetResult(); + } + catch (Exception ex) { - try + if (installPath != null) { - logger.LogDebug("Cleaning up failed installation at {path}...", installPath); - await ioManager.DeleteDirectory(installPath, cancellationToken); - } - catch (Exception ex2) - { - logger.LogError(ex2, "Error cleaning up failed installation!"); + try + { + logger.LogDebug("Cleaning up failed installation at {path}...", installPath); + await ioManager.DeleteDirectory(installPath, cancellationToken); + } + catch (Exception ex2) + { + logger.LogError(ex2, "Error cleaning up failed installation!"); + } } - } - else if (ex is not OperationCanceledException) - await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List { ex.Message }, deploymentPipelineProcesses, cancellationToken); + else if (ex is not OperationCanceledException) + await eventConsumer.HandleEvent(EventType.EngineInstallFail, new List { ex.Message }, deploymentPipelineProcesses, cancellationToken); - lock (installedVersions) - installedVersions.Remove(version); + lock (installedVersions) + installedVersions.Remove(version); - ourTcs.SetException(ex); + ourTcs.SetException(ex); + throw; + } + + return installLock; + } + catch + { + installLock.Dispose(); throw; } - - return installLock; - } - catch - { - installLock.Dispose(); - throw; } } @@ -616,18 +631,14 @@ await ioManager.WriteAllBytes( /// /// Create and add a new to . /// - /// The being added. - /// The path to the installation. - /// The representing the installation process. - /// The new . - ReferenceCountingContainer AddInstallationContainer(EngineVersion version, string installPath, Task installationTask) + /// The being added. + /// A new for the /. + ReferenceCountingContainer AddInstallationContainer(IEngineInstallation installation) { - var installation = engineInstaller.CreateInstallation(version, installPath, installationTask); - var installationContainer = new ReferenceCountingContainer(installation); lock (installedVersions) - installedVersions.Add(version, installationContainer); + installedVersions.Add(installation.Version, installationContainer); return installationContainer; } diff --git a/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs index 0df793e219..1e943f9759 100644 --- a/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/IEngineInstaller.cs @@ -17,8 +17,9 @@ interface IEngineInstaller /// The of the installation. /// The path to the installation. /// The representing the installation process for the installation. - /// The . - IEngineInstallation CreateInstallation(EngineVersion version, string path, Task installationTask); + /// The for the operation. + /// A resulting in the . + ValueTask CreateInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken); /// /// Download a given engine . diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs index e7fbd9baa6..b8f5e7f877 100644 --- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstallation.cs @@ -60,30 +60,46 @@ sealed class OpenDreamInstallation : EngineInstallationBase /// readonly IAbstractHttpClientFactory httpClientFactory; + /// + /// Path to the Robust.Server.dll. + /// + readonly string serverDllPath; + + /// + /// Path to the DMCompiler.dll. + /// + readonly string compilerDllPath; + /// /// Initializes a new instance of the class. /// /// The for the . /// The value of . /// The value of . - /// The value of . - /// The value of . + /// The path to the dotnet executable. + /// The value of . + /// The value of . /// The value of . /// The value of . public OpenDreamInstallation( IIOManager installationIOManager, IAsyncDelayer asyncDelayer, IAbstractHttpClientFactory httpClientFactory, - string serverExePath, - string compilerExePath, + string dotnetPath, + string serverDllPath, + string compilerDllPath, Task installationTask, EngineVersion version) : base(installationIOManager) { this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); - ServerExePath = serverExePath ?? throw new ArgumentNullException(nameof(serverExePath)); - CompilerExePath = compilerExePath ?? throw new ArgumentNullException(nameof(compilerExePath)); + + ServerExePath = dotnetPath ?? throw new ArgumentNullException(nameof(dotnetPath)); + CompilerExePath = dotnetPath; + + this.serverDllPath = serverDllPath ?? throw new ArgumentNullException(nameof(serverDllPath)); + this.compilerDllPath = compilerDllPath ?? throw new ArgumentNullException(nameof(compilerDllPath)); InstallationTask = installationTask ?? throw new ArgumentNullException(nameof(installationTask)); Version = version ?? throw new ArgumentNullException(nameof(version)); @@ -107,7 +123,7 @@ public override string FormatServerArguments( var parametersString = EncodeParameters(parameters, launchParameters); - var arguments = $"--cvar {(logFilePath != null ? $"log.path=\"{InstallationIOManager.GetDirectoryName(logFilePath)}\" --cvar log.format=\"{InstallationIOManager.GetFileName(logFilePath)}\"" : "log.enabled=false")} --cvar watchdog.token={accessIdentifier} --cvar log.runtimelog=false --cvar net.port={launchParameters.Port!.Value} --cvar opendream.topic_port={launchParameters.OpenDreamTopicPort!.Value} --cvar opendream.world_params=\"{parametersString}\" --cvar opendream.json_path=\"./{dmbProvider.DmbName}\""; + var arguments = $"{serverDllPath} --cvar {(logFilePath != null ? $"log.path=\"{InstallationIOManager.GetDirectoryName(logFilePath)}\" --cvar log.format=\"{InstallationIOManager.GetFileName(logFilePath)}\"" : "log.enabled=false")} --cvar watchdog.token={accessIdentifier} --cvar log.runtimelog=false --cvar net.port={launchParameters.Port!.Value} --cvar opendream.topic_port={launchParameters.OpenDreamTopicPort!.Value} --cvar opendream.world_params=\"{parametersString}\" --cvar opendream.json_path=\"./{dmbProvider.DmbName}\""; return arguments; } @@ -119,7 +135,7 @@ public override string FormatCompilerArguments(string dmePath, string? additiona else additionalArguments = $"{additionalArguments.Trim()} "; - return $"--suppress-unimplemented --notices-enabled {additionalArguments}\"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\""; + return $"{compilerDllPath} --suppress-unimplemented --notices-enabled {additionalArguments}\"{dmePath ?? throw new ArgumentNullException(nameof(dmePath))}\""; } /// diff --git a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs index f37c4489b1..ad2fb20e95 100644 --- a/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Engine/OpenDreamInstaller.cs @@ -118,14 +118,18 @@ public OpenDreamInstaller( public override Task CleanCache(CancellationToken cancellationToken) => Task.CompletedTask; /// - public override IEngineInstallation CreateInstallation(EngineVersion version, string path, Task installationTask) + public override async ValueTask CreateInstallation(EngineVersion version, string path, Task installationTask, CancellationToken cancellationToken) { CheckVersionValidity(version); GetExecutablePaths(path, out var serverExePath, out var compilerExePath); + + var dotnetPath = (await DotnetHelper.GetDotnetPath(platformIdentifier, IOManager, cancellationToken)) + ?? throw new JobException("Failed to find dotnet path!"); return new OpenDreamInstallation( new ResolvingIOManager(IOManager, path), asyncDelayer, httpClientFactory, + dotnetPath, serverExePath, compilerExePath, installationTask, @@ -370,21 +374,19 @@ protected virtual ValueTask HandleExtremelyLongPathOperation( /// The path to the DMCompiler executable. protected void GetExecutablePaths(string installationPath, out string serverExePath, out string compilerExePath) { - var exeExtension = platformIdentifier.IsWindows - ? ".exe" - : String.Empty; + const string DllExtension = ".dll"; serverExePath = IOManager.ConcatPath( installationPath, BinDir, ServerDir, - $"Robust.Server{exeExtension}"); + $"Robust.Server{DllExtension}"); compilerExePath = IOManager.ConcatPath( installationPath, BinDir, InstallationCompilerDirectory, - $"DMCompiler{exeExtension}"); + $"DMCompiler{DllExtension}"); } } } diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index 6c695ec0d6..208a33cc76 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; @@ -45,7 +45,6 @@ using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Jobs; using Tgstation.Server.Host.System; -using Tgstation.Server.Host.Utils; using Tgstation.Server.Tests.Live.Instance; namespace Tgstation.Server.Tests.Live @@ -94,7 +93,17 @@ public TestLiveServer() result.AddRange(System.Diagnostics.Process.GetProcessesByName("dd")); break; case EngineType.OpenDream: - result.AddRange(System.Diagnostics.Process.GetProcessesByName("Robust.Server")); + var potentialProcesses = System.Diagnostics.Process.GetProcessesByName("dotnet") + .Where(process => + { + if (GetCommandLine(process).Contains("Robust.Server")) + return true; + + process.Dispose(); + return false; + }); + + result.AddRange(potentialProcesses); break; default: Assert.Fail($"Unknown engine type: {engineType}");