CVR Manager 1.0.0
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using CVRManager.Models;
|
||||
|
||||
namespace CVRManager.Services;
|
||||
|
||||
public static class HookService
|
||||
{
|
||||
static readonly TimeSpan Timeout = TimeSpan.FromMinutes(5);
|
||||
|
||||
public static async Task<List<string>> RunEventAsync(
|
||||
IEnumerable<HookDef> hooks, InstallInfo install, string evt, string buildId, Action<string> log, CancellationToken ct)
|
||||
{
|
||||
var failures = new List<string>();
|
||||
foreach (var hook in hooks.Where(h => install.HookIds.Contains(h.Id) && h.Runs(evt)))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await RunAsync(hook, install, buildId, log, ct)) failures.Add(hook.Name);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception e)
|
||||
{
|
||||
log($"[{hook.Name}] failed: {e.Message}");
|
||||
failures.Add(hook.Name);
|
||||
}
|
||||
}
|
||||
return failures;
|
||||
}
|
||||
|
||||
public static async Task<bool> RunAsync(HookDef hook, InstallInfo install, string buildId, Action<string> log, CancellationToken ct)
|
||||
{
|
||||
var game = Paths.GameDir(install);
|
||||
if (!Directory.Exists(Path.Combine(game, "ChilloutVR_Data")))
|
||||
{
|
||||
log($"[{hook.Name}] skipped: the game files are not installed yet.");
|
||||
return true;
|
||||
}
|
||||
log($"[{hook.Name}] running for {install.Name}...");
|
||||
return await RunCommandAsync(hook, install, game, buildId, log, ct);
|
||||
}
|
||||
|
||||
static async Task<bool> RunCommandAsync(HookDef hook, InstallInfo install, string game, string buildId, Action<string> log, CancellationToken ct)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(hook.Command)) throw new InvalidOperationException("The hook has no command set.");
|
||||
string Expand(string s) => s
|
||||
.Replace("{game}", game, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{install}", install.Name, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{branch}", install.Branch, StringComparison.OrdinalIgnoreCase)
|
||||
.Replace("{build}", buildId, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var command = Expand(hook.Command.Trim());
|
||||
var args = SplitArgs(Expand(hook.Arguments)).ToList();
|
||||
var ext = Path.GetExtension(command).ToLowerInvariant();
|
||||
ProcessStartInfo psi;
|
||||
if (ext == ".ps1")
|
||||
{
|
||||
psi = new ProcessStartInfo(Path.Combine(Environment.SystemDirectory, "WindowsPowerShell", "v1.0", "powershell.exe"));
|
||||
foreach (var a in new[] { "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command }) psi.ArgumentList.Add(a);
|
||||
}
|
||||
else if (ext is ".bat" or ".cmd")
|
||||
{
|
||||
psi = new ProcessStartInfo("cmd.exe");
|
||||
psi.ArgumentList.Add("/c");
|
||||
psi.ArgumentList.Add(command);
|
||||
}
|
||||
else psi = new ProcessStartInfo(command);
|
||||
foreach (var a in args) psi.ArgumentList.Add(a);
|
||||
|
||||
psi.Environment["CVR_GAME_DIR"] = game;
|
||||
psi.Environment["CVR_INSTALL_NAME"] = install.Name;
|
||||
psi.Environment["CVR_BRANCH"] = install.Branch;
|
||||
psi.Environment["CVR_BUILD"] = buildId;
|
||||
|
||||
var code = await RunProcessAsync(psi, game, hook.Name, log, ct);
|
||||
if (code != 0) log($"[{hook.Name}] exited with code {code}");
|
||||
return code == 0;
|
||||
}
|
||||
|
||||
static async Task<int> RunProcessAsync(ProcessStartInfo psi, string workDir, string label, Action<string> log, CancellationToken ct)
|
||||
{
|
||||
psi.WorkingDirectory = workDir;
|
||||
psi.UseShellExecute = false;
|
||||
psi.CreateNoWindow = true;
|
||||
psi.RedirectStandardOutput = true;
|
||||
psi.RedirectStandardError = true;
|
||||
psi.RedirectStandardInput = true;
|
||||
psi.StandardOutputEncoding = Encoding.UTF8;
|
||||
psi.StandardErrorEncoding = Encoding.UTF8;
|
||||
|
||||
using var proc = new Process { StartInfo = psi };
|
||||
proc.OutputDataReceived += (_, e) => { if (!string.IsNullOrWhiteSpace(e.Data)) log($"[{label}] {e.Data.TrimEnd()}"); };
|
||||
proc.ErrorDataReceived += (_, e) => { if (!string.IsNullOrWhiteSpace(e.Data)) log($"[{label}] {e.Data.TrimEnd()}"); };
|
||||
proc.Start();
|
||||
proc.StandardInput.Close();
|
||||
proc.BeginOutputReadLine();
|
||||
proc.BeginErrorReadLine();
|
||||
|
||||
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
cts.CancelAfter(Timeout);
|
||||
try
|
||||
{
|
||||
await proc.WaitForExitAsync(cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
try { proc.Kill(true); } catch { }
|
||||
if (ct.IsCancellationRequested) throw;
|
||||
throw new TimeoutException($"timed out after {Timeout.TotalMinutes:0} minutes");
|
||||
}
|
||||
proc.WaitForExit();
|
||||
return proc.ExitCode;
|
||||
}
|
||||
|
||||
public static IEnumerable<string> SplitArgs(string s)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var quoted = false;
|
||||
foreach (var c in s)
|
||||
{
|
||||
if (c == '"') quoted = !quoted;
|
||||
else if (char.IsWhiteSpace(c) && !quoted)
|
||||
{
|
||||
if (sb.Length > 0) { yield return sb.ToString(); sb.Clear(); }
|
||||
}
|
||||
else sb.Append(c);
|
||||
}
|
||||
if (sb.Length > 0) yield return sb.ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user