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);
}
///
/// 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();
}
}
///
/// 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 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 Process 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;
}
///
/// Check if the process exited with the expected exit code.
///
/// Process to check.
/// Expected exit code.
/// Process stdout.
/// Process stderr.
/// Thrown if exit code does not match expected value.
// ReSharper disable once MemberCanBePrivate.Global
public static Task ValidateExitConditionAsync(Process process, int expectedExitCode = 0, string? stdout = null, string? stderr = null)
{
var exitCode = process.ExitCode;
if (exitCode == expectedExitCode)
{
return Task.CompletedTask;
}
var pName = process.StartInfo.FileName;
var msg = $"Process {pName} failed with exit-code {exitCode}. stdout: '{stdout}', stderr: '{stderr}'";
Logger.Error(msg);
throw new ProcessException(msg);
}
///
/// 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)
{
var stdout = new StringBuilder();
var stderr = new StringBuilder();
process.OutputDataReceived += (_, args) => stdout.Append(args.Data);
process.ErrorDataReceived += (_, args) => stderr.Append(args.Data);
await process.WaitForExitAsync(cancelToken).ConfigureAwait(false);
await ValidateExitConditionAsync(process, expectedExitCode, stdout.ToString(), stderr.ToString())
.ConfigureAwait(false);
}
}