/// <summary> /// Construct a <see cref="SessionController"/> /// </summary> /// <param name="reattachInformation">The value of <see cref="reattachInformation"/></param> /// <param name="process">The value of <see cref="process"/></param> /// <param name="byondLock">The value of <see cref="byondLock"/></param> /// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param> /// <param name="interopContext">The value of <see cref="interopContext"/></param> /// <param name="chat">The value of <see cref="chat"/></param> /// <param name="chatJsonTrackingContext">The value of <see cref="chatJsonTrackingContext"/></param> /// <param name="logger">The value of <see cref="logger"/></param> /// <param name="launchSecurityLevel">The value of <see cref="launchSecurityLevel"/></param> /// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/></param> public SessionController( ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger <SessionController> logger, DreamDaemonSecurity?launchSecurityLevel, uint?startupTimeout) { this.chatJsonTrackingContext = chatJsonTrackingContext; // null valid this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.process = process ?? throw new ArgumentNullException(nameof(process)); this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock)); this.interopContext = interopContext ?? throw new ArgumentNullException(nameof(interopContext)); this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.launchSecurityLevel = launchSecurityLevel; interopContext.RegisterHandler(this); portClosedForReboot = false; disposed = false; apiValidationStatus = ApiValidationStatus.NeverValidated; released = false; rebootTcs = new TaskCompletionSource <object>(); process.Lifetime.ContinueWith(x => chatJsonTrackingContext.Active = false, TaskScheduler.Current); async Task <LaunchResult> GetLaunchResult() { var startTime = DateTimeOffset.Now; Task toAwait = process.Startup; if (startupTimeout.HasValue) { toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime)); } await toAwait.ConfigureAwait(false); var result = new LaunchResult { ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null, StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null }; return(result); } LaunchResult = GetLaunchResult(); logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port); }
/// <summary> /// Construct a <see cref="SessionController"/> /// </summary> /// <param name="reattachInformation">The value of <see cref="reattachInformation"/></param> /// <param name="process">The value of <see cref="process"/></param> /// <param name="byondLock">The value of <see cref="byondLock"/></param> /// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param> /// <param name="bridgeRegistrar">The <see cref="IBridgeRegistrar"/> used to populate <see cref="bridgeRegistration"/>.</param> /// <param name="chat">The value of <see cref="chat"/></param> /// <param name="chatTrackingContext">The value of <see cref="chatTrackingContext"/></param> /// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> for the <see cref="SessionController"/>.</param> /// <param name="logger">The value of <see cref="logger"/></param> /// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/></param> /// <param name="reattached">If this is a reattached session.</param> public SessionController( ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, ITopicClient byondTopicSender, IChatTrackingContext chatTrackingContext, IBridgeRegistrar bridgeRegistrar, IChatManager chat, IAssemblyInformationProvider assemblyInformationProvider, ILogger <SessionController> logger, uint?startupTimeout, bool reattached) { this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); this.process = process ?? throw new ArgumentNullException(nameof(process)); this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.chatTrackingContext = chatTrackingContext ?? throw new ArgumentNullException(nameof(chatTrackingContext)); bridgeRegistration = bridgeRegistrar?.RegisterHandler(this) ?? throw new ArgumentNullException(nameof(bridgeRegistrar)); this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.chatTrackingContext.SetChannelSink(this); portClosedForReboot = false; disposed = false; apiValidationStatus = ApiValidationStatus.NeverValidated; released = false; rebootTcs = new TaskCompletionSource <object>(); primeTcs = new TaskCompletionSource <object>(); reattachTopicCts = new CancellationTokenSource(); synchronizationLock = new object(); _ = process.Lifetime.ContinueWith( x => { if (!disposed) { reattachTopicCts.Cancel(); } chatTrackingContext.Active = false; }, TaskScheduler.Current); LaunchResult = GetLaunchResult( assemblyInformationProvider, startupTimeout, reattached); logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port); }
/// <summary> /// Construct a <see cref="SessionController"/> /// </summary> /// <param name="reattachInformation">The value of <see cref="reattachInformation"/></param> /// <param name="process">The value of <see cref="process"/></param> /// <param name="byondLock">The value of <see cref="byondLock"/></param> /// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param> /// <param name="interopContext">The value of <see cref="interopContext"/></param> /// <param name="chat">The value of <see cref="chat"/></param> /// <param name="chatJsonTrackingContext">The value of <see cref="chatJsonTrackingContext"/></param> /// <param name="logger">The value of <see cref="logger"/></param> /// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/></param> public SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger <SessionController> logger, uint?startupTimeout) { this.chatJsonTrackingContext = chatJsonTrackingContext; //null valid this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.process = process ?? throw new ArgumentNullException(nameof(process)); this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock)); this.interopContext = interopContext ?? throw new ArgumentNullException(nameof(interopContext)); this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); interopContext.RegisterHandler(this); portClosedForReboot = false; disposed = false; apiValidated = false; released = false; rebootTcs = new TaskCompletionSource <object>(); async Task <LaunchResult> GetLaunchResult() { var startTime = DateTimeOffset.Now; Task toAwait = process.Startup; if (startupTimeout.HasValue) { toAwait = Task.WhenAny(process.Startup, Task.Delay(startTime.AddSeconds(startupTimeout.Value) - startTime)); } await toAwait.ConfigureAwait(false); var result = new LaunchResult { ExitCode = process.Lifetime.IsCompleted ? (int?)await process.Lifetime.ConfigureAwait(false) : null, StartupTime = process.Startup.IsCompleted ? (TimeSpan?)(DateTimeOffset.Now - startTime) : null }; return(result); }; LaunchResult = GetLaunchResult(); }
/// <summary> /// Executes and populate a given <paramref name="job"/> /// </summary> /// <param name="job">The <see cref="Models.CompileJob"/> to run and populate</param> /// <param name="dreamMakerSettings">The <see cref="Api.Models.DreamMaker"/> settings to use</param> /// <param name="byondLock">The <see cref="IByondExecutableLock"/> to use</param> /// <param name="repository">The <see cref="IRepository"/> to use</param> /// <param name="apiValidateTimeout">The timeout for validating the DMAPI</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> async Task RunCompileJob(Models.CompileJob job, Api.Models.DreamMaker dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, uint apiValidateTimeout, CancellationToken cancellationToken) { var jobPath = job.DirectoryName.ToString(); logger.LogTrace("Compile output GUID: {0}", jobPath); try { var dirA = ioManager.ConcatPath(jobPath, ADirectoryName); var dirB = ioManager.ConcatPath(jobPath, BDirectoryName); // copy the repository logger.LogTrace("Copying repository to game directory..."); var resolvedADirectory = ioManager.ResolvePath(dirA); var repoOrigin = repository.Origin; using (repository) await repository.CopyTo(resolvedADirectory, cancellationToken).ConfigureAwait(false); // repository closed now // run precompile scripts await eventConsumer.HandleEvent(EventType.CompileStart, new List <string> { resolvedADirectory, repoOrigin }, cancellationToken).ConfigureAwait(false); // determine the dme if (job.DmeName == null) { logger.LogTrace("Searching for available .dmes..."); var foundPaths = await ioManager.GetFilesWithExtension(dirA, DmeExtension, cancellationToken).ConfigureAwait(false); var foundPath = foundPaths.FirstOrDefault(); if (foundPath == default) { throw new JobException("Unable to find any .dme!"); } var dmeWithExtension = ioManager.GetFileName(foundPath); job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1); } else { var targetDme = ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)); var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken).ConfigureAwait(false); if (!targetDmeExists) { throw new JobException("Unable to locate specified .dme!"); } } logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName); await ModifyDme(job, cancellationToken).ConfigureAwait(false); // run compiler var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); // verify api try { if (exitCode != 0) { throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output)); } await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false); } catch (JobException) { // DD never validated or compile failed await eventConsumer.HandleEvent(EventType.CompileFailure, new List <string> { resolvedADirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false); throw; } logger.LogTrace("Running post compile event..."); await eventConsumer.HandleEvent(EventType.CompileComplete, new List <string> { resolvedADirectory }, cancellationToken).ConfigureAwait(false); logger.LogTrace("Duplicating compiled game..."); // duplicate the dmb et al await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false); logger.LogTrace("Applying static game file symlinks..."); // symlink in the static data var symATask = configuration.SymlinkStaticFilesTo(resolvedADirectory, cancellationToken); var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken); await Task.WhenAll(symATask, symBTask).ConfigureAwait(false); await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deployment complete!{0}", watchdog.Running ? " Changes will be applied on next server reboot." : String.Empty), cancellationToken).ConfigureAwait(false); logger.LogDebug("Compile complete!"); } catch (Exception e) { await CleanupFailedCompile(job, e is OperationCanceledException, cancellationToken).ConfigureAwait(false); throw; } }
/// <summary> /// Send a message to <see cref="chat"/> about a deployment /// </summary> /// <param name="revisionInformation">The <see cref="Models.RevisionInformation"/> for the deployment</param> /// <param name="byondLock">The <see cref="IByondExecutableLock"/> for the deployment</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> async Task SendDeploymentMessage(Models.RevisionInformation revisionInformation, IByondExecutableLock byondLock, CancellationToken cancellationToken) { var commitInsert = revisionInformation.CommitSha.Substring(0, 7); string remoteCommitInsert; if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha) { commitInsert = String.Format(CultureInfo.InvariantCulture, "^{0}", commitInsert); remoteCommitInsert = String.Empty; } else { remoteCommitInsert = String.Format(CultureInfo.InvariantCulture, ". Remote commit: ^{0}", revisionInformation.OriginCommitSha.Substring(0, 7)); } var testmergeInsert = revisionInformation.ActiveTestMerges.Count == 0 ? String.Empty : String.Format(CultureInfo.InvariantCulture, " (Test Merges: {0})", String.Join(", ", revisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => { var result = String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7)); if (x.Comment != null) { result += String.Format(CultureInfo.InvariantCulture, " ({0})", x.Comment); } return(result); }))); await chat.SendUpdateMessage(String.Format(CultureInfo.InvariantCulture, "Deploying revision: {0}{1}{2} BYOND Version: {3}", commitInsert, testmergeInsert, remoteCommitInsert, byondLock.Version), cancellationToken).ConfigureAwait(false); }
/// <summary> /// Run a quick DD instance to test the DMAPI is installed on the target code /// </summary> /// <param name="timeout">The timeout in seconds for validation</param> /// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to use to validate the API</param> /// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param> /// <param name="byondLock">The current <see cref="IByondExecutableLock"/></param> /// <param name="portToUse">The port to use for API validation</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken) { logger.LogTrace("Verifying DMAPI..."); var launchParameters = new DreamDaemonLaunchParameters { AllowWebClient = false, PrimaryPort = portToUse, SecurityLevel = securityLevel, StartupTimeout = timeout }; var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName); job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout); using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job)) using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false)) { var launchResult = await controller.LaunchResult.ConfigureAwait(false); var now = DateTimeOffset.Now; if (now < timeoutAt && launchResult.StartupTime.HasValue) { var timeoutTask = Task.Delay(timeoutAt - now, cancellationToken); await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } if (controller.Lifetime.IsCompleted) { var validationStatus = controller.ApiValidationStatus; logger.LogTrace("API validation status: {0}", validationStatus); switch (validationStatus) { case ApiValidationStatus.RequiresUltrasafe: job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe; return; case ApiValidationStatus.RequiresSafe: if (securityLevel == DreamDaemonSecurity.Ultrasafe) { throw new JobException("This game must be run with at least the 'Safe' DreamDaemon security level!"); } job.MinimumSecurityLevel = DreamDaemonSecurity.Safe; return; case ApiValidationStatus.RequiresTrusted: if (securityLevel != DreamDaemonSecurity.Trusted) { throw new JobException("This game must be run with at least the 'Trusted' DreamDaemon security level!"); } job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted; return; case ApiValidationStatus.NeverValidated: break; case ApiValidationStatus.BadValidationRequest: throw new JobException("Recieved an unrecognized API validation request from DreamDaemon!"); case ApiValidationStatus.UnaskedValidationRequest: default: throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Session controller returned unexpected ApiValidationStatus: {0}", validationStatus)); } } throw new JobException("DMAPI validation timed out!"); } }
/// <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; } }
/// <summary> /// Construct a <see cref="SessionController"/> /// </summary> /// <param name="reattachInformation">The value of <see cref="reattachInformation"/></param> /// <param name="metadata">The owning <see cref="Instance"/>.</param> /// <param name="process">The value of <see cref="process"/></param> /// <param name="byondLock">The value of <see cref="byondLock"/></param> /// <param name="byondTopicSender">The value of <see cref="byondTopicSender"/></param> /// <param name="bridgeRegistrar">The <see cref="IBridgeRegistrar"/> used to populate <see cref="bridgeRegistration"/>.</param> /// <param name="chat">The value of <see cref="chat"/></param> /// <param name="chatTrackingContext">The value of <see cref="chatTrackingContext"/></param> /// <param name="assemblyInformationProvider">The <see cref="IAssemblyInformationProvider"/> for the <see cref="SessionController"/>.</param> /// <param name="logger">The value of <see cref="logger"/></param> /// <param name="postLifetimeCallback">The <see cref="Func{TResult}"/> returning a <see cref="Task"/> to be run after the <paramref name="process"/> ends.</param> /// <param name="startupTimeout">The optional time to wait before failing the <see cref="LaunchResult"/></param> /// <param name="reattached">If this is a reattached session.</param> /// <param name="apiValidate">If this is a DMAPI validation session.</param> public SessionController( ReattachInformation reattachInformation, Api.Models.Instance metadata, IProcess process, IByondExecutableLock byondLock, ITopicClient byondTopicSender, IChatTrackingContext chatTrackingContext, IBridgeRegistrar bridgeRegistrar, IChatManager chat, IAssemblyInformationProvider assemblyInformationProvider, ILogger <SessionController> logger, Func <Task> postLifetimeCallback, uint?startupTimeout, bool reattached, bool apiValidate) { this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); this.process = process ?? throw new ArgumentNullException(nameof(process)); this.byondLock = byondLock ?? throw new ArgumentNullException(nameof(byondLock)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.chatTrackingContext = chatTrackingContext ?? throw new ArgumentNullException(nameof(chatTrackingContext)); if (bridgeRegistrar == null) { throw new ArgumentNullException(nameof(bridgeRegistrar)); } this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); portClosedForReboot = false; disposed = false; apiValidationStatus = ApiValidationStatus.NeverValidated; released = false; rebootTcs = new TaskCompletionSource <object>(); primeTcs = new TaskCompletionSource <object>(); initialBridgeRequestTcs = new TaskCompletionSource <object>(); reattachTopicCts = new CancellationTokenSource(); synchronizationLock = new object(); if (apiValidate || DMApiAvailable) { bridgeRegistration = bridgeRegistrar.RegisterHandler(this); this.chatTrackingContext.SetChannelSink(this); } else { logger.LogTrace( "Not registering session with {0} DMAPI version for interop!", reattachInformation.Dmb.CompileJob.DMApiVersion == null ? "no" : $"incompatible ({reattachInformation.Dmb.CompileJob.DMApiVersion})"); } async Task <int> WrapLifetime() { var exitCode = await process.Lifetime.ConfigureAwait(false); await postLifetimeCallback().ConfigureAwait(false); return(exitCode); } Lifetime = WrapLifetime(); LaunchResult = GetLaunchResult( assemblyInformationProvider, startupTimeout, reattached, apiValidate); logger.LogDebug( "Created session controller. CommsKey: {0}, Port: {1}", reattachInformation.AccessIdentifier, reattachInformation.Port); }
/// <summary> /// Executes and populate a given <paramref name="job"/> /// </summary> /// <param name="job">The <see cref="CompileJob"/> to run and populate</param> /// <param name="dreamMakerSettings">The <see cref="Api.Models.Internal.DreamMakerSettings"/> to use</param> /// <param name="byondLock">The <see cref="IByondExecutableLock"/> to use</param> /// <param name="repository">The <see cref="IRepository"/> to use</param> /// <param name="remoteDeploymentManager">The <see cref="IRemoteDeploymentManager"/> to use.</param> /// <param name="apiValidateTimeout">The timeout for validating the DMAPI</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> async Task RunCompileJob( Models.CompileJob job, Api.Models.Internal.DreamMakerSettings dreamMakerSettings, IByondExecutableLock byondLock, IRepository repository, IRemoteDeploymentManager remoteDeploymentManager, uint apiValidateTimeout, CancellationToken cancellationToken) { var outputDirectory = job.DirectoryName.ToString(); logger.LogTrace("Compile output GUID: {0}", outputDirectory); try { // copy the repository logger.LogTrace("Copying repository to game directory..."); var resolvedOutputDirectory = ioManager.ResolvePath(outputDirectory); var repoOrigin = repository.Origin; using (repository) await repository.CopyTo(resolvedOutputDirectory, cancellationToken).ConfigureAwait(false); // repository closed now // run precompile scripts await eventConsumer.HandleEvent( EventType.CompileStart, new List <string> { resolvedOutputDirectory, repoOrigin.ToString() }, cancellationToken) .ConfigureAwait(false); // determine the dme if (job.DmeName == null) { logger.LogTrace("Searching for available .dmes..."); var foundPaths = await ioManager.GetFilesWithExtension(resolvedOutputDirectory, DmeExtension, true, cancellationToken).ConfigureAwait(false); var foundPath = foundPaths.FirstOrDefault(); if (foundPath == default) { throw new JobException(ErrorCode.DreamMakerNoDme); } job.DmeName = foundPath.Substring( resolvedOutputDirectory.Length + 1, foundPath.Length - resolvedOutputDirectory.Length - DmeExtension.Length - 2); // +1 for . in extension } else { var targetDme = ioManager.ConcatPath(outputDirectory, String.Join('.', job.DmeName, DmeExtension)); var targetDmeExists = await ioManager.FileExists(targetDme, cancellationToken).ConfigureAwait(false); if (!targetDmeExists) { throw new JobException(ErrorCode.DreamMakerMissingDme); } } logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName); await ModifyDme(job, cancellationToken).ConfigureAwait(false); // run compiler var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); // verify api try { if (exitCode != 0) { throw new JobException( ErrorCode.DreamMakerExitCode, new JobException($"Exit code: {exitCode}{Environment.NewLine}{Environment.NewLine}{job.Output}")); } await VerifyApi( apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, dreamMakerSettings.RequireDMApiValidation.Value, cancellationToken) .ConfigureAwait(false); } catch (JobException) { // DD never validated or compile failed await eventConsumer.HandleEvent( EventType.CompileFailure, new List <string> { resolvedOutputDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken) .ConfigureAwait(false); throw; } await eventConsumer.HandleEvent(EventType.CompileComplete, new List <string> { resolvedOutputDirectory }, cancellationToken).ConfigureAwait(false); logger.LogTrace("Applying static game file symlinks..."); // symlink in the static data await configuration.SymlinkStaticFilesTo(resolvedOutputDirectory, cancellationToken).ConfigureAwait(false); logger.LogDebug("Compile complete!"); } catch (Exception ex) { await CleanupFailedCompile(job, remoteDeploymentManager, ex).ConfigureAwait(false); throw; } }
/// <summary> /// Run a quick DD instance to test the DMAPI is installed on the target code /// </summary> /// <param name="timeout">The timeout in seconds for validation</param> /// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to use to validate the API</param> /// <param name="job">The <see cref="CompileJob"/> for the operation</param> /// <param name="byondLock">The current <see cref="IByondExecutableLock"/></param> /// <param name="portToUse">The port to use for API validation</param> /// <param name="requireValidate">If the API validation is required to complete the deployment.</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task"/> representing the running operation</returns> async Task VerifyApi( uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, bool requireValidate, CancellationToken cancellationToken) { logger.LogTrace("Verifying {0}DMAPI...", requireValidate ? "required " : String.Empty); var launchParameters = new DreamDaemonLaunchParameters { AllowWebClient = false, Port = portToUse, SecurityLevel = securityLevel, StartupTimeout = timeout, TopicRequestTimeout = 0, // not used HeartbeatSeconds = 0 // not used }; job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider ApiValidationStatus validationStatus; using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job)) await using (var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, cancellationToken).ConfigureAwait(false)) { var launchResult = await controller.LaunchResult.ConfigureAwait(false); if (launchResult.StartupTime.HasValue) { await controller.Lifetime.WithToken(cancellationToken).ConfigureAwait(false); } if (!controller.Lifetime.IsCompleted) { await controller.DisposeAsync().ConfigureAwait(false); } validationStatus = controller.ApiValidationStatus; if (requireValidate && validationStatus == ApiValidationStatus.NeverValidated) { throw new JobException(ErrorCode.DreamMakerNeverValidated); } logger.LogTrace("API validation status: {0}", validationStatus); job.DMApiVersion = controller.DMApiVersion; } switch (validationStatus) { case ApiValidationStatus.RequiresUltrasafe: job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe; return; case ApiValidationStatus.RequiresSafe: job.MinimumSecurityLevel = DreamDaemonSecurity.Safe; return; case ApiValidationStatus.RequiresTrusted: job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted; return; case ApiValidationStatus.NeverValidated: if (requireValidate) { throw new JobException(ErrorCode.DreamMakerNeverValidated); } job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe; break; case ApiValidationStatus.BadValidationRequest: case ApiValidationStatus.Incompatible: throw new JobException(ErrorCode.DreamMakerInvalidValidation); case ApiValidationStatus.UnaskedValidationRequest: default: throw new InvalidOperationException( $"Session controller returned unexpected ApiValidationStatus: {validationStatus}"); } }
/// <summary> /// Run a quick DD instance to test the DMAPI is installed on the target code /// </summary> /// <param name="timeout">The timeout in seconds for validation</param> /// <param name="securityLevel">The <see cref="DreamDaemonSecurity"/> level to use to validate the API</param> /// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param> /// <param name="byondLock">The current <see cref="IByondExecutableLock"/></param> /// <param name="portToUse">The port to use for API validation</param> /// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param> /// <returns>A <see cref="Task{TResult}"/> resulting in <see langword="true"/> if the DMAPI was successfully validated, <see langword="false"/> otherwise</returns> async Task <bool> VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken) { logger.LogTrace("Verifying DMAPI..."); var launchParameters = new DreamDaemonLaunchParameters { AllowWebClient = false, PrimaryPort = portToUse, SecurityLevel = securityLevel, //all it needs to read the file and exit StartupTimeout = timeout }; var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName); var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job); var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout); using (var controller = await sessionControllerFactory.LaunchNew(launchParameters, provider, byondLock, true, true, true, cancellationToken).ConfigureAwait(false)) { var launchResult = await controller.LaunchResult.ConfigureAwait(false); var now = DateTimeOffset.Now; if (now < timeoutAt && launchResult.StartupTime.HasValue) { var timeoutTask = Task.Delay(timeoutAt - now, cancellationToken); await Task.WhenAny(controller.Lifetime, timeoutTask).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); } if (!controller.Lifetime.IsCompleted) { logger.LogDebug("API validation timed out!"); return(false); } var validated = controller.ApiValidated; logger.LogTrace("API valid: {0}", validated); return(validated); } }