tgstation-server 6.12.0
The /tg/station 13 server suite
Loading...
Searching...
No Matches
ServiceLifetime.cs
Go to the documentation of this file.
1using System;
2using System.IO;
3using System.IO.Pipes;
4using System.Linq;
5using System.Text;
6using System.Threading;
7using System.Threading.Tasks;
8
9using Microsoft.Extensions.Logging;
10
13
15{
20 {
24 readonly ILogger<ServiceLifetime> logger;
25
29 readonly Task watchdogTask;
30
34 readonly CancellationTokenSource cancellationTokenSource;
35
39 AnonymousPipeServerStream? commandPipeServer;
40
44 AnonymousPipeServerStream? readyPipeServer;
45
53 public ServiceLifetime(Action stopService, Func<ISignalChecker, IWatchdog> watchdogFactory, ILogger<ServiceLifetime> logger, string[] args)
54 {
55 ArgumentNullException.ThrowIfNull(stopService);
56 ArgumentNullException.ThrowIfNull(watchdogFactory);
57 this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
58 ArgumentNullException.ThrowIfNull(args);
59
60 logger.LogDebug("Starting service lifetime as user: {username}", Environment.UserName);
61
62 cancellationTokenSource = new CancellationTokenSource();
64 stopService,
65 watchdogFactory(this),
66 args,
68 }
69
71 public async ValueTask DisposeAsync()
72 {
74 await watchdogTask;
76
77 if (commandPipeServer != null)
78 await commandPipeServer.DisposeAsync();
79
80 if (readyPipeServer != null)
81 await readyPipeServer.DisposeAsync();
82 }
83
85 public async ValueTask CheckSignals(Func<string, (int, Task)> startChildAndGetPid, CancellationToken cancellationToken)
86 {
87 try
88 {
89 await using (commandPipeServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable))
90 await using (readyPipeServer = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable))
91 {
92 var (_, lifetimeTask) = startChildAndGetPid($"--Internal:CommandPipe={commandPipeServer.GetClientHandleAsString()} --Internal:ReadyPipe={readyPipeServer.GetClientHandleAsString()}");
93 commandPipeServer.DisposeLocalCopyOfClientHandle();
94 readyPipeServer.DisposeLocalCopyOfClientHandle();
95 await lifetimeTask;
96 }
97 }
98 finally
99 {
100 readyPipeServer = null;
101 commandPipeServer = null;
102 }
103 }
104
109 public void HandleCustomCommand(int command)
110 {
111 var commandsToCheck = PipeCommands.AllCommands;
112 foreach (var stringCommand in commandsToCheck)
113 {
114 var commandId = PipeCommands.GetServiceCommandId(stringCommand);
115 if (command == commandId)
116 {
117 SendCommandToHostThroughPipe(stringCommand);
118 return;
119 }
120 }
121
122 logger.LogWarning("Received unknown service command: {command}", command);
123 }
124
133 async Task RunWatchdog(Action stopService, IWatchdog watchdog, string[] args, CancellationToken cancellationToken)
134 {
135 var localWatchdogTask = watchdog.RunAsync(false, args, cancellationToken);
136
137 if (!localWatchdogTask.IsCompleted && (await watchdog.InitialHostVersion) >= new Version(5, 14, 0))
138 if (readyPipeServer != null)
139 {
140 logger.LogInformation("Waiting for host to finish starting...");
141 using var streamReader = new StreamReader(
143 Encoding.UTF8,
144 leaveOpen: true);
145
146 var line = streamReader.ReadLine(); // Intentionally blocking service startup
147 logger.LogDebug("Pipe read: {line}", line);
148
149 // Maybe we'll use this pipe more in the future, but for now leaving it open is just a resource waste
150 readyPipeServer.Dispose();
151 }
152 else
153 logger.LogError("Watchdog started and ready pipe was not initialized!");
154
155 await localWatchdogTask;
156
157 async void StopServiceAsync()
158 {
159 try
160 {
161 // This can call OnStop which waits on this task to complete, must be threaded off or it will deadlock
162 await Task.Run(stopService, cancellationToken);
163 }
164 catch (OperationCanceledException ex)
165 {
166 logger.LogDebug(ex, "Stopping service cancelled!");
167 }
168 catch (Exception ex)
169 {
170 logger.LogError(ex, "Error stopping service!");
171 }
172 }
173
174 StopServiceAsync();
175 }
176
181 void SendCommandToHostThroughPipe(string command)
182 {
183 var localPipeServer = commandPipeServer;
184 if (localPipeServer == null)
185 {
186 logger.LogWarning("Unable to send command \"{command}\" to main server process. Is the service running?", command);
187 return;
188 }
189
190 logger.LogDebug("Send command: {command}", command);
191 try
192 {
193 var encoding = Encoding.UTF8;
194 using var streamWriter = new StreamWriter(
195 localPipeServer,
196 encoding,
198 .AllCommands
199 .Select(
200 command => encoding.GetByteCount(
201 command + Environment.NewLine))
202 .Max(),
203 true);
204 streamWriter.WriteLine(command);
205 }
206 catch (Exception ex)
207 {
208 logger.LogError(ex, "Error attempting to send command \"{command}\"", command);
209 }
210 }
211 }
212}
Values able to be passed via the update file path.
static ? int GetServiceCommandId(string command)
Gets the int value of a given command .
Represents the lifetime of the service.
readonly CancellationTokenSource cancellationTokenSource
The cancellationTokenSource for the ServerService.
AnonymousPipeServerStream? readyPipeServer
The AnonymousPipeServerStream for receiving the PipeCommands.CommandStartupComplete.
void HandleCustomCommand(int command)
Handle a custom service command .
readonly ILogger< ServiceLifetime > logger
The ILogger for the ServerService.
ServiceLifetime(Action stopService, Func< ISignalChecker, IWatchdog > watchdogFactory, ILogger< ServiceLifetime > logger, string[] args)
Initializes a new instance of the ServiceLifetime class.
void SendCommandToHostThroughPipe(string command)
Sends a command to the main server process.
async ValueTask CheckSignals(Func< string,(int, Task)> startChildAndGetPid, CancellationToken cancellationToken)
async Task RunWatchdog(Action stopService, IWatchdog watchdog, string[] args, CancellationToken cancellationToken)
Executes the watchdog , stopping the service if it exits.
readonly Task watchdogTask
The Task that represents the running ServerService.
AnonymousPipeServerStream? commandPipeServer
The AnonymousPipeServerStream for sending PipeCommands to the server process.
For relaying signals received to the host process.
ValueTask< bool > RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
Run the IWatchdog.
Task< Version > InitialHostVersion
Gets a Task<TResult> resulting in the current version of the host process. Guaranteed to complete onc...
Definition IWatchdog.cs:15