using System.Diagnostics; using System.Text; using NLog; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Helper; namespace StabilityMatrix.Core.Processes; public static class ProcessRunner { private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); /// /// Opens the given URL in the default browser. /// /// URL as string public static void OpenUrl(string url) { Logger.Debug($"Opening URL '{url}'"); Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); } /// /// Opens the given URL in the default browser. /// /// URI, using AbsoluteUri component public static void OpenUrl(Uri url) { OpenUrl(url.AbsoluteUri); } /// /// Start an executable or .app on macOS. /// public static Process StartApp(string path, ProcessArgs args) { var startInfo = new ProcessStartInfo(); if (Compat.IsMacOS) { startInfo.FileName = "open"; startInfo.Arguments = args.Prepend(path).ToString(); startInfo.UseShellExecute = true; } else { startInfo.FileName = path; startInfo.Arguments = args; } return Process.Start(startInfo) ?? throw new NullReferenceException("Process.Start returned null"); } /// /// Opens the given folder in the system file explorer. /// public static async Task OpenFolderBrowser(string directoryPath) { if (Compat.IsWindows) { using var process = new Process(); process.StartInfo.FileName = "explorer.exe"; process.StartInfo.Arguments = Quote(directoryPath); process.Start(); await process.WaitForExitAsync().ConfigureAwait(false); } else if (Compat.IsLinux) { using var process = new Process(); process.StartInfo.FileName = directoryPath; process.StartInfo.UseShellExecute = true; process.Start(); await process.WaitForExitAsync().ConfigureAwait(false); } else if (Compat.IsMacOS) { using var process = new Process(); process.StartInfo.FileName = "open"; process.StartInfo.Arguments = Quote(directoryPath); process.Start(); await process.WaitForExitAsync().ConfigureAwait(false); } else { throw new PlatformNotSupportedException(); } } /// /// Opens the given file within its folder in the system file explorer. /// public static async Task OpenFileBrowser(string filePath) { if (Compat.IsWindows) { using var process = new Process(); process.StartInfo.FileName = "explorer.exe"; process.StartInfo.Arguments = $"/select, {Quote(filePath)}"; process.Start(); await process.WaitForExitAsync().ConfigureAwait(false); } else if (Compat.IsLinux) { using var process = new Process(); process.StartInfo.FileName = "dbus-send"; process.StartInfo.Arguments = "--print-reply --dest=org.freedesktop.FileManager1 " + "/org/freedesktop/FileManager1 org.freedesktop.FileManager1.ShowItems " + $"array:string:\"file://{filePath}\" string:\"\""; process.StartInfo.UseShellExecute = true; process.Start(); await process.WaitForExitAsync().ConfigureAwait(false); } else if (Compat.IsMacOS) { using var process = new Process(); process.StartInfo.FileName = "open"; process.StartInfo.Arguments = $"-R {Quote(filePath)}"; process.Start(); await process.WaitForExitAsync().ConfigureAwait(false); } else { throw new PlatformNotSupportedException(); } } /// /// Starts and tracks a process. /// private static Process StartTrackedProcess(Process process) { process.Start(); // Currently only supported on Windows if (Compat.IsWindows) { // Supress errors here since the process may have already exited try { ProcessTracker.AddProcess(process); } catch (InvalidOperationException) { } } return process; } public static async Task GetProcessOutputAsync( string fileName, string arguments, string? workingDirectory = null, Dictionary? environmentVariables = null ) { Logger.Debug($"Starting process '{fileName}' with arguments '{arguments}'"); var info = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, CreateNoWindow = true }; if (environmentVariables != null) { foreach (var (key, value) in environmentVariables) { info.EnvironmentVariables[key] = value; } } if (workingDirectory != null) { info.WorkingDirectory = workingDirectory; } using var process = new Process(); process.StartInfo = info; StartTrackedProcess(process); var output = await process.StandardOutput.ReadToEndAsync(); await process.WaitForExitAsync(); return output; } public static async Task GetProcessResultAsync( string fileName, ProcessArgs arguments, string? workingDirectory = null, IReadOnlyDictionary? environmentVariables = null ) { Logger.Debug($"Starting process '{fileName}' with arguments '{arguments}'"); var info = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; if (environmentVariables != null) { foreach (var (key, value) in environmentVariables) { info.EnvironmentVariables[key] = value; } } if (workingDirectory != null) { info.WorkingDirectory = workingDirectory; } using var process = new Process(); process.StartInfo = info; StartTrackedProcess(process); var stdout = await process.StandardOutput.ReadToEndAsync().ConfigureAwait(false); var stderr = await process.StandardError.ReadToEndAsync().ConfigureAwait(false); await process.WaitForExitAsync().ConfigureAwait(false); string? processName = null; TimeSpan elapsed = default; // Accessing these properties may throw an exception if the process has already exited try { processName = process.ProcessName; } catch (SystemException) { } try { elapsed = process.ExitTime - process.StartTime; } catch (SystemException) { } return new ProcessResult { ExitCode = process.ExitCode, StandardOutput = stdout, StandardError = stderr, ProcessName = processName, Elapsed = elapsed }; } public static Process StartProcess( string fileName, string arguments, string? workingDirectory = null, Action? outputDataReceived = null, IReadOnlyDictionary? environmentVariables = null ) { Logger.Debug($"Starting process '{fileName}' with arguments '{arguments}'"); var info = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; if (environmentVariables != null) { foreach (var (key, value) in environmentVariables) { info.EnvironmentVariables[key] = value; } } if (workingDirectory != null) { info.WorkingDirectory = workingDirectory; } var process = new Process { StartInfo = info }; StartTrackedProcess(process); if (outputDataReceived == null) return process; process.OutputDataReceived += (sender, args) => outputDataReceived(args.Data); process.ErrorDataReceived += (sender, args) => outputDataReceived(args.Data); process.BeginOutputReadLine(); process.BeginErrorReadLine(); return process; } public static AnsiProcess StartAnsiProcess( string fileName, string arguments, string? workingDirectory = null, Action? outputDataReceived = null, IReadOnlyDictionary? environmentVariables = null ) { Logger.Debug( $"Starting process '{fileName}' with arguments '{arguments}' in working directory '{workingDirectory}'" ); var info = new ProcessStartInfo { FileName = fileName, Arguments = arguments, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; if (environmentVariables != null) { foreach (var (key, value) in environmentVariables) { info.EnvironmentVariables[key] = value; } } if (workingDirectory != null) { info.WorkingDirectory = workingDirectory; } var process = new AnsiProcess(info); StartTrackedProcess(process); if (outputDataReceived != null) { process.BeginAnsiRead(outputDataReceived); } return process; } public static AnsiProcess StartAnsiProcess( string fileName, IEnumerable arguments, string? workingDirectory = null, Action? outputDataReceived = null, Dictionary? environmentVariables = null ) { // Quote arguments containing spaces var args = string.Join(" ", arguments.Where(s => !string.IsNullOrEmpty(s)).Select(Quote)); return StartAnsiProcess(fileName, args, workingDirectory, outputDataReceived, environmentVariables); } public static async Task RunBashCommand(string command, string workingDirectory = "") { // Escape any single quotes in the command var escapedCommand = command.Replace("\"", "\\\""); var arguments = $"-c \"{escapedCommand}\""; Logger.Info($"Running bash command [bash {arguments}]"); var processInfo = new ProcessStartInfo("bash", arguments) { UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, WorkingDirectory = workingDirectory, }; using var process = new Process(); process.StartInfo = processInfo; var stdout = new StringBuilder(); var stderr = new StringBuilder(); process.OutputDataReceived += (_, args) => stdout.Append(args.Data); process.ErrorDataReceived += (_, args) => stderr.Append(args.Data); StartTrackedProcess(process); process.BeginOutputReadLine(); process.BeginErrorReadLine(); await process.WaitForExitAsync().ConfigureAwait(false); return new ProcessResult { ExitCode = process.ExitCode, StandardOutput = stdout.ToString(), StandardError = stderr.ToString() }; } public static Task RunBashCommand( IEnumerable commands, string workingDirectory = "" ) { // Quote arguments containing spaces var args = string.Join(" ", commands.Select(Quote)); return RunBashCommand(args, workingDirectory); } /// /// Quotes argument with double quotes if it contains spaces, /// and does not already start and end with double quotes. /// public static string Quote(string argument) { var inner = argument.Trim('"'); return inner.Contains(' ') ? $"\"{inner}\"" : argument; } /// /// Waits for process to exit, then validates exit code. /// /// Process to check. /// Expected exit code. /// Cancellation token. /// Thrown if exit code does not match expected value. public static async Task WaitForExitConditionAsync( Process process, int expectedExitCode = 0, CancellationToken cancelToken = default ) { if (!process.HasExited) { await process.WaitForExitAsync(cancelToken).ConfigureAwait(false); } if (process.ExitCode == expectedExitCode) { return; } // Accessing ProcessName may error on some platforms string? processName = null; try { processName = process.ProcessName; } catch (SystemException) { } throw new ProcessException( "Process " + (processName == null ? "" : processName + " ") + $"failed with exit-code {process.ExitCode}." ); } }