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.

314 lines
11 KiB

1 year ago
10 months ago
1 year ago
6 months ago
6 months ago
6 months ago
1 year ago
6 months ago
1 year ago
6 months ago
1 year ago
10 months ago
1 year ago
6 months ago
1 year ago
6 months ago
6 months ago
6 months ago
6 months ago
6 months ago
1 year ago
6 months ago
1 year ago
6 months ago
1 year ago
6 months ago
1 year ago
6 months ago
1 year ago
6 months ago
1 year ago
6 months ago
1 year ago
  1. using System;
  2. using System.Diagnostics;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Threading;
  7. using System.Threading.Tasks;
  8. using System.Windows.Forms;
  9. using TrustedUninstaller.Shared.Exceptions;
  10. using TrustedUninstaller.Shared.Tasks;
  11. using YamlDotNet.Serialization;
  12. namespace TrustedUninstaller.Shared.Actions
  13. {
  14. public class PowerShellAction : TaskAction, ITaskAction
  15. {
  16. public void RunTaskOnMainThread()
  17. {
  18. if (InProgress) throw new TaskInProgressException("Another Powershell action was called while one was in progress.");
  19. InProgress = true;
  20. var privilegeText = RunAs == Privilege.CurrentUser ? " as the current user" : RunAs == Privilege.CurrentUserElevated ? " as the current user elevated" : RunAs == Privilege.System ?
  21. " as the system account" : "";
  22. Console.WriteLine($"Running PowerShel command '{Command}'{privilegeText}...");
  23. WinUtil.CheckKph();
  24. if (RunAs == Privilege.TrustedInstaller)
  25. RunAsProcess();
  26. else
  27. RunAsPrivilegedProcess();
  28. InProgress = false;
  29. return;
  30. }
  31. [YamlMember(typeof(Privilege), Alias = "runas")]
  32. public Privilege RunAs { get; set; } = Privilege.TrustedInstaller;
  33. [YamlMember(typeof(string), Alias = "command")]
  34. public string Command { get; set; }
  35. [YamlMember(typeof(string), Alias = "timeout")]
  36. public int? Timeout { get; set; }
  37. [YamlMember(typeof(string), Alias = "wait")]
  38. public bool Wait { get; set; } = true;
  39. [YamlMember(typeof(bool), Alias = "exeDir")]
  40. public bool ExeDir { get; set; } = false;
  41. [YamlMember(typeof(string), Alias = "weight")]
  42. public int ProgressWeight { get; set; } = 1;
  43. private int? ExitCode { get; set; }
  44. public string? StandardError { get; set; }
  45. public string StandardOutput { get; set; }
  46. public int GetProgressWeight() => ProgressWeight;
  47. private bool InProgress { get; set; }
  48. public void ResetProgress() => InProgress = false;
  49. public string ErrorString() => $"PowerShellAction failed to run command '{Command}'.";
  50. public UninstallTaskStatus GetStatus()
  51. {
  52. if (InProgress)
  53. {
  54. return UninstallTaskStatus.InProgress;
  55. }
  56. return ExitCode == null ? UninstallTaskStatus.ToDo: UninstallTaskStatus.Completed;
  57. }
  58. public Task<bool> RunTask()
  59. {
  60. return null;
  61. }
  62. private void RunAsProcess()
  63. {
  64. var process = new Process();
  65. var startInfo = new ProcessStartInfo
  66. {
  67. WindowStyle = ProcessWindowStyle.Normal,
  68. FileName = "PowerShell.exe",
  69. Arguments = $@"-NoP -ExecutionPolicy Bypass -NonInteractive -C ""{Command}""",
  70. UseShellExecute = false,
  71. RedirectStandardError = true,
  72. RedirectStandardOutput = true,
  73. CreateNoWindow = true
  74. };
  75. if (ExeDir) startInfo.WorkingDirectory = AmeliorationUtil.Playbook.Path + "\\Executables";
  76. if (!Wait)
  77. {
  78. startInfo.RedirectStandardError = false;
  79. startInfo.RedirectStandardOutput = false;
  80. startInfo.WindowStyle = ProcessWindowStyle.Hidden;
  81. startInfo.UseShellExecute = true;
  82. }
  83. process.StartInfo = startInfo;
  84. process.Start();
  85. if (!Wait)
  86. {
  87. process.Dispose();
  88. return;
  89. }
  90. var error = new StringBuilder();
  91. process.OutputDataReceived += ProcOutputHandler;
  92. process.ErrorDataReceived += delegate(object sender, DataReceivedEventArgs args)
  93. {
  94. if (!String.IsNullOrEmpty(args.Data))
  95. error.AppendLine(args.Data);
  96. else
  97. error.AppendLine();
  98. };
  99. process.BeginOutputReadLine();
  100. process.BeginErrorReadLine();
  101. if (Timeout != null)
  102. {
  103. var exited = process.WaitForExit(Timeout.Value);
  104. if (!exited)
  105. {
  106. process.Kill();
  107. throw new TimeoutException($"Command '{Command}' timeout exceeded.");
  108. }
  109. }
  110. else
  111. {
  112. bool exited = process.WaitForExit(30000);
  113. // WaitForExit alone seems to not be entirely reliable
  114. while (!exited && PowerShellRunning(process.Id))
  115. {
  116. exited = process.WaitForExit(30000);
  117. }
  118. }
  119. StandardError = error.ToString();
  120. int exitCode = 0;
  121. try
  122. {
  123. exitCode = process.ExitCode;
  124. }
  125. catch (Exception ex)
  126. {
  127. ErrorLogger.WriteToErrorLog("Error fetching process exit code. (1)", null, "PowerShellAction Error", Command);
  128. }
  129. if (exitCode != 0)
  130. {
  131. Console.WriteLine($"PowerShell instance exited with error code: {exitCode}");
  132. if (!String.IsNullOrWhiteSpace(StandardError)) Console.WriteLine($"Error message: {StandardError}");
  133. ErrorLogger.WriteToErrorLog("PowerShell exited with a non-zero exit code: " + exitCode, null, "PowerShellAction Error", Command);
  134. this.ExitCode = exitCode;
  135. }
  136. else
  137. {
  138. if (!String.IsNullOrWhiteSpace(StandardError)) Console.WriteLine($"Error output: {StandardError}");
  139. ExitCode = 0;
  140. }
  141. process.CancelOutputRead();
  142. process.CancelErrorRead();
  143. process.Dispose();
  144. }
  145. private static bool PowerShellRunning(int id)
  146. {
  147. try
  148. {
  149. return Process.GetProcessesByName("powershell").Any(x => x.Id == id);
  150. }
  151. catch (Exception)
  152. {
  153. return false;
  154. }
  155. }
  156. private void RunAsPrivilegedProcess()
  157. {
  158. var process = new AugmentedProcess.Process();
  159. var startInfo = new AugmentedProcess.ProcessStartInfo
  160. {
  161. WindowStyle = ProcessWindowStyle.Normal,
  162. FileName = "PowerShell.exe",
  163. Arguments = $@"-NoP -ExecutionPolicy Bypass -NonInteractive -C ""{Command}""",
  164. UseShellExecute = false,
  165. RedirectStandardError = true,
  166. RedirectStandardOutput = true,
  167. CreateNoWindow = true
  168. };
  169. if (ExeDir) startInfo.WorkingDirectory = AmeliorationUtil.Playbook.Path + "\\Executables";
  170. if (!Wait)
  171. {
  172. startInfo.RedirectStandardError = false;
  173. startInfo.RedirectStandardOutput = false;
  174. startInfo.WindowStyle = ProcessWindowStyle.Hidden;
  175. startInfo.UseShellExecute = true;
  176. }
  177. process.StartInfo = startInfo;
  178. ProcessPrivilege.StartPrivilegedTask(process, RunAs);
  179. if (!Wait)
  180. {
  181. process.Dispose();
  182. return;
  183. }
  184. var error = new StringBuilder();
  185. process.OutputDataReceived += PrivilegedProcOutputHandler;
  186. process.ErrorDataReceived += delegate(object sender, AugmentedProcess.DataReceivedEventArgs args)
  187. {
  188. if (!String.IsNullOrEmpty(args.Data))
  189. error.AppendLine(args.Data);
  190. else
  191. error.AppendLine();
  192. };
  193. process.BeginOutputReadLine();
  194. process.BeginErrorReadLine();
  195. if (Timeout != null)
  196. {
  197. var exited = process.WaitForExit(Timeout.Value);
  198. if (!exited)
  199. {
  200. process.Kill();
  201. throw new TimeoutException($"Command '{Command}' timeout exceeded.");
  202. }
  203. }
  204. else process.WaitForExit();
  205. StandardError = error.ToString();
  206. if (process.ExitCode != 0)
  207. {
  208. Console.WriteLine($"PowerShell instance exited with error code: {process.ExitCode}");
  209. if (!String.IsNullOrWhiteSpace(StandardError)) Console.WriteLine($"Error message: {StandardError}");
  210. ErrorLogger.WriteToErrorLog("PowerShell exited with a non-zero exit code: " + process.ExitCode, null, "PowerShellAction Error", Command);
  211. this.ExitCode = process.ExitCode;
  212. }
  213. else
  214. {
  215. if (!String.IsNullOrWhiteSpace(StandardError)) Console.WriteLine($"Error output: {StandardError}");
  216. ExitCode = 0;
  217. }
  218. process.CancelOutputRead();
  219. process.CancelErrorRead();
  220. process.Dispose();
  221. }
  222. private static bool ExeRunning(string exe, int id)
  223. {
  224. try
  225. {
  226. return Process.GetProcessesByName(Path.GetFileNameWithoutExtension(exe)).Any(x => x.Id == id);
  227. }
  228. catch (Exception)
  229. {
  230. return false;
  231. }
  232. }
  233. private void PrivilegedProcOutputHandler(object sendingProcess, AugmentedProcess.DataReceivedEventArgs outLine)
  234. {
  235. var outputString = outLine.Data;
  236. // Collect the sort command output.
  237. if (!String.IsNullOrEmpty(outLine.Data))
  238. {
  239. if (outputString.Contains("\\AME"))
  240. {
  241. outputString = outputString.Substring(outputString.IndexOf('>') + 1);
  242. }
  243. Console.WriteLine(outputString);
  244. }
  245. else
  246. {
  247. Console.WriteLine();
  248. }
  249. }
  250. private void ProcOutputHandler(object sendingProcess, DataReceivedEventArgs outLine)
  251. {
  252. var outputString = outLine.Data;
  253. // Collect the sort command output.
  254. if (!String.IsNullOrEmpty(outLine.Data))
  255. {
  256. if (outputString.Contains("\\AME"))
  257. {
  258. outputString = outputString.Substring(outputString.IndexOf('>') + 1);
  259. }
  260. Console.WriteLine(outputString);
  261. }
  262. else
  263. {
  264. Console.WriteLine();
  265. }
  266. }
  267. }
  268. }