tgstation-server  4.4.0
The /tg/station 13 server suite
SessionControllerFactory.cs
Go to the documentation of this file.
1 using Microsoft.Extensions.Logging;
2 using System;
3 using System.Globalization;
4 using System.Linq;
5 using System.Net;
6 using System.Net.Sockets;
7 using System.Text;
8 using System.Threading;
9 using System.Threading.Tasks;
10 using Tgstation.Server.Api;
19 using Tgstation.Server.Host.IO;
23 
24 namespace Tgstation.Server.Host.Components.Session
25 {
28  {
33 
37  readonly IByondManager byond;
38 
43 
48 
53 
58 
62  readonly IChatManager chat;
63 
68 
73 
78 
83 
87  readonly ILoggerFactory loggerFactory;
88 
92  readonly ILogger<SessionControllerFactory> logger;
93 
97  readonly Api.Models.Instance instance;
98 
104  static string SecurityWord(DreamDaemonSecurity securityLevel)
105  {
106  return securityLevel switch
107  {
108  DreamDaemonSecurity.Safe => "safe",
109  DreamDaemonSecurity.Trusted => "trusted",
110  DreamDaemonSecurity.Ultrasafe => "ultrasafe",
111  _ => throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel)),
112  };
113  }
114 
119  static void PortBindTest(ushort port)
120  {
121  using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
122 
123  try
124  {
125  socket.Bind(new IPEndPoint(IPAddress.Any, port));
126  }
127  catch (Exception ex)
128  {
129  throw new JobException(ErrorCode.DreamDaemonPortInUse, ex);
130  }
131  }
132 
151  IProcessExecutor processExecutor,
152  IByondManager byond,
153  ITopicClientFactory topicClientFactory,
154  ICryptographySuite cryptographySuite,
155  IAssemblyInformationProvider assemblyInformationProvider,
156  IIOManager ioManager,
157  IChatManager chat,
158  INetworkPromptReaper networkPromptReaper,
159  IPlatformIdentifier platformIdentifier,
160  IBridgeRegistrar bridgeRegistrar,
161  IServerPortProvider serverPortProvider,
162  ILoggerFactory loggerFactory,
163  ILogger<SessionControllerFactory> logger,
164  Api.Models.Instance instance)
165  {
166  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
167  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
168  this.topicClientFactory = topicClientFactory ?? throw new ArgumentNullException(nameof(topicClientFactory));
169  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
170  this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider));
171  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
172  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
173  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
174  this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
175  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
176  this.bridgeRegistrar = bridgeRegistrar ?? throw new ArgumentNullException(nameof(bridgeRegistrar));
177  this.serverPortProvider = serverPortProvider ?? throw new ArgumentNullException(nameof(serverPortProvider));
178  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
179  this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
180  }
181 
183  #pragma warning disable CA1506 // TODO: Decomplexify
184  public async Task<ISessionController> LaunchNew(
185  IDmbProvider dmbProvider,
186  IByondExecutableLock currentByondLock,
187  DreamDaemonLaunchParameters launchParameters,
188  bool apiValidate,
189  CancellationToken cancellationToken)
190  {
191  logger.LogTrace("Begin session launch...");
192  if (!launchParameters.Port.HasValue)
193  throw new InvalidOperationException("Given port is null!");
194  switch (dmbProvider.CompileJob.MinimumSecurityLevel)
195  {
196  case DreamDaemonSecurity.Ultrasafe:
197  break;
198  case DreamDaemonSecurity.Safe:
199  if (launchParameters.SecurityLevel == DreamDaemonSecurity.Ultrasafe)
200  launchParameters.SecurityLevel = DreamDaemonSecurity.Safe;
201  break;
202  case DreamDaemonSecurity.Trusted:
203  launchParameters.SecurityLevel = DreamDaemonSecurity.Trusted;
204  break;
205  default:
206  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel));
207  }
208 
209  var chatTrackingContext = chat.CreateTrackingContext();
210  try
211  {
212  // get the byond lock
213  var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
214  try
215  {
216  logger.LogDebug(
217  "Launching session with CompileJob {0}...",
218  byondLock.Version.Semver(),
219  dmbProvider.CompileJob.Id);
220 
221  if (launchParameters.SecurityLevel == DreamDaemonSecurity.Trusted)
222  await byondLock.TrustDmbPath(ioManager.ConcatPath(dmbProvider.Directory, dmbProvider.DmbName), cancellationToken).ConfigureAwait(false);
223 
224  PortBindTest(launchParameters.Port.Value);
225  await CheckPagerIsNotRunning(cancellationToken).ConfigureAwait(false);
226 
227  var accessIdentifier = cryptographySuite.GetSecureString();
228 
229  var byondTopicSender = topicClientFactory.CreateTopicClient(
230  TimeSpan.FromMilliseconds(
231  launchParameters.TopicRequestTimeout.Value));
232 
233  // set command line options
234  // more sanitization here cause it uses the same scheme
235  var parameters = $"{DMApiConstants.ParamApiVersion}={byondTopicSender.SanitizeString(DMApiConstants.Version.Semver().ToString())}&{byondTopicSender.SanitizeString(DMApiConstants.ParamServerPort)}={serverPortProvider.HttpApiPort}&{byondTopicSender.SanitizeString(DMApiConstants.ParamAccessIdentifier)}={byondTopicSender.SanitizeString(accessIdentifier)}";
236 
237  var visibility = apiValidate ? "invisible" : "public";
238 
239  // important to run on all ports to allow port changing
240  Guid? logFileGuid = null;
241  var arguments = String.Format(
242  CultureInfo.InvariantCulture,
243  "{0} -port {1} -ports 1-65535 {2}-close -{3} -{4}{5} -public -params \"{6}\"",
244  dmbProvider.DmbName,
245  launchParameters.Port.Value,
246  launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
247  SecurityWord(launchParameters.SecurityLevel.Value),
248  visibility,
249  platformIdentifier.IsWindows
250  ? $" -log {logFileGuid = Guid.NewGuid()}"
251  : String.Empty, // Just use stdout on linux
252  parameters);
253 
254  // See https://github.com/tgstation/tgstation-server/issues/719
255  var noShellExecute = !platformIdentifier.IsWindows;
256 
257  if (!apiValidate && dmbProvider.CompileJob.DMApiVersion == null)
258  logger.LogDebug("Session will have no DMAPI support!");
259 
260  // launch dd
261  var process = processExecutor.LaunchProcess(
262  byondLock.DreamDaemonPath,
263  dmbProvider.Directory,
264  arguments,
265  noShellExecute,
266  noShellExecute,
267  noShellExecute: noShellExecute);
268 
269  async Task<string> GetDDOutput()
270  {
271  if (!platformIdentifier.IsWindows)
272  return process.GetCombinedOutput();
273 
274  var logFilePath = ioManager.ConcatPath(dmbProvider.Directory, logFileGuid.ToString());
275  try
276  {
277  var dreamDaemonLogBytes = await ioManager.ReadAllBytes(
278  logFilePath,
279  default)
280  .ConfigureAwait(false);
281 
282  return Encoding.UTF8.GetString(dreamDaemonLogBytes);
283  }
284  finally
285  {
286  try
287  {
288  await ioManager.DeleteFile(logFilePath, default).ConfigureAwait(false);
289  }
290  catch (Exception ex)
291  {
292  logger.LogWarning("Failed to delete DreamDaemon log file {0}: {1}", logFilePath, ex);
293  }
294  }
295  }
296 
297  // Log DD output
298  _ = process.Lifetime.ContinueWith(
299  async x =>
300  {
301  try
302  {
303  var ddOutput = await GetDDOutput().ConfigureAwait(false);
304  logger.LogTrace(
305  "DreamDaemon Output:{0}{1}",
306  Environment.NewLine, ddOutput);
307  }
308  catch (Exception ex)
309  {
310  logger.LogWarning("Error reading DreamDaemon output: {0}", ex);
311  }
312  },
313  TaskScheduler.Current);
314 
315  try
316  {
317  networkPromptReaper.RegisterProcess(process);
318 
319  var runtimeInformation = CreateRuntimeInformation(
320  dmbProvider,
321  chatTrackingContext,
322  launchParameters.SecurityLevel.Value,
323  apiValidate);
324 
325  var reattachInformation = new ReattachInformation(
326  dmbProvider,
327  process,
328  runtimeInformation,
329  accessIdentifier,
330  launchParameters.Port.Value);
331 
332  var sessionController = new SessionController(
333  reattachInformation,
334  instance,
335  process,
336  byondLock,
337  byondTopicSender,
338  chatTrackingContext,
339  bridgeRegistrar,
340  chat,
341  assemblyInformationProvider,
342  loggerFactory.CreateLogger<SessionController>(),
343  launchParameters.StartupTimeout,
344  false,
345  apiValidate);
346 
347  return sessionController;
348  }
349  catch
350  {
351  process.Terminate();
352  process.Dispose();
353  throw;
354  }
355  }
356  catch
357  {
358  if (currentByondLock == null)
359  byondLock.Dispose();
360  throw;
361  }
362  }
363  catch
364  {
365  chatTrackingContext.Dispose();
366  throw;
367  }
368  }
369  #pragma warning restore CA1506
370 
372  public async Task<ISessionController> Reattach(
373  ReattachInformation reattachInformation,
374  CancellationToken cancellationToken)
375  {
376  if (reattachInformation == null)
377  throw new ArgumentNullException(nameof(reattachInformation));
378 
379  logger.LogTrace("Begin session reattach...");
380  var byondTopicSender = topicClientFactory.CreateTopicClient(reattachInformation.TopicRequestTimeout);
381  var chatTrackingContext = chat.CreateTrackingContext();
382  try
383  {
384  var byondLock = await byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
385 
386  try
387  {
388  logger.LogDebug(
389  "Attaching to session PID: {0}, CompileJob: {1}...",
390  reattachInformation.ProcessId,
391  reattachInformation.Dmb.CompileJob.Id);
392 
393  var process = processExecutor.GetProcess(reattachInformation.ProcessId);
394  if (process == null)
395  return null;
396 
397  try
398  {
399  networkPromptReaper.RegisterProcess(process);
400  var runtimeInformation = CreateRuntimeInformation(
401  reattachInformation.Dmb,
402  chatTrackingContext,
403  null,
404  false);
405  reattachInformation.SetRuntimeInformation(runtimeInformation);
406 
407  var controller = new SessionController(
408  reattachInformation,
409  instance,
410  process,
411  byondLock,
412  byondTopicSender,
413  chatTrackingContext,
414  bridgeRegistrar,
415  chat,
416  assemblyInformationProvider,
417  loggerFactory.CreateLogger<SessionController>(),
418  null,
419  true,
420  false);
421 
422  process = null;
423  byondLock = null;
424  chatTrackingContext = null;
425 
426  return controller;
427  }
428  finally
429  {
430  process?.Dispose();
431  }
432  }
433  finally
434  {
435  byondLock?.Dispose();
436  }
437  }
438  finally
439  {
440  chatTrackingContext?.Dispose();
441  }
442  }
443 
453  IDmbProvider dmbProvider,
454  IChatTrackingContext chatTrackingContext,
455  DreamDaemonSecurity? securityLevel,
456  bool apiValidateOnly)
457  {
458  var revisionInfo = new Api.Models.Internal.RevisionInformation
459  {
460  CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha,
461  OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha
462  };
463 
464  var testMerges = dmbProvider
465  .CompileJob
468  .Select(x => x.TestMerge)
469  .Select(x => new TestMergeInformation(x, revisionInfo))
470  ?? Enumerable.Empty<TestMergeInformation>();
471 
472  return new RuntimeInformation(
473  assemblyInformationProvider,
474  serverPortProvider,
475  testMerges,
476  chatTrackingContext.Channels,
477  instance,
478  revisionInfo,
479  securityLevel,
480  apiValidateOnly);
481  }
482 
488  async Task CheckPagerIsNotRunning(CancellationToken cancellationToken)
489  {
490  if (!platformIdentifier.IsWindows)
491  return;
492 
493  using var otherProcess = processExecutor.GetProcessByName("byond");
494  if (otherProcess == null)
495  return;
496 
497  var otherUsernameTask = otherProcess.GetExecutingUsername(cancellationToken);
498  using var ourProcess = processExecutor.GetCurrentProcess();
499  var ourUserName = await ourProcess.GetExecutingUsername(cancellationToken).ConfigureAwait(false);
500  var otherUserName = await otherUsernameTask.ConfigureAwait(false);
501 
502  if(otherUserName.Equals(ourUserName, StringComparison.Ordinal))
503  throw new JobException(ErrorCode.DeploymentPagerRunning);
504  }
505  }
506 }
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:24
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
readonly IIOManager ioManager
The IIOManager for the SessionControllerFactory
long Id
The ID of the entity.
Definition: EntityId.cs:11
ErrorCode
Types of ErrorMessages that the API may return.
Definition: ErrorCode.cs:10
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the SessionControllerFactory
ushort Port
The first port DreamDaemon uses. This should be the publically advertised port
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
uint TopicRequestTimeout
The timeout for sending and receiving BYOND topics in milliseconds.
virtual Version DMApiVersion
The DMAPI Version.
Definition: CompileJob.cs:39
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
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:33
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
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
TimeSpan TopicRequestTimeout
The TimeSpan which indicates when topic requests should timeout.
readonly IServerPortProvider serverPortProvider
The IServerPortProvider for the SessionControllerFactory.
string Directory
The primary game directory with a trailing directory separator
Definition: IDmbProvider.cs:19
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.
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
async Task< ISessionController > LaunchNew(IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, DreamDaemonLaunchParameters launchParameters, bool apiValidate, CancellationToken cancellationToken)
Create a ISessionController from a freshly launch DreamDaemon instance
readonly Api.Models.Instance instance
The Api.Models.Instance for the SessionControllerFactory
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.
SessionControllerFactory(IProcessExecutor processExecutor, IByondManager byond, ITopicClientFactory topicClientFactory, 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
string OriginCommitSha
The sha of the most recent remote commit
readonly IProcessExecutor processExecutor
The IProcessExecutor for the SessionControllerFactory
readonly ITopicClientFactory topicClientFactory
The ITopicClientFactory for the SessionControllerFactory
bool AllowWebClient
If the BYOND web client can be used to connect to the game server