Multi-Platform Package Manager for Stable Diffusion
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.

323 lines
11 KiB

using System.Diagnostics;
using System.Text;
using NLog;
using StabilityMatrix.Core.Exceptions;
using StabilityMatrix.Core.Helper;
namespace StabilityMatrix.Core.Processes;
public record struct ProcessResult(int ExitCode, string? StandardOutput, string? StandardError);
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>
/// 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>
/// 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 Process StartProcess(
string fileName,
string arguments,
string? workingDirectory = null,
Action<string?>? outputDataReceived = 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,
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,
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,
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<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 = "")
{
// 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(process.ExitCode, stdout.ToString(), stderr.ToString());
}
public static Task<ProcessResult> RunBashCommand(
IEnumerable<string> commands,
string workingDirectory = "")
{
// Quote arguments containing spaces
var args = string.Join(" ", commands.Select(Quote));
return RunBashCommand(args, workingDirectory);
}
/// <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>
/// Check if the process exited with the expected exit code.
/// </summary>
/// <param name="process">Process to check.</param>
/// <param name="expectedExitCode">Expected exit code.</param>
/// <param name="stdout">Process stdout.</param>
/// <param name="stderr">Process stderr.</param>
/// <exception cref="ProcessException">Thrown if exit code does not match expected value.</exception>
// 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);
}
/// <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)
{
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);
await ValidateExitConditionAsync(process, expectedExitCode, stdout.ToString(), stderr.ToString());
}
}