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.
467 lines
14 KiB
467 lines
14 KiB
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(); |
|
|
|
/// <summary> |
|
/// Opens the given URL in the default browser. |
|
/// </summary> |
|
/// <param name="url">URL as string</param> |
|
public static void OpenUrl(string url) |
|
{ |
|
Logger.Debug($"Opening URL '{url}'"); |
|
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); |
|
} |
|
|
|
/// <summary> |
|
/// Opens the given URL in the default browser. |
|
/// </summary> |
|
/// <param name="url">URI, using AbsoluteUri component</param> |
|
public static void OpenUrl(Uri url) |
|
{ |
|
OpenUrl(url.AbsoluteUri); |
|
} |
|
|
|
/// <summary> |
|
/// Start an executable or .app on macOS. |
|
/// </summary> |
|
public static Process StartApp(string path, ProcessArgs args) |
|
{ |
|
var startInfo = new ProcessStartInfo(); |
|
|
|
if (Compat.IsMacOS) |
|
{ |
|
startInfo.FileName = "open"; |
|
startInfo.Arguments = args.Prepend([path, "--args"]).ToString(); |
|
startInfo.UseShellExecute = true; |
|
} |
|
else |
|
{ |
|
startInfo.FileName = path; |
|
startInfo.Arguments = args; |
|
} |
|
|
|
return Process.Start(startInfo) ?? throw new NullReferenceException("Process.Start returned null"); |
|
} |
|
|
|
/// <summary> |
|
/// Opens the given folder in the system file explorer. |
|
/// </summary> |
|
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(); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Opens the given file within its folder in the system file explorer. |
|
/// </summary> |
|
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(); |
|
} |
|
} |
|
|
|
/// <summary> |
|
/// Starts and tracks a process. |
|
/// </summary> |
|
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<string> GetProcessOutputAsync( |
|
string fileName, |
|
string arguments, |
|
string? workingDirectory = null, |
|
Dictionary<string, string>? 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<ProcessResult> GetProcessResultAsync( |
|
string fileName, |
|
ProcessArgs arguments, |
|
string? workingDirectory = null, |
|
IReadOnlyDictionary<string, string>? 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<string?>? outputDataReceived = null, |
|
IReadOnlyDictionary<string, string>? 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<ProcessOutput>? outputDataReceived = null, |
|
IReadOnlyDictionary<string, string>? 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<string> arguments, |
|
string? workingDirectory = null, |
|
Action<ProcessOutput>? outputDataReceived = null, |
|
Dictionary<string, string>? 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<ProcessResult> RunBashCommand( |
|
string command, |
|
string workingDirectory = "", |
|
IReadOnlyDictionary<string, string>? environmentVariables = null |
|
) |
|
{ |
|
// 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, |
|
}; |
|
|
|
if (environmentVariables != null) |
|
{ |
|
foreach (var (key, value) in environmentVariables) |
|
{ |
|
processInfo.EnvironmentVariables[key] = value; |
|
} |
|
} |
|
|
|
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<ProcessResult> RunBashCommand( |
|
IEnumerable<string> commands, |
|
string workingDirectory = "", |
|
IReadOnlyDictionary<string, string>? environmentVariables = null |
|
) |
|
{ |
|
// Quote arguments containing spaces |
|
var args = string.Join(" ", commands.Select(Quote)); |
|
return RunBashCommand(args, workingDirectory, environmentVariables); |
|
} |
|
|
|
/// <summary> |
|
/// Quotes argument with double quotes if it contains spaces, |
|
/// and does not already start and end with double quotes. |
|
/// </summary> |
|
public static string Quote(string argument) |
|
{ |
|
var inner = argument.Trim('"'); |
|
return inner.Contains(' ') ? $"\"{inner}\"" : argument; |
|
} |
|
|
|
/// <summary> |
|
/// Waits for process to exit, then validates exit code. |
|
/// </summary> |
|
/// <param name="process">Process to check.</param> |
|
/// <param name="expectedExitCode">Expected exit code.</param> |
|
/// <param name="cancelToken">Cancellation token.</param> |
|
/// <exception cref="ProcessException">Thrown if exit code does not match expected value.</exception> |
|
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}." |
|
); |
|
} |
|
}
|
|
|