tgstation-server  4.3.2
The /tg/station 13 server suite
SessionControllerFactory.cs
Go to the documentation of this file.
1 using Byond.TopicSender;
2 using Microsoft.Extensions.Logging;
3 using System;
4 using System.Globalization;
5 using System.Linq;
6 using System.Net;
7 using System.Net.Sockets;
8 using System.Text;
9 using System.Threading;
10 using System.Threading.Tasks;
11 using Tgstation.Server.Api;
20 using Tgstation.Server.Host.IO;
24 
25 namespace Tgstation.Server.Host.Components.Session
26 {
29  {
34 
38  readonly IByondManager byond;
39 
43  readonly ITopicClient byondTopicSender;
44 
49 
54 
59 
63  readonly IChatManager chat;
64 
69 
74 
79 
84 
88  readonly ILoggerFactory loggerFactory;
89 
93  readonly ILogger<SessionControllerFactory> logger;
94 
98  readonly Api.Models.Instance instance;
99 
105  static string SecurityWord(DreamDaemonSecurity securityLevel)
106  {
107  return securityLevel switch
108  {
109  DreamDaemonSecurity.Safe => "safe",
110  DreamDaemonSecurity.Trusted => "trusted",
111  DreamDaemonSecurity.Ultrasafe => "ultrasafe",
112  _ => throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel)),
113  };
114  }
115 
120  static void PortBindTest(ushort port)
121  {
122  using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
123 
124  try
125  {
126  socket.Bind(new IPEndPoint(IPAddress.Any, port));
127  }
128  catch (Exception ex)
129  {
130  throw new JobException(ErrorCode.DreamDaemonPortInUse, ex);
131  }
132  }
133 
152  IProcessExecutor processExecutor,
153  IByondManager byond,
154  ITopicClient byondTopicSender,
155  ICryptographySuite cryptographySuite,
156  IAssemblyInformationProvider assemblyInformationProvider,
157  IIOManager ioManager,
158  IChatManager chat,
159  INetworkPromptReaper networkPromptReaper,
160  IPlatformIdentifier platformIdentifier,
161  IBridgeRegistrar bridgeRegistrar,
162  IServerPortProvider serverPortProvider,
163  ILoggerFactory loggerFactory,
164  ILogger<SessionControllerFactory> logger,
165  Api.Models.Instance instance)
166  {
167  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
168  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
169  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
170  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
171  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
172  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
173  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
174  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
175  this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
176  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
177  this.bridgeRegistrar = bridgeRegistrar ?? throw new ArgumentNullException(nameof(bridgeRegistrar));
178  this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
179  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
180  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
181  }
182 
184  #pragma warning disable CA1506 // TODO: Decomplexify
185  public async Task<ISessionController> LaunchNew(
186  IDmbProvider dmbProvider,
187  IByondExecutableLock currentByondLock,
188  DreamDaemonLaunchParameters launchParameters,
189  bool primaryPort,
190  bool primaryDirectory,
191  bool apiValidate,
192  CancellationToken cancellationToken)
193  {
194  var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort;
195  if (!portToUse.HasValue)
196  throw new InvalidOperationException("Given port is null!");
197 
198  var basePath = primaryDirectory ? dmbProvider.PrimaryDirectory : dmbProvider.SecondaryDirectory;
199 
200  switch (dmbProvider.CompileJob.MinimumSecurityLevel)
201  {
202  case DreamDaemonSecurity.Ultrasafe:
203  break;
204  case DreamDaemonSecurity.Safe:
205  if (launchParameters.SecurityLevel == DreamDaemonSecurity.Ultrasafe)
206  launchParameters.SecurityLevel = DreamDaemonSecurity.Safe;
207  break;
208  case DreamDaemonSecurity.Trusted:
209  launchParameters.SecurityLevel = DreamDaemonSecurity.Trusted;
210  break;
211  default:
212  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel));
213  }
214 
215  var chatTrackingContext = chat.CreateTrackingContext();
216  try
217  {
218  // get the byond lock
219  var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
220  try
221  {
222  if (launchParameters.SecurityLevel == DreamDaemonSecurity.Trusted)
223  await byondLock.TrustDmbPath(ioManager.ConcatPath(basePath, dmbProvider.DmbName), cancellationToken).ConfigureAwait(false);
224 
225  PortBindTest(portToUse.Value);
226  await CheckPagerIsNotRunning(cancellationToken).ConfigureAwait(false);
227 
228  var accessIdentifier = cryptographySuite.GetSecureString();
229 
230  // set command line options
231  // more sanitization here cause it uses the same scheme
232  var parameters = $"{DMApiConstants.ParamApiVersion}={byondTopicSender.SanitizeString(DMApiConstants.Version.Semver().ToString())}&{byondTopicSender.SanitizeString(DMApiConstants.ParamServerPort)}={serverPortProvider.HttpApiPort}&{byondTopicSender.SanitizeString(DMApiConstants.ParamAccessIdentifier)}={byondTopicSender.SanitizeString(accessIdentifier)}";
233 
234  var visibility = apiValidate ? "invisible" : "public";
235 
236  // important to run on all ports to allow port changing
237  Guid? logFileGuid = null;
238  var arguments = String.Format(
239  CultureInfo.InvariantCulture,
240  "{0} -port {1} -ports 1-65535 {2}-close -{3} -{4}{5} -public -params \"{6}\"",
241  dmbProvider.DmbName,
242  portToUse,
243  launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
244  SecurityWord(launchParameters.SecurityLevel.Value),
245  visibility,
246  platformIdentifier.IsWindows
247  ? $" -log {logFileGuid = Guid.NewGuid()}"
248  : String.Empty, // Just use stdout on linux
249  parameters);
250 
251  // See https://github.com/tgstation/tgstation-server/issues/719
252  var noShellExecute = !platformIdentifier.IsWindows;
253 
254  // launch dd
255  var process = processExecutor.LaunchProcess(
256  byondLock.DreamDaemonPath,
257  basePath,
258  arguments,
259  noShellExecute,
260  noShellExecute,
261  noShellExecute: noShellExecute);
262 
263  async Task<string> GetDDOutput()
264  {
265  if (!platformIdentifier.IsWindows)
266  return process.GetCombinedOutput();
267 
268  var logFilePath = ioManager.ConcatPath(basePath, logFileGuid.ToString());
269  try
270  {
271  var dreamDaemonLogBytes = await ioManager.ReadAllBytes(
272  logFilePath,
273  default)
274  .ConfigureAwait(false);
275 
276  return Encoding.UTF8.GetString(dreamDaemonLogBytes);
277  }
278  finally
279  {
280  try
281  {
282  await ioManager.DeleteFile(logFilePath, default).ConfigureAwait(false);
283  }
284  catch (Exception ex)
285  {
286  logger.LogWarning("Failed to delete DreamDaemon log file {0}: {1}", logFilePath, ex);
287  }
288  }
289  }
290 
291  // Log DD output
292  _ = process.Lifetime.ContinueWith(
293  async x =>
294  {
295  try
296  {
297  var ddOutput = await GetDDOutput().ConfigureAwait(false);
298  logger.LogTrace(
299  "DreamDaemon Output:{0}{1}",
300  Environment.NewLine, ddOutput);
301  }
302  catch (Exception ex)
303  {
304  logger.LogWarning("Error reading DreamDaemon output: {0}", ex);
305  }
306  },
307  TaskScheduler.Current);
308 
309  try
310  {
311  networkPromptReaper.RegisterProcess(process);
312 
313  var runtimeInformation = CreateRuntimeInformation(
314  dmbProvider,
315  chatTrackingContext,
316  launchParameters.SecurityLevel.Value,
317  apiValidate);
318 
319  var reattachInformation = new ReattachInformation(
320  dmbProvider,
321  process,
322  runtimeInformation,
323  accessIdentifier,
324  portToUse.Value,
325  primaryDirectory);
326 
327  var sessionController = new SessionController(
328  reattachInformation,
329  instance,
330  process,
331  byondLock,
332  byondTopicSender,
333  chatTrackingContext,
334  bridgeRegistrar,
335  chat,
336  assemblyInformationProvider,
337  loggerFactory.CreateLogger<SessionController>(),
338  launchParameters.StartupTimeout,
339  false);
340 
341  return sessionController;
342  }
343  catch
344  {
345  process.Terminate();
346  process.Dispose();
347  throw;
348  }
349  }
350  catch
351  {
352  if (currentByondLock == null)
353  byondLock.Dispose();
354  throw;
355  }
356  }
357  catch
358  {
359  chatTrackingContext.Dispose();
360  throw;
361  }
362  }
363  #pragma warning restore CA1506
364 
366  public async Task<ISessionController> Reattach(
367  ReattachInformation reattachInformation,
368  CancellationToken cancellationToken)
369  {
370  if (reattachInformation == null)
371  throw new ArgumentNullException(nameof(reattachInformation));
372 
373  var chatTrackingContext = chat.CreateTrackingContext();
374  try
375  {
376  var byondLock = await byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
377  try
378  {
379  var process = processExecutor.GetProcess(reattachInformation.ProcessId);
380  if (process == null)
381  return null;
382 
383  try
384  {
385  networkPromptReaper.RegisterProcess(process);
386  var runtimeInformation = CreateRuntimeInformation(
387  reattachInformation.Dmb,
388  chatTrackingContext,
389  null,
390  false);
391  reattachInformation.SetRuntimeInformation(runtimeInformation);
392 
393  var controller = new SessionController(
394  reattachInformation,
395  instance,
396  process,
397  byondLock,
398  byondTopicSender,
399  chatTrackingContext,
400  bridgeRegistrar,
401  chat,
402  assemblyInformationProvider,
403  loggerFactory.CreateLogger<SessionController>(),
404  null,
405  true);
406 
407  process = null;
408  byondLock = null;
409  chatTrackingContext = null;
410 
411  return controller;
412  }
413  finally
414  {
415  process?.Dispose();
416  }
417  }
418  finally
419  {
420  byondLock?.Dispose();
421  }
422  }
423  finally
424  {
425  chatTrackingContext?.Dispose();
426  }
427  }
428 
430  public ISessionController CreateDeadSession(IDmbProvider dmbProvider) => new DeadSessionController(dmbProvider);
431 
441  IDmbProvider dmbProvider,
442  IChatTrackingContext chatTrackingContext,
443  DreamDaemonSecurity? securityLevel,
444  bool apiValidateOnly)
445  {
446  var revisionInfo = new Api.Models.Internal.RevisionInformation
447  {
448  CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha,
449  OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha
450  };
451 
452  var testMerges = dmbProvider
453  .CompileJob
456  .Select(x => x.TestMerge)
457  .Select(x => new TestMergeInformation(x, revisionInfo))
458  ?? Enumerable.Empty<TestMergeInformation>();
459 
460  return new RuntimeInformation(
461  assemblyInformationProvider,
462  serverPortProvider,
463  testMerges,
464  chatTrackingContext.Channels,
465  instance,
466  revisionInfo,
467  securityLevel,
468  apiValidateOnly);
469  }
470 
476  async Task CheckPagerIsNotRunning(CancellationToken cancellationToken)
477  {
478  if (!platformIdentifier.IsWindows)
479  return;
480 
481  using var otherProcess = processExecutor.GetProcessByName("byond");
482  if (otherProcess == null)
483  return;
484 
485  var otherUsernameTask = otherProcess.GetExecutingUsername(cancellationToken);
486  using var ourProcess = processExecutor.GetCurrentProcess();
487  var ourUserName = await ourProcess.GetExecutingUsername(cancellationToken).ConfigureAwait(false);
488  var otherUserName = await otherUsernameTask.ConfigureAwait(false);
489 
490  if(otherUserName.Equals(ourUserName, StringComparison.Ordinal))
491  throw new JobException(ErrorCode.DeploymentPagerRunning);
492  }
493  }
494 }
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
readonly IIOManager ioManager
The IIOManager for the SessionControllerFactory
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
async Task< ISessionController > LaunchNew(IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, DreamDaemonLaunchParameters launchParameters, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
Create a ISessionController from a freshly launch DreamDaemon instance
SessionControllerFactory(IProcessExecutor processExecutor, IByondManager byond, ITopicClient byondTopicSender, ICryptographySuite cryptographySuite, IAssemblyInformationProvider assemblyInformationProvider, IIOManager ioManager, IChatManager chat, INetworkPromptReaper networkPromptReaper, IPlatformIdentifier platformIdentifier, IBridgeRegistrar bridgeRegistrar, IServerPortProvider serverPortProvider, ILoggerFactory loggerFactory, ILogger< SessionControllerFactory > logger, Api.Models.Instance instance)
Construct a SessionControllerFactory
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the SessionControllerFactory
readonly IChatManager chat
The IChatManager for the SessionControllerFactory
readonly INetworkPromptReaper networkPromptReaper
The INetworkPromptReaper for the SessionControllerFactory
RevisionInformation RevisionInformation
Git revision the compiler ran on. Not modifiable
Definition: CompileJob.cs:16
static string SecurityWord(DreamDaemonSecurity securityLevel)
Change a given securityLevel into the appropriate DreamDaemon command line word
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the SessionControllerFactory
DreamDaemonSecurity SecurityLevel
The DreamDaemonSecurity level of DreamDaemon
Parameters necessary for duplicating a ISessionController session
readonly ILogger< SessionControllerFactory > logger
The ILogger for the SessionControllerFactory
RuntimeInformation CreateRuntimeInformation(IDmbProvider dmbProvider, IChatTrackingContext chatTrackingContext, DreamDaemonSecurity?securityLevel, bool apiValidateOnly)
Create RuntimeInformation.
Contains various cryptographic functions
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the SessionControllerFactory
For managing connected chat services
Definition: IChatManager.cs:13
readonly ITopicClient byondTopicSender
The ITopicClient for the SessionControllerFactory
void SetRuntimeInformation(RuntimeInformation runtimeInformation)
Set the RuntimeInformation post construction.
DreamDaemonSecurity MinimumSecurityLevel
The minimum DreamDaemonSecurity required to run the CompileJob&#39;s output
Definition: CompileJob.cs:34
Version ByondVersion
The Byond.Version the CompileJob was made with
Definition: CompileJob.cs:21
Operation exceptions thrown from the context of a Models.Job
Definition: JobException.cs:9
ICollection< TestMerge > ActiveTestMerges
The TestMerges active in the RevisionInformation
string PrimaryDirectory
The primary game directory with a trailing directory separator
Definition: IDmbProvider.cs:19
string SecondaryDirectory
The secondary game directory with a trailing directory separator
Definition: IDmbProvider.cs:24
IReadOnlyCollection< ChannelRepresentation > Channels
IReadOnlyCollection<T> of ChannelRepresentations in the IChatTrackingContext.
async Task CheckPagerIsNotRunning(CancellationToken cancellationToken)
Make sure the BYOND pager is not running.
IDmbProvider Dmb
The IDmbProvider used by DreamDaemon
For managing the BYOND installation
readonly IServerPortProvider serverPortProvider
The IServerPortProvider for the SessionControllerFactory.
Representation of the initial data passed as part of a BridgeCommandType.Startup request.
Represents usage of the two primary BYOND server executables
Task TrustDmbPath(string fullDmbPath, CancellationToken cancellationToken)
Add a given fullDmbPath to the trusted DMBs list in BYOND&#39;s config.
Handles communication with a DreamDaemon IProcess
readonly IByondManager byond
The IByondManager for the SessionControllerFactory
On Windows, DreamDaemon will show an unskippable prompt when using /world/proc/OpenPort(). This looks out for those prompts and immediately clicks "Yes" if the owning process has registered for it
readonly Api.Models.Instance instance
The Api.Models.Instance for the SessionControllerFactory
Represents a BYOND installation
Definition: Byond.cs:8
Represents a tracking of dynamic chat json files
readonly IAssemblyInformationProvider assemblyInformationProvider
The IAssemblyInformationProvider for the SessionControllerFactory
DreamDaemonSecurity
DreamDaemon&#39;s security level
Provides absolute paths to the latest compiled .dmbs
Definition: IDmbProvider.cs:9
Interface for using filesystems
Definition: IIOManager.cs:11
Provides access to the server&#39;s HttpApiPort.
readonly IBridgeRegistrar bridgeRegistrar
The IBridgeRegistrar for the SessionControllerFactory.
For identifying the current platform
This model mirrors /datum/tgs_revision_information/test_merge
static void PortBindTest(ushort port)
Check if a given port can be bound to.
string OriginCommitSha
The sha of the most recent remote commit
ushort PrimaryPort
The first port DreamDaemon uses. This should be the publically advertised port
readonly IProcessExecutor processExecutor
The IProcessExecutor for the SessionControllerFactory
bool AllowWebClient
If the BYOND web client can be used to connect to the game server