CVR Manager 1.0.0

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Exterrata
2026-10-25 02:46:35 -04:00
commit 9bb4f85802
59 changed files with 7913 additions and 0 deletions
+130
View File
@@ -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();
}
}