2using System.Collections.Specialized;
3using System.Configuration.Install;
5using System.Reflection;
6using System.Runtime.Versioning;
7using System.ServiceProcess;
9using System.Threading.Tasks;
11using McMaster.Extensions.CommandLineUtils;
13using Microsoft.Extensions.Hosting.WindowsServices;
14using Microsoft.Extensions.Logging;
24 [SupportedOSPlatform(
"windows")]
30#pragma warning disable SA1401
32#pragma warning restore SA1401
37 [Option(ShortName =
"u", Description =
"Uninstalls ANY installed tgstation-server service >=v4.0.0")]
38 public bool Uninstall {
get; }
43 [Option(ShortName =
"x", Description =
"If the service has to stop, detach any running DreamDaemon processes beforehand. Only supported on versions >=5.13.0")]
44 public bool Detach {
get; }
49 [Option(ShortName =
"r", Description =
"Stop and restart the tgstation-server service")]
55 [Option(ShortName =
"i", Description =
"Installs this executable as the tgstation-server Windows service")]
56 public bool Install {
get;
set; }
61 [Option(ShortName =
"f", Description =
"Automatically agree to uninstall prompts")]
62 public bool Force {
get;
set; }
67 [Option(ShortName =
"s", Description =
"Suppresses console output from the host watchdog")]
68 public bool Silent {
get;
set; }
73 [Option(ShortName =
"c", Description =
"Runs the TGS setup wizard")]
74 public bool Configure {
get;
set; }
79 [Option(ShortName =
"p", Description =
"Arguments passed to main host process")]
80 public string? PassthroughArgs {
get;
set; }
87 static Task<int> Main(
string[] args) => CommandLineApplication.ExecuteAsync<
Program>(args);
93 public async Task OnExecuteAsync()
95 var standardRun = !Install && !Uninstall && !Configure;
97 if (!Silent && !WindowsServiceHelpers.IsWindowsService())
99 var result = NativeMethods.MessageBox(
101 "You are running the TGS windows service executable directly. It should only be run by the service control manager. Would you like to install and configure the service in this location?",
103 NativeMethods.MessageBoxButtons.YesNo);
105 if (result != NativeMethods.DialogResult.Yes)
112 using (var service =
new ServerService(
WatchdogFactory, GetPassthroughArgs(), LogLevel.Trace))
119 await RunConfigure(CancellationToken.None);
121 bool stopped =
false;
124 foreach (ServiceController sc
in ServiceController.GetServices())
129 match = sc.ServiceName == ServerService.Name;
135 InvokeSC(ServerService.Name);
142 stopped |= RunServiceInstall();
145 foreach (ServiceController sc
in ServiceController.GetServices())
147 if (sc.ServiceName == ServerService.Name)
160 void InvokeSC(
string? serviceToUninstall)
162 using var installer =
new ServiceInstaller();
163 if (serviceToUninstall !=
null)
165 installer.Context =
new InstallContext($
"old-{serviceToUninstall}-uninstall.log",
null);
166 installer.ServiceName = serviceToUninstall;
167 installer.Uninstall(
null);
171 var fullPathToAssembly = Path.GetFullPath(
172 Assembly.GetExecutingAssembly().Location);
174 var assemblyDirectory = Path.GetDirectoryName(fullPathToAssembly);
175 if (assemblyDirectory ==
null)
176 throw new InvalidOperationException($
"Failed to resolve directory name of {assemblyDirectory}");
178 var assemblyNameWithoutExtension = Path.GetFileNameWithoutExtension(fullPathToAssembly);
179 var exePath = Path.Combine(assemblyDirectory, $
"{assemblyNameWithoutExtension}.exe");
181 var programDataDirectory = Path.Combine(
182 Environment.GetFolderPath(
183 Environment.SpecialFolder.CommonApplicationData,
184 Environment.SpecialFolderOption.DoNotVerify),
185 Server.Common.Constants.CanonicalPackageName);
187 using var processInstaller =
new ServiceProcessInstaller();
188 processInstaller.Account = ServiceAccount.LocalSystem;
191 installer.Context =
new InstallContext(
192 Path.Combine(programDataDirectory, $
"tgs-install-{Guid.NewGuid()}.log"),
194 $
"assemblypath=\"{exePath}\"{(String.IsNullOrWhiteSpace(PassthroughArgs) ? String.Empty : $" -p=\
"{PassthroughArgs}\"")}
",
196 installer.Description = $"{
Server.Common.Constants.CanonicalPackageName} running as a Windows service.
";
197 installer.DisplayName = Server.Common.Constants.CanonicalPackageName;
198 installer.StartType = ServiceStartMode.Automatic;
199 installer.ServicesDependedOn = new string[] { "Tcpip
", "Dhcp
", "Dnscache
" };
200 installer.ServiceName = ServerService.Name;
201 installer.Parent = processInstaller;
203 var state = new ListDictionary();
206 installer.Install(state);
208 installer.Commit(state);
212 installer.Rollback(state);
221 bool RunServiceInstall()
223 // First check if the service already exists
224 bool serviceStopped = false;
225 if (Force || !WindowsServiceHelpers.IsWindowsService())
226 foreach (ServiceController sc in ServiceController.GetServices())
229 var serviceName = sc.ServiceName;
230 if (serviceName == ServerService.Name || serviceName == "tgstation-server-4
")
232 NativeMethods.DialogResult result = !Force
233 ? NativeMethods.MessageBox(
235 $"You already have another TGS service installed ({sc.ServiceName}). Would you like to uninstall it now? Pressing \
"No\" will cancel this install.",
237 NativeMethods.MessageBoxButtons.YesNo)
238 : NativeMethods.DialogResult.Yes;
239 if (result != NativeMethods.DialogResult.Yes)
246 InvokeSC(sc.ServiceName);
252 return serviceStopped;
259 void RestartService(ServiceController serviceController)
261 if (serviceController.Status != ServiceControllerStatus.Running)
268 serviceController.ExecuteCommand(serviceControllerCommand!.Value);
269 serviceController.WaitForStatus(ServiceControllerStatus.Stopped, TimeSpan.FromSeconds(30));
270 if (serviceController.Status != ServiceControllerStatus.Stopped)
276 serviceController.Stop();
277 serviceController.WaitForStatus(ServiceControllerStatus.Stopped);
286 async ValueTask RunConfigure(CancellationToken cancellationToken)
288 using var loggerFactory = LoggerFactory.Create(builder =>
291 builder.AddConsole();
295 await watchdog.
RunAsync(
true, GetPassthroughArgs(), cancellationToken);
302 string[] GetPassthroughArgs() => PassthroughArgs?.Split(
' ', StringSplitOptions.RemoveEmptyEntries) ?? Array.Empty<
string>();
Values able to be passed via the update file path.
static ? int GetServiceCommandId(string command)
Gets the int value of a given command .
const string CommandDetachingShutdown
Stops the server ASAP, detaching the watchdog for any running instances.
Entrypoint for the Process.
IWatchdog CreateWatchdog(ISignalChecker signalChecker, ILoggerFactory loggerFactory)
Create a IWatchdog.A new IWatchdog.
Factory for creating IWatchdogs.
ValueTask< bool > RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
Run the IWatchdog.
@ Restart
User can immediately restart the Watchdog.
@ Force
Force run the wizard.