CLI tool for running Playbooks
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

320 lines
15 KiB

1 year ago
1 year ago
1 year ago
1 year ago
  1. #nullable enable
  2. using System;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Management;
  7. using System.ServiceProcess;
  8. using System.Text.RegularExpressions;
  9. using System.Threading.Tasks;
  10. using Microsoft.Win32;
  11. using TrustedUninstaller.Shared.Exceptions;
  12. using TrustedUninstaller.Shared.Tasks;
  13. using YamlDotNet.Serialization;
  14. namespace TrustedUninstaller.Shared.Actions
  15. {
  16. internal enum ServiceOperation
  17. {
  18. Stop,
  19. Continue,
  20. Start,
  21. Pause,
  22. Delete,
  23. Change
  24. }
  25. internal class ServiceAction : ITaskAction
  26. {
  27. [YamlMember(typeof(ServiceOperation), Alias = "operation")]
  28. public ServiceOperation Operation { get; set; } = ServiceOperation.Delete;
  29. [YamlMember(typeof(string), Alias = "name")]
  30. public string ServiceName { get; set; } = null!;
  31. [YamlMember(typeof(int), Alias = "startup")]
  32. public int? Startup { get; set; }
  33. [YamlMember(typeof(bool), Alias = "deleteStop")]
  34. public bool DeleteStop { get; set; } = true;
  35. [YamlMember(typeof(bool), Alias = "deleteUsingRegistry")]
  36. public bool RegistryDelete { get; set; } = false;
  37. [YamlMember(typeof(string), Alias = "device")]
  38. public bool Device { get; set; } = false;
  39. [YamlMember(typeof(string), Alias = "weight")]
  40. public int ProgressWeight { get; set; } = 4;
  41. public int GetProgressWeight() => ProgressWeight;
  42. private bool InProgress { get; set; }
  43. public void ResetProgress() => InProgress = false;
  44. public string ErrorString() => $"ServiceAction failed to {Operation.ToString().ToLower()} service {ServiceName}.";
  45. private ServiceController? GetService()
  46. {
  47. if (ServiceName.EndsWith("*") && ServiceName.StartsWith("*")) return ServiceController.GetServices()
  48. .FirstOrDefault(service => service.ServiceName.IndexOf(ServiceName.Trim('*'), StringComparison.CurrentCultureIgnoreCase) >= 0);
  49. if (ServiceName.EndsWith("*")) return ServiceController.GetServices()
  50. .FirstOrDefault(service => service.ServiceName.StartsWith(ServiceName.TrimEnd('*'), StringComparison.CurrentCultureIgnoreCase));
  51. if (ServiceName.StartsWith("*")) return ServiceController.GetServices()
  52. .FirstOrDefault(service => service.ServiceName.EndsWith(ServiceName.TrimStart('*'), StringComparison.CurrentCultureIgnoreCase));
  53. return ServiceController.GetServices()
  54. .FirstOrDefault(service => service.ServiceName.Equals(ServiceName, StringComparison.CurrentCultureIgnoreCase));
  55. }
  56. private ServiceController? GetDevice()
  57. {
  58. if (ServiceName.EndsWith("*") && ServiceName.StartsWith("*")) return ServiceController.GetDevices()
  59. .FirstOrDefault(service => service.ServiceName.IndexOf(ServiceName.Trim('*'), StringComparison.CurrentCultureIgnoreCase) >= 0);
  60. if (ServiceName.EndsWith("*")) return ServiceController.GetDevices()
  61. .FirstOrDefault(service => service.ServiceName.StartsWith(ServiceName.TrimEnd('*'), StringComparison.CurrentCultureIgnoreCase));
  62. if (ServiceName.StartsWith("*")) return ServiceController.GetDevices()
  63. .FirstOrDefault(service => service.ServiceName.EndsWith(ServiceName.TrimStart('*'), StringComparison.CurrentCultureIgnoreCase));
  64. return ServiceController.GetDevices()
  65. .FirstOrDefault(service => service.ServiceName.Equals(ServiceName, StringComparison.CurrentCultureIgnoreCase));
  66. }
  67. public UninstallTaskStatus GetStatus()
  68. {
  69. if (InProgress) return UninstallTaskStatus.InProgress;
  70. if (Operation == ServiceOperation.Change && Startup.HasValue)
  71. {
  72. // TODO: Implement dev log. Example:
  73. // if (Registry.LocalMachine.OpenSubKey($@"SYSTEM\CurrentControlSet\Services\{ServiceName}") == null) WriteToDevLog($"Warning: Service name '{ServiceName}' not found in registry.");
  74. var root = Registry.LocalMachine.OpenSubKey($@"SYSTEM\CurrentControlSet\Services\{ServiceName}");
  75. if (root == null) return UninstallTaskStatus.Completed;
  76. var value = root.GetValue("Start");
  77. return (int)value == Startup.Value ? UninstallTaskStatus.Completed : UninstallTaskStatus.ToDo;
  78. }
  79. ServiceController serviceController;
  80. if (Device) serviceController = GetDevice();
  81. else serviceController = GetService();
  82. if (Operation == ServiceOperation.Delete && RegistryDelete)
  83. {
  84. // TODO: Implement dev log. Example:
  85. // if (Registry.LocalMachine.OpenSubKey($@"SYSTEM\CurrentControlSet\Services\{ServiceName}") == null) WriteToDevLog($"Warning: Service name '{ServiceName}' not found in registry.");
  86. var root = Registry.LocalMachine.OpenSubKey($@"SYSTEM\CurrentControlSet\Services\{ServiceName}");
  87. return root == null ? UninstallTaskStatus.Completed : UninstallTaskStatus.ToDo;
  88. }
  89. return Operation switch
  90. {
  91. ServiceOperation.Stop =>
  92. serviceController == null ||
  93. serviceController?.Status == ServiceControllerStatus.Stopped
  94. || serviceController?.Status == ServiceControllerStatus.StopPending ?
  95. UninstallTaskStatus.Completed : UninstallTaskStatus.ToDo,
  96. ServiceOperation.Continue =>
  97. serviceController == null ||
  98. serviceController?.Status == ServiceControllerStatus.Running
  99. || serviceController?.Status == ServiceControllerStatus.ContinuePending ?
  100. UninstallTaskStatus.Completed : UninstallTaskStatus.ToDo,
  101. ServiceOperation.Start =>
  102. serviceController?.Status == ServiceControllerStatus.StartPending
  103. || serviceController?.Status == ServiceControllerStatus.Running ?
  104. UninstallTaskStatus.Completed : UninstallTaskStatus.ToDo,
  105. ServiceOperation.Pause =>
  106. serviceController == null ||
  107. serviceController?.Status == ServiceControllerStatus.Paused
  108. || serviceController?.Status == ServiceControllerStatus.PausePending ?
  109. UninstallTaskStatus.Completed : UninstallTaskStatus.ToDo,
  110. ServiceOperation.Delete =>
  111. serviceController == null ?
  112. UninstallTaskStatus.Completed : UninstallTaskStatus.ToDo,
  113. _ => throw new ArgumentOutOfRangeException("Argument out of Range", new ArgumentOutOfRangeException())
  114. };
  115. }
  116. private readonly string[] RegexNoKill = { "DcomLaunch" };
  117. public async Task<bool> RunTask()
  118. {
  119. if (InProgress) throw new TaskInProgressException("Another Service action was called while one was in progress.");
  120. if (Operation == ServiceOperation.Change && !Startup.HasValue) throw new ArgumentException("Startup property must be specified with the change operation.");
  121. if (Operation == ServiceOperation.Change && (Startup.Value > 4 || Startup.Value < 0)) throw new ArgumentException("Startup property must be between 1 and 4.");
  122. // This is a little cursed but it works and is concise lol
  123. Console.WriteLine($"{Operation.ToString().Replace("Stop", "Stopp").TrimEnd('e')}ing services matching '{ServiceName}'...");
  124. if (Operation == ServiceOperation.Change)
  125. {
  126. var action = new RegistryValueAction()
  127. {
  128. KeyName = $@"HKLM\SYSTEM\CurrentControlSet\Services\{ServiceName}",
  129. Value = "Start",
  130. Data = Startup.Value,
  131. Type = RegistryValueType.REG_DWORD,
  132. Operation = RegistryValueOperation.Set
  133. };
  134. await action.RunTask();
  135. InProgress = false;
  136. return true;
  137. }
  138. ServiceController? service;
  139. if (Device) service = GetDevice();
  140. else service = GetService();
  141. if (service == null)
  142. {
  143. Console.WriteLine($"No services found matching '{ServiceName}'.");
  144. //ErrorLogger.WriteToErrorLog($"The service matching '{ServiceName}' does not exist.", Environment.StackTrace, "ServiceAction Error");
  145. return false;
  146. }
  147. InProgress = true;
  148. var cmdAction = new CmdAction();
  149. if (Operation == ServiceOperation.Delete || Operation == ServiceOperation.Stop)
  150. {
  151. if (RegexNoKill.Any(regex => Regex.Match(ServiceName, regex, RegexOptions.IgnoreCase).Success))
  152. {
  153. Console.WriteLine($"Skipping {ServiceName}...");
  154. return false;
  155. }
  156. foreach (ServiceController dependentService in service.DependentServices)
  157. {
  158. Console.WriteLine($"Killing dependent service {dependentService.ServiceName}...");
  159. cmdAction.Command = Environment.Is64BitOperatingSystem ?
  160. $"ProcessHacker\\x64\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {dependentService.ServiceName} -caction stop" :
  161. $"ProcessHacker\\x86\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {dependentService.ServiceName} -caction stop";
  162. await cmdAction.RunTask();
  163. Console.WriteLine("Waiting for the service to stop...");
  164. int delay = 100;
  165. while (service.Status != ServiceControllerStatus.Stopped && delay <= 1000)
  166. {
  167. service.Refresh();
  168. //Wait for the service to stop
  169. Task.Delay(delay).Wait();
  170. delay += 100;
  171. }
  172. if (delay >= 1000)
  173. {
  174. Console.WriteLine("\r\nService stop timeout exceeded. Trying second method...");
  175. try
  176. {
  177. using var search = new ManagementObjectSearcher($"SELECT * FROM Win32_Service WHERE Name='{service.ServiceName}'");
  178. foreach (ManagementObject queryObj in search.Get())
  179. {
  180. var serviceId = (UInt32)queryObj["ProcessId"]; // Access service name
  181. var killServ = new TaskKillAction()
  182. {
  183. ProcessID = (int)serviceId
  184. };
  185. await killServ.RunTask();
  186. }
  187. }
  188. catch (Exception e)
  189. {
  190. ErrorLogger.WriteToErrorLog($"Could not kill dependent service {dependentService.ServiceName}.",
  191. e.StackTrace, "ServiceAction Error");
  192. }
  193. }
  194. }
  195. if (service.ServiceName == "SgrmAgent" && ((Operation == ServiceOperation.Delete && DeleteStop) || Operation == ServiceOperation.Stop))
  196. {
  197. await new TaskKillAction() { ProcessName = "SgrmBroker" }.RunTask();
  198. }
  199. }
  200. if (Operation == ServiceOperation.Delete)
  201. {
  202. if (DeleteStop && service.Status != ServiceControllerStatus.StopPending && service.Status != ServiceControllerStatus.Stopped)
  203. {
  204. cmdAction.Command = Environment.Is64BitOperatingSystem ?
  205. $"ProcessHacker\\x64\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {service.ServiceName} -caction stop" :
  206. $"ProcessHacker\\x86\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {service.ServiceName} -caction stop";
  207. await cmdAction.RunTask();
  208. }
  209. Console.WriteLine("Waiting for the service to stop...");
  210. int delay = 100;
  211. while (DeleteStop && service.Status != ServiceControllerStatus.Stopped && delay <= 1500)
  212. {
  213. service.Refresh();
  214. //Wait for the service to stop
  215. await Task.Delay(delay);
  216. delay += 100;
  217. }
  218. if (delay >= 1500)
  219. {
  220. Console.WriteLine("\r\nService stop timeout exceeded. Trying second method...");
  221. try
  222. {
  223. using var search = new ManagementObjectSearcher($"SELECT * FROM Win32_Service WHERE Name='{service.ServiceName}'");
  224. foreach (ManagementObject queryObj in search.Get())
  225. {
  226. var serviceId = (UInt32)queryObj["ProcessId"]; // Access service name
  227. var killServ = new TaskKillAction()
  228. {
  229. ProcessID = (int)serviceId
  230. };
  231. await killServ.RunTask();
  232. }
  233. }
  234. catch (Exception e)
  235. {
  236. ErrorLogger.WriteToErrorLog($"Could not kill service {service.ServiceName}.",
  237. e.StackTrace, "ServiceAction Error");
  238. }
  239. }
  240. if (RegistryDelete)
  241. {
  242. var action = new RegistryKeyAction()
  243. {
  244. KeyName = $@"HKLM\SYSTEM\CurrentControlSet\Services\{ServiceName}",
  245. Operation = RegistryKeyOperation.Delete
  246. };
  247. await action.RunTask();
  248. }
  249. else
  250. {
  251. cmdAction.Command = Environment.Is64BitOperatingSystem ?
  252. $"ProcessHacker\\x64\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {service.ServiceName} -caction delete" :
  253. $"ProcessHacker\\x86\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {service.ServiceName} -caction delete";
  254. await cmdAction.RunTask();
  255. }
  256. }
  257. else
  258. {
  259. cmdAction.Command = Environment.Is64BitOperatingSystem ?
  260. $"ProcessHacker\\x64\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {ServiceName} -caction {Operation.ToString().ToLower()}" :
  261. $"ProcessHacker\\x86\\ProcessHacker.exe -s -elevate -c -ctype service -cobject {ServiceName} -caction {Operation.ToString().ToLower()}";
  262. await cmdAction.RunTask();
  263. }
  264. service?.Dispose();
  265. await Task.Delay(100);
  266. InProgress = false;
  267. return true;
  268. }
  269. }
  270. }