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.

302 lines
10 KiB

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