tgstation-server
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 Newtonsoft.Json;
4 using Newtonsoft.Json.Serialization;
5 using System;
6 using System.Globalization;
7 using System.Linq;
8 using System.Runtime.InteropServices;
9 using System.Text;
10 using System.Threading;
11 using System.Threading.Tasks;
18 using Tgstation.Server.Host.IO;
20 
21 namespace Tgstation.Server.Host.Components.Watchdog
22 {
25  {
30 
34  readonly IByondManager byond;
35 
39  readonly IByondTopicSender byondTopicSender;
40 
45 
50 
55 
59  readonly IChat chat;
60 
65 
70 
74  readonly ILoggerFactory loggerFactory;
75 
79  readonly Api.Models.Instance instance;
80 
86  static string SecurityWord(DreamDaemonSecurity securityLevel)
87  {
88  switch (securityLevel)
89  {
90  case DreamDaemonSecurity.Safe:
91  return "safe";
92  case DreamDaemonSecurity.Trusted:
93  return "trusted";
94  case DreamDaemonSecurity.Ultrasafe:
95  return "ultrasafe";
96  default:
97  throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, String.Format(CultureInfo.InvariantCulture, "Bad DreamDaemon security level: {0}", securityLevel));
98  }
99  }
100 
115  public SessionControllerFactory(IProcessExecutor processExecutor, IByondManager byond, IByondTopicSender byondTopicSender, ICryptographySuite cryptographySuite, IApplication application, IIOManager ioManager, IChat chat, INetworkPromptReaper networkPromptReaper, IPlatformIdentifier platformIdentifier, ILoggerFactory loggerFactory, Api.Models.Instance instance)
116  {
117  this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
118  this.byond = byond ?? throw new ArgumentNullException(nameof(byond));
119  this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender));
120  this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
121  this.application = application ?? throw new ArgumentNullException(nameof(application));
122  this.instance = instance ?? throw new ArgumentNullException(nameof(instance));
123  this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
124  this.chat = chat ?? throw new ArgumentNullException(nameof(chat));
125  this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper));
126  this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
127  this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
128  }
129 
131  public async Task<ISessionController> LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
132  {
133  var portToUse = primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort;
134  if (!portToUse.HasValue)
135  throw new InvalidOperationException("Given port is null!");
136  var accessIdentifier = cryptographySuite.GetSecureString();
137 
138  const string JsonPostfix = "tgs.json";
139 
140  var basePath = primaryDirectory ? dmbProvider.PrimaryDirectory : dmbProvider.SecondaryDirectory;
141  //delete all previous tgs json files
142  var files = await ioManager.GetFilesWithExtension(basePath, JsonPostfix, cancellationToken).ConfigureAwait(false);
143 
144  await Task.WhenAll(files.Select(x => ioManager.DeleteFile(x, cancellationToken))).ConfigureAwait(false);
145 
146  //i changed this back from guids, hopefully i don't regret that
147  string JsonFile(string name) => String.Format(CultureInfo.InvariantCulture, "{0}.{1}", name, JsonPostfix);
148 
149  var securityLevelToUse = launchParameters.SecurityLevel.Value;
150  switch (dmbProvider.CompileJob.MinimumSecurityLevel)
151  {
152  case DreamDaemonSecurity.Ultrasafe:
153  break;
154  case DreamDaemonSecurity.Safe:
155  if (securityLevelToUse == DreamDaemonSecurity.Ultrasafe)
156  securityLevelToUse = DreamDaemonSecurity.Safe;
157  break;
158  case DreamDaemonSecurity.Trusted:
159  securityLevelToUse = DreamDaemonSecurity.Trusted;
160  break;
161  default:
162  throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel));
163  }
164 
165  //setup interop files
166  var interopInfo = new JsonFile
167  {
168  AccessIdentifier = accessIdentifier,
169  ApiValidateOnly = apiValidate,
170  ChatChannelsJson = JsonFile("chat_channels"),
171  ChatCommandsJson = JsonFile("chat_commands"),
172  ServerCommandsJson = JsonFile("server_commands"),
173  InstanceName = instance.Name,
174  SecurityLevel = securityLevelToUse,
175  Revision = new Api.Models.Internal.RevisionInformation
176  {
177  CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha,
178  OriginCommitSha = dmbProvider.CompileJob.RevisionInformation.OriginCommitSha
179  }
180  };
181 
182  interopInfo.TestMerges.AddRange(dmbProvider.CompileJob.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).Select(x => new Interop.TestMerge(x, interopInfo.Revision)));
183 
184  var interopJsonFile = JsonFile("interop");
185 
186  var interopJson = JsonConvert.SerializeObject(interopInfo, new JsonSerializerSettings
187  {
188  ContractResolver = new CamelCasePropertyNamesContractResolver(),
189  ReferenceLoopHandling = ReferenceLoopHandling.Ignore
190  });
191 
192  var localIoManager = new ResolvingIOManager(ioManager, basePath);
193 
194  var chatJsonTrackingTask = chat.TrackJsons(basePath, interopInfo.ChatChannelsJson, interopInfo.ChatCommandsJson, cancellationToken);
195 
196  await localIoManager.WriteAllBytes(interopJsonFile, Encoding.UTF8.GetBytes(interopJson), cancellationToken).ConfigureAwait(false);
197  var chatJsonTrackingContext = await chatJsonTrackingTask.ConfigureAwait(false);
198  try
199  {
200  //get the byond lock
201  var byondLock = currentByondLock ?? await byond.UseExecutables(Version.Parse(dmbProvider.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
202  try
203  {
204  //create interop context
205  var context = new CommContext(ioManager, loggerFactory.CreateLogger<CommContext>(), basePath, interopInfo.ServerCommandsJson);
206  try
207  {
208  //set command line options
209  //more sanitization here cause it uses the same scheme
210  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));
211 
212  //important to run on all ports to allow port changing
213  var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -{3} -verbose -public -params \"{4}\"",
214  dmbProvider.DmbName,
215  primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort,
216  launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
217  SecurityWord(securityLevelToUse),
218  parameters);
219 
220  //See #719
221  var noShellExecute = !platformIdentifier.IsWindows;
222  //launch dd
223  var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments, noShellExecute: noShellExecute);
224  try
225  {
226  networkPromptReaper.RegisterProcess(process);
227 
228  //return the session controller for it
229  var result = new SessionController(new ReattachInformation
230  {
231  AccessIdentifier = accessIdentifier,
232  Dmb = dmbProvider,
233  IsPrimary = primaryDirectory,
234  Port = portToUse.Value,
235  ProcessId = process.Id,
236  ChatChannelsJson = interopInfo.ChatChannelsJson,
237  ChatCommandsJson = interopInfo.ChatCommandsJson,
238  ServerCommandsJson = interopInfo.ServerCommandsJson,
239  }, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger<SessionController>(), launchParameters.SecurityLevel, launchParameters.StartupTimeout);
240 
241  //writeback launch parameter's fixed security level
242  launchParameters.SecurityLevel = securityLevelToUse;
243 
244  return result;
245  }
246  catch
247  {
248  process.Dispose();
249  throw;
250  }
251  }
252  catch
253  {
254  context.Dispose();
255  throw;
256  }
257  }
258  catch
259  {
260  if (currentByondLock == null)
261  byondLock.Dispose();
262  throw;
263  }
264  }
265  catch
266  {
267  chatJsonTrackingContext.Dispose();
268  throw;
269  }
270  }
271 
273  public async Task<ISessionController> Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
274  {
275  if (reattachInformation == null)
276  throw new ArgumentNullException(nameof(reattachInformation));
277 
278  SessionController result = null;
279  var basePath = reattachInformation.IsPrimary ? reattachInformation.Dmb.PrimaryDirectory : reattachInformation.Dmb.SecondaryDirectory;
280  var chatJsonTrackingContext = await chat.TrackJsons(basePath, reattachInformation.ChatChannelsJson, reattachInformation.ChatCommandsJson, cancellationToken).ConfigureAwait(false);
281  try
282  {
283  var byondLock = await byond.UseExecutables(Version.Parse(reattachInformation.Dmb.CompileJob.ByondVersion), cancellationToken).ConfigureAwait(false);
284  try
285  {
286  var context = new CommContext(ioManager, loggerFactory.CreateLogger<CommContext>(), basePath, reattachInformation.ServerCommandsJson);
287  try
288  {
289  var process = processExecutor.GetProcess(reattachInformation.ProcessId);
290 
291  if (process != null)
292  try
293  {
294  networkPromptReaper.RegisterProcess(process);
295  result = new SessionController(reattachInformation, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger<SessionController>(), null, null);
296  }
297  finally
298  {
299  if (result == null)
300  process.Dispose();
301  }
302  }
303  finally
304  {
305  if (result == null)
306  context.Dispose();
307  }
308  }
309  finally
310  {
311  if (result == null)
312  byondLock.Dispose();
313  }
314  }
315  finally
316  {
317  if (result == null)
318  chatJsonTrackingContext.Dispose();
319  }
320  return result;
321  }
322 
324  public ISessionController CreateDeadSession(IDmbProvider dmbProvider) => new DeadSessionController(dmbProvider);
325  }
326 }
string ServerCommandsJson
Path to the server commands json file
readonly ILoggerFactory loggerFactory
The ILoggerFactory for the SessionControllerFactory
readonly IIOManager ioManager
The IIOManager for the SessionControllerFactory
CompileJob CompileJob
The CompileJob of the .dmb
Definition: IDmbProvider.cs:29
string DmbName
The file name of the .dmb
Definition: IDmbProvider.cs:14
readonly IProcessExecutor processExecutor
The IProcessExecutor for the SessionControllerFactory
An IIOManager that resolve relative paths from another IIOManager to a subdirectory of that ...
readonly Api.Models.Instance instance
The Api.Models.Instance for the SessionControllerFactory
readonly ICryptographySuite cryptographySuite
The ICryptographySuite for the SessionControllerFactory
Configures the ASP.NET Core web application
Definition: IApplication.cs:8
string SecondaryDirectory
The secondary game directory with a trailing directory separator
Definition: IDmbProvider.cs:24
RevisionInformation RevisionInformation
Git revision the compiler ran on. Not modifiable
Definition: CompileJob.cs:16
string ChatCommandsJson
Path to the chat commands json file
readonly INetworkPromptReaper networkPromptReaper
The INetworkPromptReaper for the SessionControllerFactory
DreamDaemonSecurity SecurityLevel
The DreamDaemonSecurity level of DreamDaemon
string PrimaryDirectory
The primary game directory with a trailing directory separator
Definition: IDmbProvider.cs:19
Representation of the initial json passed to DreamDaemon
Definition: JsonFile.cs:10
List< TestMerge > ActiveTestMerges
The TestMerges active in the RevisionInformation
For managing connected chat services
Definition: IChat.cs:13
Contains various cryptographic functions
Handles communication with a DreamDaemon IProcess
readonly IApplication application
The IApplication for the SessionControllerFactory
async Task< ISessionController > LaunchNew(DreamDaemonLaunchParameters launchParameters, IDmbProvider dmbProvider, IByondExecutableLock currentByondLock, bool primaryPort, bool primaryDirectory, bool apiValidate, CancellationToken cancellationToken)
Create a ISessionController from a freshly launch DreamDaemon instance
DreamDaemonSecurity MinimumSecurityLevel
The minimum DreamDaemonSecurity required to run the CompileJob&#39;s output
Definition: CompileJob.cs:38
Version ByondVersion
The Byond.Version the CompileJob was made with
Definition: CompileJob.cs:21
readonly IByondTopicSender byondTopicSender
The IByondTopicSender for the SessionControllerFactory
readonly IPlatformIdentifier platformIdentifier
The IPlatformIdentifier for the SessionControllerFactory
async Task< ISessionController > Reattach(ReattachInformation reattachInformation, CancellationToken cancellationToken)
Create a ISessionController from an existing DreamDaemon instance
IDmbProvider Dmb
The IDmbProvider used by DreamDaemon
For managing the BYOND installation
bool IsPrimary
If the Components.IDmbProvider.PrimaryDirectory of the associated dmb is being used ...
string ChatChannelsJson
Path to the chat channels json file
readonly IChat chat
The IChat for the SessionControllerFactory
SessionControllerFactory(IProcessExecutor processExecutor, IByondManager byond, IByondTopicSender byondTopicSender, ICryptographySuite cryptographySuite, IApplication application, IIOManager ioManager, IChat chat, INetworkPromptReaper networkPromptReaper, IPlatformIdentifier platformIdentifier, ILoggerFactory loggerFactory, Api.Models.Instance instance)
Construct a SessionControllerFactory
Represents usage of the two primary BYOND server executables
Represents a BYOND installation
Definition: Byond.cs:8
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
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
For identifying the current platform
static string SecurityWord(DreamDaemonSecurity securityLevel)
Change a given securityLevel into the appropriate DreamDaemon command line word
string OriginCommitSha
The sha of the most recent remote commit
ushort PrimaryPort
The first port DreamDaemon uses. This should be the publically advertised port
Parameters necessary for duplicating a ISessionController session
bool AllowWebClient
If the BYOND web client can be used to connect to the game server