59 public async ValueTask<bool>
RunAsync(
bool runConfigure,
string[] args, CancellationToken cancellationToken)
61 logger.LogInformation(
"Host watchdog starting...");
63 using (var currentProc = Process.GetCurrentProcess())
64 currentProcessId = currentProc.Id;
66 logger.LogDebug(
"PID: {pid}", currentProcessId);
67 string? updateDirectory =
null;
70 var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
71 var dotnetPath = DotnetHelper.GetPotentialDotnetPaths(isWindows)
72 .Where(potentialDotnetPath =>
74 logger.LogTrace(
"Checking for dotnet at {potentialDotnetPath}", potentialDotnetPath);
75 return File.Exists(potentialDotnetPath);
79 if (dotnetPath ==
default)
81 logger.LogCritical(
"Unable to locate dotnet executable in PATH! Please ensure the .NET Core runtime is installed and is in your PATH!");
85 logger.LogInformation(
"Detected dotnet executable at {dotnetPath}", dotnetPath);
87 var executingAssembly = Assembly.GetExecutingAssembly();
88 var rootLocation = Path.GetDirectoryName(executingAssembly.Location);
89 if (rootLocation ==
null)
91 logger.LogCritical(
"Failed to get the directory name of the executing assembly: {location}", executingAssembly.Location);
95 var bootstrappable = args.Contains(
"--bootstrap", StringComparer.OrdinalIgnoreCase);
96 var homeDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".tgstation-server");
98 var assemblyStoragePath = Path.Combine(bootstrappable ? homeDirectory : rootLocation,
"lib");
100 var defaultAssemblyPath = Path.GetFullPath(Path.Combine(assemblyStoragePath,
"Default"));
102 if (Debugger.IsAttached)
106 if (Directory.Exists(assemblyStoragePath))
107 Directory.Delete(assemblyStoragePath,
true);
108 Directory.CreateDirectory(defaultAssemblyPath);
110 var sourcePath = Path.GetFullPath(
113 "../../../../Tgstation.Server.Host/bin/Debug/net8.0"));
114 foreach (
string dirPath
in Directory.GetDirectories(sourcePath,
"*", SearchOption.AllDirectories))
115 Directory.CreateDirectory(dirPath.Replace(sourcePath, defaultAssemblyPath, StringComparison.Ordinal));
117 foreach (
string newPath
in Directory.GetFiles(sourcePath,
"*.*", SearchOption.AllDirectories))
118 File.Copy(newPath, newPath.Replace(sourcePath, defaultAssemblyPath, StringComparison.Ordinal),
true);
120 const string AppSettingsYaml =
"appsettings.yml";
121 var rootYaml = Path.Combine(rootLocation, AppSettingsYaml);
122 File.Delete(rootYaml);
123 File.Move(Path.Combine(defaultAssemblyPath, AppSettingsYaml), rootYaml);
126 Directory.CreateDirectory(assemblyStoragePath);
128 var bootstrapperSettingsFile = Path.Combine(homeDirectory,
"bootstrap.json");
130 if (!Directory.Exists(defaultAssemblyPath))
134 logger.LogCritical(
"Unable to locate host assembly directory!");
138 if (File.Exists(bootstrapperSettingsFile))
140 logger.LogInformation(
"Loading bootstrap settings...");
141 var bootstrapperSettingsJson = await File.ReadAllTextAsync(bootstrapperSettingsFile, cancellationToken);
142 bootstrapSettings = JsonSerializer.Deserialize<
BootstrapSettings>(bootstrapperSettingsJson);
143 if (bootstrapSettings ==
null)
145 logger.LogCritical(
"Failed to deserialize {settingsFile}!", bootstrapperSettingsFile);
151 logger.LogInformation(
"Using default bootstrap settings...");
162 logger.LogInformation(
"Bootstrapping from: {url}", downloadUrl);
163 using var httpClient =
new HttpClient();
164 await
using var zipData = await httpClient.GetStreamAsync(
new Uri(downloadUrl), cancellationToken);
165 using var archive =
new ZipArchive(zipData, ZipArchiveMode.Read,
true);
166 archive.ExtractToDirectory(defaultAssemblyPath);
169 var assemblyName = String.Join(
".", nameof(
Tgstation), nameof(
Server), nameof(Host),
"dll");
170 var assemblyPath = Path.Combine(defaultAssemblyPath, assemblyName);
172 if (assemblyPath.Contains(
'"', StringComparison.Ordinal))
174 logger.LogCritical(
"Running from paths with \"'s in the name is not supported!");
178 var watchdogVersion = executingAssembly.GetName().Version?.Semver().ToString();
180 while (!cancellationToken.IsCancellationRequested)
182 if (!File.Exists(assemblyPath))
184 logger.LogCritical(
"Unable to locate host assembly!");
188 var fileVersion = FileVersionInfo.GetVersionInfo(assemblyPath).FileVersion;
189 if (fileVersion ==
null)
191 logger.LogCritical(
"Failed to parse version info from {assemblyPath}!", assemblyPath);
197 if (!Version.TryParse(fileVersion, out var bootstrappedVersion))
199 logger.LogCritical(
"Failed to parse bootstrapped version prior to launch: {fileVersion}", fileVersion);
207 TgsVersion = bootstrappedVersion.Semver(),
212 Directory.CreateDirectory(homeDirectory);
213 await File.WriteAllTextAsync(
214 bootstrapperSettingsFile,
215 JsonSerializer.Serialize(
217 new JsonSerializerOptions
219 WriteIndented = true,
225 using (
logger.BeginScope(
"Host invocation"))
231 updateDirectory = Path.GetFullPath(Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString()));
232 logger.LogInformation(
"Update path set to {updateDirectory}", updateDirectory);
233 using (var process =
new Process())
235 process.StartInfo.FileName = dotnetPath;
236 process.StartInfo.WorkingDirectory = rootLocation;
238 var arguments =
new List<string>
240 $
"\"{assemblyPath}\"",
241 $
"\"{updateDirectory}\"",
242 $
"\"{watchdogVersion}\"",
245 if (args.Any(x => x.Equals(
"--attach-host-debugger", StringComparison.OrdinalIgnoreCase)))
246 arguments.Add(
"--attach-debugger");
250 logger.LogInformation(
"Running configuration check and wizard...");
251 arguments.Add(
"--General:SetupWizardMode=Only");
254 arguments.AddRange(args);
256 process.StartInfo.Arguments = String.Join(
" ", arguments);
258 process.StartInfo.UseShellExecute =
false;
260 var killedHostProcess =
false;
263 Task? processTask =
null;
264 (int, Task) StartProcess(
string? additionalArg)
266 if (additionalArg !=
null)
267 process.StartInfo.Arguments += $
" {additionalArg}";
269 logger.LogInformation(
"Launching host with arguments: {arguments}", process.StartInfo.Arguments);
272 return (process.Id, processTask = process.WaitForExitAsync(cancellationToken));
275 using (var processCts =
new CancellationTokenSource())
276 using (cancellationToken.Register(() =>
278 if (!Directory.Exists(updateDirectory))
280 logger.LogInformation(
"Cancellation requested! Writing shutdown lock file...");
281 File.WriteAllBytes(updateDirectory, Array.Empty<byte>());
284 logger.LogWarning(
"Cancellation requested while update directory exists!");
286 logger.LogInformation(
"Will force close host process if it doesn't exit in 10 seconds...");
290 processCts.CancelAfter(TimeSpan.FromSeconds(10));
292 catch (ObjectDisposedException ex)
295 logger.LogWarning(ex,
"Error triggering timeout!");
299 using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
313 catch (InvalidOperationException ex)
315 logger.LogWarning(ex,
"Error triggering timeout!");
321 if (!process.HasExited)
323 killedHostProcess =
true;
325 process.WaitForExit();
328 catch (InvalidOperationException ex2)
330 logger.LogWarning(ex2,
"Error killing host process!");
335 if (File.Exists(updateDirectory))
336 File.Delete(updateDirectory);
340 logger.LogWarning(ex2,
"Error deleting comms file!");
343 logger.LogInformation(
"Host exited!");
348 logger.LogInformation(
"Exiting due to configure intent...");
357 if (!cancellationToken.IsCancellationRequested)
358 logger.LogInformation(
"Watchdog will restart host...");
360 logger.LogWarning(
"Host requested restart but watchdog shutdown is in progress!");
364 logger.LogCritical(
"Host crashed, propagating exception dump...");
366 var data =
"(NOT PRESENT)";
367 if (File.Exists(updateDirectory))
368 data = File.ReadAllText(updateDirectory);
372 File.Delete(updateDirectory);
376 logger.LogWarning(e,
"Unable to delete exception dump file at {updateDirectory}!", updateDirectory);
379#pragma warning disable CA2201
380 throw new Exception(String.Format(CultureInfo.InvariantCulture,
"Host propagated exception: {0}", data));
381#pragma warning restore CA2201
383 if (killedHostProcess)
385 logger.LogWarning(
"Watchdog forced to kill host process!");
386 cancellationToken.ThrowIfCancellationRequested();
389#pragma warning disable CA2201
390 throw new Exception(String.Format(CultureInfo.InvariantCulture,
"Host crashed with exit code {0}!", process.ExitCode));
391#pragma warning restore CA2201
398 if (Directory.Exists(updateDirectory))
400 logger.LogInformation(
"Applying server update...");
404 GC.Collect(Int32.MaxValue, GCCollectionMode.Default,
true);
405 await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(
false);
408 var tempPath = Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString());
411 Directory.Move(defaultAssemblyPath, tempPath);
414 Directory.Move(updateDirectory, defaultAssemblyPath);
415 logger.LogInformation(
"Server update complete, deleting old server...");
418 Directory.Delete(tempPath,
true);
422 logger.LogWarning(e,
"Error deleting old server at {tempPath}!", tempPath);
427 logger.LogError(e,
"Error moving updated server directory, attempting revert!");
428 Directory.Delete(defaultAssemblyPath,
true);
429 Directory.Move(tempPath, defaultAssemblyPath);
430 logger.LogInformation(
"Revert successful!");
435 logger.LogWarning(e,
"Failed to move out active host assembly!");
441 catch (OperationCanceledException ex)
443 logger.LogDebug(ex,
"Exiting due to cancellation...");
444 if (updateDirectory !=
null)
445 if (!Directory.Exists(updateDirectory))
446 File.Delete(updateDirectory);
448 Directory.Delete(updateDirectory,
true);
452 logger.LogCritical(ex,
"Host watchdog error!");
457 logger.LogInformation(
"Host watchdog exiting...");