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 var serializerOptions =
new JsonSerializerOptions
182 WriteIndented =
true,
184 while (!cancellationToken.IsCancellationRequested)
186 if (!File.Exists(assemblyPath))
188 logger.LogCritical(
"Unable to locate host assembly!");
192 var fileVersion = FileVersionInfo.GetVersionInfo(assemblyPath).FileVersion;
193 if (fileVersion ==
null)
195 logger.LogCritical(
"Failed to parse version info from {assemblyPath}!", assemblyPath);
201 if (!Version.TryParse(fileVersion, out var bootstrappedVersion))
203 logger.LogCritical(
"Failed to parse bootstrapped version prior to launch: {fileVersion}", fileVersion);
211 TgsVersion = bootstrappedVersion.Semver(),
216 Directory.CreateDirectory(homeDirectory);
217 await File.WriteAllTextAsync(
218 bootstrapperSettingsFile,
219 JsonSerializer.Serialize(
226 using (
logger.BeginScope(
"Host invocation"))
232 updateDirectory = Path.GetFullPath(Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString()));
233 logger.LogInformation(
"Update path set to {updateDirectory}", updateDirectory);
234 using (var process =
new Process())
236 process.StartInfo.FileName = dotnetPath;
237 process.StartInfo.WorkingDirectory = rootLocation;
239 var arguments =
new List<string>
241 $
"\"{assemblyPath}\"",
242 $
"\"{updateDirectory}\"",
243 $
"\"{watchdogVersion}\"",
246 if (args.Any(x => x.Equals(
"--attach-host-debugger", StringComparison.OrdinalIgnoreCase)))
247 arguments.Add(
"--attach-debugger");
251 logger.LogInformation(
"Running configuration check and wizard...");
252 arguments.Add(
"--General:SetupWizardMode=Only");
255 arguments.AddRange(args);
257 process.StartInfo.Arguments = String.Join(
" ", arguments);
259 process.StartInfo.UseShellExecute =
false;
261 var killedHostProcess =
false;
262 var createdShutdownFile =
false;
265 Task? processTask =
null;
266 (int, Task) StartProcess(
string? additionalArg)
268 if (additionalArg !=
null)
269 process.StartInfo.Arguments += $
" {additionalArg}";
271 logger.LogInformation(
"Launching host with arguments: {arguments}", process.StartInfo.Arguments);
274 return (process.Id, processTask = process.WaitForExitAsync(cancellationToken));
277 using (var processCts =
new CancellationTokenSource())
278 using (cancellationToken.Register(() =>
280 if (!Directory.Exists(updateDirectory))
282 logger.LogInformation(
"Cancellation requested! Writing shutdown lock file...");
283 File.WriteAllBytes(updateDirectory, Array.Empty<byte>());
284 createdShutdownFile = true;
287 logger.LogWarning(
"Cancellation requested while update directory exists!");
289 logger.LogInformation(
"Will force close host process if it doesn't exit in 10 seconds...");
293 processCts.CancelAfter(TimeSpan.FromSeconds(10));
295 catch (ObjectDisposedException ex)
298 logger.LogWarning(ex,
"Error triggering timeout!");
302 using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
316 catch (InvalidOperationException ex)
318 logger.LogWarning(ex,
"Error triggering timeout!");
324 if (!process.HasExited)
326 killedHostProcess =
true;
328 process.WaitForExit();
331 catch (InvalidOperationException ex2)
333 logger.LogWarning(ex2,
"Error killing host process!");
336 if (createdShutdownFile)
339 if (File.Exists(updateDirectory))
340 File.Delete(updateDirectory);
344 logger.LogWarning(ex2,
"Error deleting comms file!");
347 logger.LogInformation(
"Host exited!");
352 logger.LogInformation(
"Exiting due to configure intent...");
361 if (!cancellationToken.IsCancellationRequested)
362 logger.LogInformation(
"Watchdog will restart host...");
364 logger.LogWarning(
"Host requested restart but watchdog shutdown is in progress!");
368 logger.LogCritical(
"Host crashed, propagating exception dump...");
370 var data =
"(NOT PRESENT)";
371 if (File.Exists(updateDirectory))
372 data = File.ReadAllText(updateDirectory);
376 File.Delete(updateDirectory);
380 logger.LogWarning(e,
"Unable to delete exception dump file at {updateDirectory}!", updateDirectory);
383#pragma warning disable CA2201
384 throw new Exception(String.Format(CultureInfo.InvariantCulture,
"Host propagated exception: {0}", data));
385#pragma warning restore CA2201
387 if (killedHostProcess)
389 logger.LogWarning(
"Watchdog forced to kill host process!");
390 cancellationToken.ThrowIfCancellationRequested();
393#pragma warning disable CA2201
394 throw new Exception(String.Format(CultureInfo.InvariantCulture,
"Host crashed with exit code {0}!", process.ExitCode));
395#pragma warning restore CA2201
402 if (Directory.Exists(updateDirectory))
404 logger.LogInformation(
"Applying server update...");
408 GC.Collect(Int32.MaxValue, GCCollectionMode.Default,
true);
409 await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(
false);
412 var tempPath = Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString());
415 Directory.Move(defaultAssemblyPath, tempPath);
418 Directory.Move(updateDirectory, defaultAssemblyPath);
419 logger.LogInformation(
"Server update complete, deleting old server...");
422 Directory.Delete(tempPath,
true);
426 logger.LogWarning(e,
"Error deleting old server at {tempPath}!", tempPath);
431 logger.LogError(e,
"Error moving updated server directory, attempting revert!");
432 Directory.Delete(defaultAssemblyPath,
true);
433 Directory.Move(tempPath, defaultAssemblyPath);
434 logger.LogInformation(
"Revert successful!");
439 logger.LogWarning(e,
"Failed to move out active host assembly!");
445 catch (OperationCanceledException ex)
447 logger.LogDebug(ex,
"Exiting due to cancellation...");
448 if (updateDirectory !=
null)
449 if (!Directory.Exists(updateDirectory))
450 File.Delete(updateDirectory);
452 Directory.Delete(updateDirectory,
true);
456 logger.LogCritical(ex,
"Host watchdog error!");
461 logger.LogInformation(
"Host watchdog exiting...");