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> RunEventAsync( IEnumerable hooks, InstallInfo install, string evt, string buildId, Action log, CancellationToken ct) { var failures = new List(); 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 RunAsync(HookDef hook, InstallInfo install, string buildId, Action 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 RunCommandAsync(HookDef hook, InstallInfo install, string game, string buildId, Action 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 RunProcessAsync(ProcessStartInfo psi, string workDir, string label, Action 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 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(); } }