WrapperService.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using System.Reflection;
  5. using System.ServiceProcess;
  6. using System.Text;
  7. using System.Threading.Tasks;
  8. using log4net;
  9. using WinSW.Extensions;
  10. using WinSW.Logging;
  11. using WinSW.Native;
  12. using WinSW.Util;
  13. using Messages = WinSW.ServiceMessages;
  14. namespace WinSW
  15. {
  16. public sealed class WrapperService : ServiceBase, IEventLogger, IServiceEventLog
  17. {
  18. internal static readonly WrapperServiceEventLogProvider EventLogProvider = new();
  19. private static readonly int AdditionalStopTimeout = 1_000;
  20. private static readonly ILog Log = LogManager.GetLogger(LoggerNames.Service);
  21. private readonly XmlServiceConfig config;
  22. private Process process = null!;
  23. private volatile Process? startingProcess;
  24. private volatile Process? stoppingProcess;
  25. internal WinSWExtensionManager ExtensionManager { get; }
  26. private SharedDirectoryMapper? sharedDirectoryMapper;
  27. private bool shuttingdown;
  28. /// <summary>
  29. /// Version of Windows service wrapper
  30. /// </summary>
  31. /// <remarks>
  32. /// The version will be taken from <see cref="AssemblyInfo"/>
  33. /// </remarks>
  34. public static Version Version => Assembly.GetExecutingAssembly().GetName().Version!;
  35. public WrapperService(XmlServiceConfig config)
  36. {
  37. this.ServiceName = config.Name;
  38. this.CanStop = true;
  39. this.AutoLog = false;
  40. this.config = config;
  41. this.ExtensionManager = new WinSWExtensionManager(config);
  42. // Register the event log provider
  43. EventLogProvider.Service = this;
  44. if (config.Preshutdown)
  45. {
  46. this.AcceptPreshutdown();
  47. }
  48. Environment.CurrentDirectory = config.WorkingDirectory;
  49. }
  50. /// <summary>
  51. /// Process the file copy instructions, so that we can replace files that are always in use while
  52. /// the service runs.
  53. /// </summary>
  54. private void HandleFileCopies()
  55. {
  56. string? file = this.config.BasePath + ".copies";
  57. if (!File.Exists(file))
  58. {
  59. return; // nothing to handle
  60. }
  61. try
  62. {
  63. using var tr = new StreamReader(file, Encoding.UTF8);
  64. string? line;
  65. while ((line = tr.ReadLine()) != null)
  66. {
  67. Log.Info("Handling copy: " + line);
  68. string[] tokens = line.Split('>');
  69. if (tokens.Length > 2)
  70. {
  71. Log.Error("Too many delimiters in " + line);
  72. continue;
  73. }
  74. this.MoveFile(tokens[0], tokens[1]);
  75. }
  76. }
  77. finally
  78. {
  79. File.Delete(file);
  80. }
  81. }
  82. /// <summary>
  83. /// File replacement.
  84. /// </summary>
  85. private void MoveFile(string sourceFileName, string destFileName)
  86. {
  87. try
  88. {
  89. FileHelper.MoveOrReplaceFile(sourceFileName, destFileName);
  90. }
  91. catch (IOException e)
  92. {
  93. Log.Error("Failed to move :" + sourceFileName + " to " + destFileName + " because " + e.Message);
  94. }
  95. }
  96. /// <summary>
  97. /// Handle the creation of the logfiles based on the optional logmode setting.
  98. /// </summary>
  99. /// <returns>Log Handler, which should be used for the spawned process</returns>
  100. private LogHandler CreateExecutableLogHandler()
  101. {
  102. string logDirectory = this.config.LogDirectory;
  103. if (!Directory.Exists(logDirectory))
  104. {
  105. Directory.CreateDirectory(logDirectory);
  106. }
  107. var logAppender = this.config.LogHandler;
  108. logAppender.EventLogger = this;
  109. return logAppender;
  110. }
  111. public void WriteEntry(string message)
  112. {
  113. if (this.shuttingdown)
  114. {
  115. // The Event Log service exits earlier.
  116. return;
  117. }
  118. try
  119. {
  120. this.EventLog.WriteEntry(message);
  121. }
  122. catch (Exception e)
  123. {
  124. Log.Error("Failed to log event in Windows Event Log: " + message + "; Reason: ", e);
  125. }
  126. }
  127. public void WriteEntry(string message, EventLogEntryType type)
  128. {
  129. if (this.shuttingdown)
  130. {
  131. // The Event Log service exits earlier.
  132. return;
  133. }
  134. try
  135. {
  136. this.EventLog.WriteEntry(message, type);
  137. }
  138. catch (Exception e)
  139. {
  140. Log.Error("Failed to log event in Windows Event Log. Reason: ", e);
  141. }
  142. }
  143. void IServiceEventLog.WriteEntry(string message, EventLogEntryType type)
  144. {
  145. if (this.shuttingdown)
  146. {
  147. // The Event Log service exits earlier.
  148. return;
  149. }
  150. this.EventLog.WriteEntry(message, type);
  151. }
  152. private void LogExited(string message, int exitCode)
  153. {
  154. if (exitCode == 0)
  155. {
  156. Log.Info(message);
  157. }
  158. else
  159. {
  160. Log.Warn(message);
  161. }
  162. }
  163. private void LogMinimal(string message)
  164. {
  165. this.WriteEntry(message);
  166. Log.Info(message);
  167. }
  168. internal void RaiseOnStart(string[] args) => this.OnStart(args);
  169. internal void RaiseOnStop() => this.OnStop();
  170. protected override void OnStart(string[] args)
  171. {
  172. try
  173. {
  174. this.DoStart();
  175. this.LogMinimal(Messages.StartedSuccessfully);
  176. }
  177. catch (Exception e)
  178. {
  179. Log.Error("Failed to start service.", e);
  180. throw;
  181. }
  182. }
  183. protected override void OnStop()
  184. {
  185. try
  186. {
  187. this.DoStop();
  188. this.LogMinimal(Messages.StoppedSuccessfully);
  189. }
  190. catch (Exception e)
  191. {
  192. Log.Error("Failed to stop service.", e);
  193. throw;
  194. }
  195. }
  196. protected override void OnShutdown()
  197. {
  198. try
  199. {
  200. this.shuttingdown = true;
  201. this.DoStop();
  202. this.LogMinimal("Service was shut down successfully.");
  203. }
  204. catch (Exception e)
  205. {
  206. Log.Error("Failed to shut down service.", e);
  207. throw;
  208. }
  209. }
  210. protected override void OnCustomCommand(int command)
  211. {
  212. if (command == 0x0000000F)
  213. {
  214. // SERVICE_CONTROL_PRESHUTDOWN
  215. this.Stop();
  216. }
  217. }
  218. private void DoStart()
  219. {
  220. bool succeeded = ConsoleApis.FreeConsole();
  221. Debug.Assert(succeeded);
  222. succeeded = ConsoleApis.SetConsoleCtrlHandler(null, true);
  223. Debug.Assert(succeeded);
  224. this.HandleFileCopies();
  225. // handle downloads
  226. var downloads = this.config.Downloads;
  227. var tasks = new Task[downloads.Count];
  228. for (int i = 0; i < downloads.Count; i++)
  229. {
  230. var download = downloads[i];
  231. string downloadMessage = $"Downloading: {download.From} to {download.To}. failOnError={download.FailOnError.ToString()}";
  232. Log.Info(downloadMessage);
  233. tasks[i] = download.PerformAsync();
  234. }
  235. Task.WaitAll(tasks);
  236. var sharedDirectories = this.config.SharedDirectories;
  237. if (sharedDirectories.Count > 0)
  238. {
  239. this.sharedDirectoryMapper = new(sharedDirectories);
  240. this.sharedDirectoryMapper.Map();
  241. }
  242. var prestart = this.config.Prestart;
  243. string? prestartExecutable = prestart.Executable;
  244. if (prestartExecutable != null)
  245. {
  246. try
  247. {
  248. using var process = this.StartProcess(prestartExecutable, prestart.Arguments, prestart.CreateLogHandler());
  249. this.WaitForProcessToExit(process);
  250. this.LogExited($"Pre-start process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
  251. process.StopDescendants(AdditionalStopTimeout);
  252. }
  253. catch (Exception e)
  254. {
  255. Log.Error(e);
  256. }
  257. }
  258. string startArguments = this.config.StartArguments ?? this.config.Arguments;
  259. Log.Info("Starting " + this.config.Executable);
  260. // Load and start extensions
  261. this.ExtensionManager.LoadExtensions();
  262. this.ExtensionManager.FireOnWrapperStarted();
  263. var executableLogHandler = this.CreateExecutableLogHandler();
  264. this.process = this.StartProcess(this.config.Executable, startArguments, executableLogHandler, this.OnMainProcessExited);
  265. this.ExtensionManager.FireOnProcessStarted(this.process);
  266. var poststart = this.config.Poststart;
  267. string? poststartExecutable = poststart.Executable;
  268. if (poststartExecutable != null)
  269. {
  270. try
  271. {
  272. using var process = StartProcessLocked();
  273. this.WaitForProcessToExit(process);
  274. this.LogExited($"Post-start process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
  275. process.StopDescendants(AdditionalStopTimeout);
  276. this.startingProcess = null;
  277. Process StartProcessLocked()
  278. {
  279. lock (this)
  280. {
  281. return this.startingProcess = this.StartProcess(poststartExecutable, poststart.Arguments, poststart.CreateLogHandler());
  282. }
  283. }
  284. }
  285. catch (Exception e)
  286. {
  287. Log.Error(e);
  288. }
  289. }
  290. }
  291. /// <summary>
  292. /// Called when we are told by Windows SCM to exit.
  293. /// </summary>
  294. private void DoStop()
  295. {
  296. var prestop = this.config.Prestop;
  297. string? prestopExecutable = prestop.Executable;
  298. if (prestopExecutable != null)
  299. {
  300. try
  301. {
  302. using var process = StartProcessLocked(prestopExecutable, prestop.Arguments, prestop.CreateLogHandler());
  303. this.WaitForProcessToExit(process);
  304. this.LogExited($"Pre-stop process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
  305. process.StopDescendants(AdditionalStopTimeout);
  306. this.stoppingProcess = null;
  307. }
  308. catch (Exception e)
  309. {
  310. Log.Error(e);
  311. }
  312. }
  313. Log.Info("Stopping " + this.config.Name);
  314. this.process.EnableRaisingEvents = false;
  315. string? stopExecutable = this.config.StopExecutable;
  316. string? stopArguments = this.config.StopArguments;
  317. if (stopExecutable is null && stopArguments is null)
  318. {
  319. var process = this.process;
  320. Log.Debug("ProcessKill " + process.Id);
  321. bool? result = process.Stop(this.config.StopTimeoutInMs);
  322. this.LogMinimal($"Child process '{process.Format()}' " + result switch
  323. {
  324. true => $"canceled with code {process.ExitCode}.",
  325. false => "terminated.",
  326. null => $"finished with code '{process.ExitCode}'."
  327. });
  328. this.process.StopDescendants(this.config.StopTimeoutInMs);
  329. this.ExtensionManager.FireOnProcessTerminated(process);
  330. }
  331. else
  332. {
  333. this.SignalPending();
  334. stopExecutable ??= this.config.Executable;
  335. try
  336. {
  337. // TODO: Redirect logging to Log4Net once https://github.com/kohsuke/winsw/pull/213 is integrated
  338. using var stopProcess = StartProcessLocked(stopExecutable, stopArguments);
  339. Log.Debug("WaitForProcessToExit " + this.process.Id + "+" + stopProcess.Id);
  340. this.WaitForProcessToExit(stopProcess);
  341. stopProcess.StopDescendants(AdditionalStopTimeout);
  342. this.stoppingProcess = null;
  343. this.WaitForProcessToExit(this.process);
  344. this.process.StopDescendants(this.config.StopTimeoutInMs);
  345. }
  346. catch
  347. {
  348. this.process.StopTree(this.config.StopTimeoutInMs);
  349. throw;
  350. }
  351. }
  352. var poststop = this.config.Poststop;
  353. string? poststopExecutable = poststop.Executable;
  354. if (poststopExecutable != null)
  355. {
  356. try
  357. {
  358. using var process = StartProcessLocked(poststopExecutable, poststop.Arguments, poststop.CreateLogHandler());
  359. this.WaitForProcessToExit(process);
  360. this.LogExited($"Post-stop process '{process.Format()}' exited with code {process.ExitCode}.", process.ExitCode);
  361. process.StopDescendants(AdditionalStopTimeout);
  362. this.stoppingProcess = null;
  363. }
  364. catch (Exception e)
  365. {
  366. Log.Error(e);
  367. }
  368. }
  369. try
  370. {
  371. this.sharedDirectoryMapper?.Unmap();
  372. }
  373. catch (Exception e)
  374. {
  375. Log.Error(e);
  376. }
  377. // Stop extensions
  378. this.ExtensionManager.FireBeforeWrapperStopped();
  379. if (this.shuttingdown && this.config.BeepOnShutdown)
  380. {
  381. Console.Beep();
  382. }
  383. Log.Info("Finished " + this.config.Name);
  384. Process StartProcessLocked(string executable, string? arguments, LogHandler? logHandler = null)
  385. {
  386. lock (this)
  387. {
  388. return this.stoppingProcess = this.StartProcess(executable, arguments, logHandler);
  389. }
  390. }
  391. }
  392. private void WaitForProcessToExit(Process process)
  393. {
  394. this.SignalPending();
  395. // A good interval is one-tenth of the wait hint but not less than 1 second and not more than 10 seconds.
  396. while (!process.WaitForExit(1_500))
  397. {
  398. this.SignalPending();
  399. }
  400. }
  401. /// <exception cref="MissingFieldException" />
  402. private void AcceptPreshutdown()
  403. {
  404. const string acceptedCommandsFieldName =
  405. #if NET
  406. "_acceptedCommands";
  407. #else
  408. "acceptedCommands";
  409. #endif
  410. var acceptedCommandsField = typeof(ServiceBase).GetField(acceptedCommandsFieldName, BindingFlags.Instance | BindingFlags.NonPublic);
  411. if (acceptedCommandsField is null)
  412. {
  413. throw new MissingFieldException(nameof(ServiceBase), acceptedCommandsFieldName);
  414. }
  415. int acceptedCommands = (int)acceptedCommandsField.GetValue(this)!;
  416. acceptedCommands |= 0x00000100; // SERVICE_ACCEPT_PRESHUTDOWN
  417. acceptedCommandsField.SetValue(this, acceptedCommands);
  418. }
  419. private void SignalPending()
  420. {
  421. this.RequestAdditionalTime(15_000);
  422. }
  423. private void SignalStopped()
  424. {
  425. using var scm = ServiceManager.Open();
  426. using var sc = scm.OpenService(this.ServiceName, ServiceApis.ServiceAccess.QueryStatus);
  427. sc.SetStatus(this.ServiceHandle, ServiceControllerStatus.Stopped);
  428. }
  429. private void OnMainProcessExited(Process process)
  430. {
  431. lock (this)
  432. {
  433. try
  434. {
  435. Log.Warn($"Child process '{process.Format()}' finished with code {process.ExitCode}.");
  436. process.StopDescendants(this.config.StopTimeoutInMs);
  437. this.startingProcess?.StopTree(AdditionalStopTimeout);
  438. this.stoppingProcess?.StopTree(AdditionalStopTimeout);
  439. // if we finished orderly, report that to SCM.
  440. // by not reporting unclean shutdown, we let Windows SCM to decide if it wants to
  441. // restart the service automatically
  442. if (process.ExitCode == 0)
  443. {
  444. this.SignalStopped();
  445. }
  446. }
  447. finally
  448. {
  449. Environment.Exit(process.ExitCode);
  450. }
  451. }
  452. }
  453. /// <summary>
  454. /// <paramref name="onExited"/> will not be raised if <see cref="Process.EnableRaisingEvents"/> is <see langword="false"/>.
  455. /// </summary>
  456. private Process StartProcess(string executable, string? arguments, LogHandler? logHandler = null, Action<Process>? onExited = null)
  457. {
  458. var startInfo = new ProcessStartInfo(executable, arguments ?? string.Empty)
  459. {
  460. UseShellExecute = false,
  461. WorkingDirectory = this.config.WorkingDirectory,
  462. CreateNoWindow = this.config.HideWindow,
  463. RedirectStandardOutput = logHandler?.OutFileDisabled == false,
  464. RedirectStandardError = logHandler?.ErrFileDisabled == false,
  465. };
  466. var environment = this.config.EnvironmentVariables;
  467. if (environment.Count > 0)
  468. {
  469. var newEnvironment =
  470. #if NET
  471. startInfo.Environment;
  472. #else
  473. startInfo.EnvironmentVariables;
  474. #endif
  475. foreach (var pair in environment)
  476. {
  477. newEnvironment[pair.Key] = pair.Value;
  478. }
  479. }
  480. bool succeeded = ConsoleApis.AllocConsole(); // inherited
  481. Debug.Assert(succeeded);
  482. succeeded = ConsoleApis.SetConsoleCtrlHandler(null, false); // inherited
  483. Debug.Assert(succeeded);
  484. succeeded = ConsoleApis.SetConsoleOutputCP(ConsoleApis.CP_UTF8);
  485. Debug.Assert(succeeded);
  486. Process process;
  487. try
  488. {
  489. process = Process.Start(startInfo)!;
  490. }
  491. finally
  492. {
  493. succeeded = ConsoleApis.FreeConsole();
  494. Debug.Assert(succeeded);
  495. succeeded = ConsoleApis.SetConsoleCtrlHandler(null, true);
  496. Debug.Assert(succeeded);
  497. }
  498. Log.Info($"Started process {process.Format()}.");
  499. if (this.config.Priority is ProcessPriorityClass priority)
  500. {
  501. try
  502. {
  503. process.PriorityClass = priority;
  504. }
  505. catch (InvalidOperationException)
  506. {
  507. // exited
  508. }
  509. }
  510. if (logHandler != null)
  511. {
  512. logHandler.Log(
  513. startInfo.RedirectStandardOutput ? process.StandardOutput : StreamReader.Null,
  514. startInfo.RedirectStandardError ? process.StandardError : StreamReader.Null);
  515. }
  516. if (onExited != null)
  517. {
  518. process.Exited += (sender, _) =>
  519. {
  520. var process = (Process)sender!;
  521. if (!process.EnableRaisingEvents)
  522. {
  523. return;
  524. }
  525. try
  526. {
  527. onExited(process);
  528. }
  529. catch (Exception e)
  530. {
  531. Log.Error("Unhandled exception in event handler.", e);
  532. }
  533. };
  534. process.EnableRaisingEvents = true;
  535. }
  536. return process;
  537. }
  538. }
  539. }