/// <inheritdoc /> public void ReplaceDmbProvider(IDmbProvider dmbProvider) { var oldDmb = reattachInformation.Dmb; reattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider)); oldDmb.Dispose(); }
/// <inheritdoc /> protected sealed override async Task <IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken) { if (ActiveSwappable != null) { throw new InvalidOperationException("Expected activeSwappable to be null!"); } if (startupDmbProvider != null) { throw new InvalidOperationException("Expected startupDmbProvider to be null!"); } Logger.LogTrace("Prep for server launch. pendingSwappable is {0}available", pendingSwappable == null ? "not " : String.Empty); // Add another lock to the startup DMB because it'll be used throughout the lifetime of the watchdog startupDmbProvider = await DmbFactory.FromCompileJob(dmbToUse.CompileJob, cancellationToken).ConfigureAwait(false); pendingSwappable ??= new SwappableDmbProvider(dmbToUse, GameIOManager, symlinkFactory); ActiveSwappable = pendingSwappable; pendingSwappable = null; try { await InitialLink(cancellationToken).ConfigureAwait(false); } catch { // We won't worry about disposing activeSwappable here as we can't dispose dmbToUse here. ActiveSwappable = null; throw; } return(ActiveSwappable); }
/// <inheritdoc /> public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken) { if (job == null) { throw new ArgumentNullException(nameof(job)); } CompileJob finalCompileJob = null; //now load the entire compile job tree await databaseContextFactory.UseContext(async db => finalCompileJob = await db.CompileJobs.Where(x => x.Id == job.Id) .Include(x => x.Job).ThenInclude(x => x.StartedBy) .Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy) .Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy) .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); //can't wait to see that query if (finalCompileJob == null) { //lol git f****d return; } var newProvider = await FromCompileJob(finalCompileJob, cancellationToken).ConfigureAwait(false); if (newProvider == null) { return; } lock (this) { nextDmbProvider?.Dispose(); nextDmbProvider = newProvider; newerDmbTcs.SetResult(nextDmbProvider); newerDmbTcs = new TaskCompletionSource <object>(); } }
/// <inheritdoc /> public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken) { if (job == null) { throw new ArgumentNullException(nameof(job)); } var newProvider = await FromCompileJob(job, cancellationToken).ConfigureAwait(false); if (newProvider == null) { return; } // Do this first, because it's entirely possible when we set the tcs it will immediately need to be applied if (started) { await gitHubDeploymentManager.StageDeployment( newProvider.CompileJob, cancellationToken) .ConfigureAwait(false); } lock (jobLockCounts) { nextDmbProvider?.Dispose(); nextDmbProvider = newProvider; // Oh god dammit var temp = newerDmbTcs; newerDmbTcs = new TaskCompletionSource <object>(); temp.SetResult(nextDmbProvider); } }
/// <inheritdoc /> protected override async Task HandleNewDmbAvailable(CancellationToken cancellationToken) { IDmbProvider compileJobProvider = DmbFactory.LockNextDmb(1); WindowsSwappableDmbProvider windowsProvider = null; try { windowsProvider = new WindowsSwappableDmbProvider(compileJobProvider, ioManager, symlinkFactory); Logger.LogDebug("Swapping to compile job {0}...", windowsProvider.CompileJob.Id); Server.Suspend(); await windowsProvider.MakeActive(cancellationToken).ConfigureAwait(false); Server.Resume(); } catch { IDmbProvider providerToDispose = windowsProvider ?? compileJobProvider; providerToDispose.Dispose(); throw; } pendingSwappable?.Dispose(); pendingSwappable = windowsProvider; }
/// <summary> /// Construct a <see cref="ReattachInformation"/> from a given <paramref name="copy"/> and <paramref name="dmb"/> /// </summary> /// <param name="copy">The <see cref="Models.ReattachInformation"/> to copy values from</param> /// <param name="dmb">The value of <see cref="Dmb"/></param> /// <param name="topicRequestTimeout">The value of <see cref="TopicRequestTimeout"/>.</param> public ReattachInformation( Models.ReattachInformation copy, IDmbProvider dmb, TimeSpan topicRequestTimeout) : base(copy) { Dmb = dmb ?? throw new ArgumentNullException(nameof(dmb)); TopicRequestTimeout = topicRequestTimeout; runtimeInformationLock = new object(); }
/// <inheritdoc /> protected override async Task HandleNewDmbAvailable(CancellationToken cancellationToken) { IDmbProvider compileJobProvider = DmbFactory.LockNextDmb(1); if (compileJobProvider.CompileJob.ByondVersion != ActiveCompileJob.ByondVersion) { // have to do a graceful restart Logger.LogDebug( "Not swapping to new compile job {0} as it uses a different BYOND version ({1}) than what is currently active {2}. Queueing graceful restart instead...", compileJobProvider.CompileJob.Id, compileJobProvider.CompileJob.ByondVersion, ActiveCompileJob.ByondVersion); compileJobProvider.Dispose(); await base.HandleNewDmbAvailable(cancellationToken).ConfigureAwait(false); return; } WindowsSwappableDmbProvider windowsProvider = null; bool suspended = false; try { windowsProvider = new WindowsSwappableDmbProvider(compileJobProvider, ioManager, symlinkFactory); Logger.LogDebug("Swapping to compile job {0}...", windowsProvider.CompileJob.Id); try { Server.Suspend(); suspended = true; } catch (Exception ex) { Logger.LogWarning("Exception while suspending server: {0}", ex); } await windowsProvider.MakeActive(cancellationToken).ConfigureAwait(false); } catch (Exception ex) { Logger.LogError("Exception while swapping: {0}", ex); IDmbProvider providerToDispose = windowsProvider ?? compileJobProvider; providerToDispose.Dispose(); throw; } // Let this throw hard if it fails if (suspended) { Server.Resume(); } pendingSwappable?.Dispose(); pendingSwappable = windowsProvider; }
/// <summary> /// Construct a <see cref="WatchdogReattachInformation"/> from a given <paramref name="copy"/> with a given <paramref name="dmbAlpha"/> and <paramref name="dmbBravo"/> /// </summary> /// <param name="copy">The <see cref="WatchdogReattachInformationBase"/> to copy information from</param> /// <param name="dmbAlpha">The <see cref="IDmbProvider"/> used to build <see cref="Alpha"/></param> /// <param name="dmbBravo">The <see cref="IDmbProvider"/> used to build <see cref="Bravo"/></param> public WatchdogReattachInformation(Models.WatchdogReattachInformation copy, IDmbProvider dmbAlpha, IDmbProvider dmbBravo) : base(copy) { if (copy.Alpha != null) { Alpha = new ReattachInformation(copy.Alpha, dmbAlpha); } if (copy.Bravo != null) { Bravo = new ReattachInformation(copy.Bravo, dmbBravo); } }
/// <inheritdoc /> protected override async Task DisposeAndNullControllersImpl() { await base.DisposeAndNullControllersImpl().ConfigureAwait(false); // If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again. ActiveSwappable = null; pendingSwappable?.Dispose(); pendingSwappable = null; startupDmbProvider?.Dispose(); startupDmbProvider = null; }
/// <inheritdoc /> public void ReplaceDmbProvider(IDmbProvider dmbProvider) { #pragma warning disable IDE0016 // Use 'throw' expression if (dmbProvider == null) { throw new ArgumentNullException(nameof(dmbProvider)); } #pragma warning restore IDE0016 // Use 'throw' expression reattachInformation.Dmb.Dispose(); reattachInformation.Dmb = dmbProvider; }
/// <inheritdoc /> protected override void DisposeAndNullControllersImpl() { base.DisposeAndNullControllersImpl(); // If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again. activeSwappable = null; pendingSwappable?.Dispose(); pendingSwappable = null; startupDmbProvider?.Dispose(); startupDmbProvider = null; }
/// <inheritdoc /> protected override async Task InitControllers(Task chatTask, ReattachInformation reattachInfo, CancellationToken cancellationToken) { try { await base.InitControllers(chatTask, reattachInfo, cancellationToken).ConfigureAwait(false); } finally { // Then we move it back and apply the symlink if (hardLinkedDmb != null) { try { Logger.LogTrace("Unhardlinking compile job..."); Server?.Suspend(); var hardLink = hardLinkedDmb.Directory; var originalPosition = hardLinkedDmb.CompileJob.DirectoryName.ToString(); await GameIOManager.MoveDirectory( hardLink, originalPosition, default) .ConfigureAwait(false); } catch (Exception ex) { Logger.LogError( ex, "Failed to un-hard link compile job #{0} ({1})", hardLinkedDmb.CompileJob.Id, hardLinkedDmb.CompileJob.DirectoryName); } hardLinkedDmb = null; } } if (reattachInfo != null) { Logger.LogTrace("Skipping symlink due to reattach"); return; } Logger.LogTrace("Symlinking compile job..."); await ActiveSwappable.MakeActive(cancellationToken).ConfigureAwait(false); Server.Resume(); }
/// <inheritdoc /> public async Task LoadCompileJob(CompileJob job, CancellationToken cancellationToken) { if (job == null) { throw new ArgumentNullException(nameof(job)); } var newProvider = await FromCompileJob(job, cancellationToken).ConfigureAwait(false); if (newProvider == null) { return; } lock (this) { nextDmbProvider?.Dispose(); nextDmbProvider = newProvider; newerDmbTcs.SetResult(nextDmbProvider); newerDmbTcs = new TaskCompletionSource <object>(); } }
/// <inheritdoc /> protected override async Task <IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken) { Debug.Assert(activeSwappable == null, "Expected swappableDmbProvider to be null!"); Logger.LogTrace("Prep for server launch. pendingSwappable is {0}avaiable", pendingSwappable == null ? "not " : String.Empty); activeSwappable = pendingSwappable ?? new WindowsSwappableDmbProvider(dmbToUse, ioManager, symlinkFactory); pendingSwappable = null; try { await activeSwappable.MakeActive(cancellationToken).ConfigureAwait(false); } catch { // We won't worry about disposing activeSwappable here as we can't dispose dmbToUse here. activeSwappable = null; throw; } return(activeSwappable); }
/// <summary> /// Initializes a new isntance of the <see cref="ReattachInformation"/> <see langword="class"/>. /// </summary> /// <param name="dmb">The value of <see cref="Dmb"/>.</param> /// <param name="process">The <see cref="IProcess"/> used to get the <see cref="ReattachInformationBase.ProcessId"/>.</param> /// <param name="runtimeInformation">The value of <see cref="RuntimeInformation"/>.</param> /// <param name="accessIdentifier">The value of <see cref="Interop.DMApiParameters.AccessIdentifier"/>.</param> /// <param name="port">The value of <see cref="ReattachInformationBase.Port"/>.</param> internal ReattachInformation( IDmbProvider dmb, IProcess process, RuntimeInformation runtimeInformation, string accessIdentifier, ushort port) { Dmb = dmb ?? throw new ArgumentNullException(nameof(dmb)); ProcessId = process?.Id ?? throw new ArgumentNullException(nameof(process)); RuntimeInformation = runtimeInformation ?? throw new ArgumentNullException(nameof(runtimeInformation)); if (!runtimeInformation.SecurityLevel.HasValue) { throw new ArgumentException("runtimeInformation must have a valid SecurityLevel!", nameof(runtimeInformation)); } AccessIdentifier = accessIdentifier ?? throw new ArgumentNullException(nameof(accessIdentifier)); LaunchSecurityLevel = runtimeInformation.SecurityLevel.Value; Port = port; runtimeInformationLock = new object(); }
/// <summary> /// Initializes a new instance of the <see cref="RuntimeInformation"/> <see langword="class"/>. /// </summary> /// <param name="chatTrackingContext">The <see cref="IChatTrackingContext"/> to use.</param> /// <param name="dmbProvider">The <see cref="IDmbProvider"/> to get revision information from.</param> /// <param name="serverVersion">The value of <see cref="ServerVersion"/>.</param> /// <param name="instanceName">The value of <see cref="InstanceName"/>.</param> /// <param name="securityLevel">The value of <see cref="SecurityLevel"/>.</param> /// <param name="serverPort">The value of <see cref="ServerPort"/>.</param> /// <param name="apiValidateOnly">The value of <see cref="ApiValidateOnly"/>.</param> public RuntimeInformation( IChatTrackingContext chatTrackingContext, IDmbProvider dmbProvider, Version serverVersion, string instanceName, DreamDaemonSecurity?securityLevel, ushort serverPort, bool apiValidateOnly) : base(chatTrackingContext?.Channels ?? throw new ArgumentNullException(nameof(chatTrackingContext))) { if (dmbProvider == null) { throw new ArgumentNullException(nameof(dmbProvider)); } ServerVersion = serverVersion ?? throw new ArgumentNullException(nameof(serverVersion)); Revision = new Api.Models.Internal.RevisionInformation { CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha, Timestamp = dmbProvider.CompileJob.RevisionInformation.Timestamp, OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha }; TestMerges = (IReadOnlyCollection <TestMergeInformation>)dmbProvider .CompileJob .RevisionInformation .ActiveTestMerges? .Select(x => x.TestMerge) .Select(x => new TestMergeInformation(x, Revision)) .ToList() ?? Array.Empty <TestMergeInformation>(); InstanceName = instanceName ?? throw new ArgumentNullException(nameof(instanceName)); SecurityLevel = securityLevel; ServerPort = serverPort; ApiValidateOnly = apiValidateOnly; }
/// <inheritdoc /> protected override async Task InitialLink(CancellationToken cancellationToken) { // The logic to check for an active live directory is in SwappableDmbProvider, so we just do it again here for safety Logger.LogTrace("Hard linking compile job..."); // Symlinks are counted as a file on linux?? if (await GameIOManager.DirectoryExists(ActiveSwappable.Directory, cancellationToken).ConfigureAwait(false)) { await GameIOManager.DeleteDirectory(ActiveSwappable.Directory, cancellationToken).ConfigureAwait(false); } else { await GameIOManager.DeleteFile(ActiveSwappable.Directory, cancellationToken).ConfigureAwait(false); } // Instead of symlinking to begin with we actually rename the directory await GameIOManager.MoveDirectory( ActiveSwappable.CompileJob.DirectoryName.ToString(), ActiveSwappable.Directory, cancellationToken) .ConfigureAwait(false); hardLinkedDmb = ActiveSwappable; }
/// <summary> /// Prepare the server to launch a new instance with the <see cref="WatchdogBase.ActiveLaunchParameters"/> and a given <paramref name="dmbToUse"/>. /// </summary> /// <param name="dmbToUse">The <see cref="IDmbProvider"/> to be launched. Will not be disposed by this function.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param> /// <returns>A <see cref="Task{TResult}"/> resulting in the modified <see cref="IDmbProvider"/> to be used.</returns> protected virtual Task <IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken) => Task.FromResult(dmbToUse);
/// <summary> /// Construct a <see cref="ReattachInformation"/> from a given <paramref name="copy"/> and <paramref name="dmb"/> /// </summary> /// <param name="copy">The <see cref="Models.ReattachInformation"/> to copy values from</param> /// <param name="dmb">The value of <see cref="Dmb"/></param> public ReattachInformation(Models.ReattachInformation copy, IDmbProvider dmb) : base(copy) { Dmb = dmb ?? throw new ArgumentNullException(nameof(dmb)); runtimeInformationLock = new object(); }
/// <summary> /// Initializes a new instance of the <see cref="WindowsSwappableDmbProvider"/> <see langword="class"/>. /// </summary> /// <param name="baseProvider">The value of <see cref="baseProvider"/>.</param> /// <param name="ioManager">The value of <see cref="ioManager"/>.</param> /// <param name="symlinkFactory">The value of <see cref="symlinkFactory"/>.</param> public WindowsSwappableDmbProvider(IDmbProvider baseProvider, IIOManager ioManager, ISymlinkFactory symlinkFactory) { this.baseProvider = baseProvider ?? throw new ArgumentNullException(nameof(baseProvider)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory)); }
/// <inheritdoc /> public ISessionController CreateDeadSession(IDmbProvider dmbProvider) => new DeadSessionController(dmbProvider);
/// <inheritdoc /> #pragma warning disable CA1506 // TODO: Decomplexify public async Task <ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken) { var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort; if (!portToUse.HasValue) { throw new InvalidOperationException("Given port is null!"); } var accessIdentifier = cryptographySuite.GetSecureString(); const string JsonPostfix = "tgs.json"; var basePath = primaryDirectory ? dmbProvider.PrimaryDirectory : dmbProvider.SecondaryDirectory; // delete all previous tgs json files var files = await ioManager.GetFilesWithExtension(basePath, JsonPostfix, cancellationToken).ConfigureAwait(false); await Task.WhenAll(files.Select(x => ioManager.DeleteFile(x, cancellationToken))).ConfigureAwait(false); // i changed this back from guids, hopefully i don't regret that string JsonFile(string name) => String.Format(CultureInfo.InvariantCulture, "{0}.{1}", name, JsonPostfix); var securityLevelToUse = launchParameters.SecurityLevel.Value; switch (dmbProvider.CompileJob.MinimumSecurityLevel) { case DreamDaemonSecurity.Ultrasafe: break; case DreamDaemonSecurity.Safe: if (securityLevelToUse == DreamDaemonSecurity.Ultrasafe) { securityLevelToUse = DreamDaemonSecurity.Safe; } break; case DreamDaemonSecurity.Trusted: securityLevelToUse = DreamDaemonSecurity.Trusted; break; default: throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel)); } // setup interop files var interopInfo = new JsonFile { AccessIdentifier = accessIdentifier, ApiValidateOnly = apiValidate, ChatChannelsJson = JsonFile("chat_channels"), ChatCommandsJson = JsonFile("chat_commands"), ServerCommandsJson = JsonFile("server_commands"), InstanceName = instance.Name, SecurityLevel = securityLevelToUse, Revision = new Api.Models.Internal.RevisionInformation { CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha, OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha } }; interopInfo.TestMerges.AddRange(dmbProvider.CompileJob.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => new Interop.TestMerge(x, interopInfo.Revision))); var interopJsonFile = JsonFile("interop"); var interopJson = JsonConvert.SerializeObject(interopInfo, new JsonSerializerSettings { ContractResolver = new CamelCasePropertyNamesContractResolver(), ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); var chatJsonTrackingTask = chat.TrackJsons(basePath, interopInfo.ChatChannelsJson, interopInfo.ChatCommandsJson, cancellationToken); await ioManager.WriteAllBytes(ioManager.ConcatPath(basePath, interopJsonFile), Encoding.UTF8.GetBytes(interopJson), cancellationToken).ConfigureAwait(false); var chatJsonTrackingContext = await chatJsonTrackingTask.ConfigureAwait(false); try { // get the byond lock var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false); try { // create interop context var context = new CommContext(ioManager, loggerFactory.CreateLogger <CommContext>(), basePath, interopInfo.ServerCommandsJson); try { // set command line options // more sanitization here cause it uses the same scheme var parameters = String.Format(CultureInfo.InvariantCulture, "{2}={0}&{3}={1}", byondTopicSender.SanitizeString(application.Version.ToString()), byondTopicSender.SanitizeString(interopJsonFile), byondTopicSender.SanitizeString(Constants.DMParamHostVersion), byondTopicSender.SanitizeString(Constants.DMParamInfoJson)); var visibility = apiValidate ? "invisible" : "public"; // important to run on all ports to allow port changing var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -{3} -{5} -public -params \"{4}\"", dmbProvider.DmbName, primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort, launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty, SecurityWord(securityLevelToUse), parameters, visibility); // See https://github.com/tgstation/tgstation-server/issues/719 var noShellExecute = !platformIdentifier.IsWindows; // launch dd var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments, noShellExecute: noShellExecute); try { networkPromptReaper.RegisterProcess(process); // return the session controller for it var result = new SessionController(new ReattachInformation { AccessIdentifier = accessIdentifier, Dmb = dmbProvider, IsPrimary = primaryDirectory, Port = portToUse.Value, ProcessId = process.Id, ChatChannelsJson = interopInfo.ChatChannelsJson, ChatCommandsJson = interopInfo.ChatCommandsJson, ServerCommandsJson = interopInfo.ServerCommandsJson, }, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger <SessionController>(), launchParameters.SecurityLevel, launchParameters.StartupTimeout); // writeback launch parameter's fixed security level launchParameters.SecurityLevel = securityLevelToUse; return(result); } catch { process.Dispose(); throw; } } catch { context.Dispose(); throw; } } catch { if (currentByondLock == null) { byondLock.Dispose(); } throw; } } catch { chatJsonTrackingContext.Dispose(); throw; } }