using System.Diagnostics; using System.Text.RegularExpressions; namespace ChromeboxFanControl; public sealed class EctoolRunner { private static readonly Regex NumberRegex = new(@"\d+", RegexOptions.Compiled); public static (bool ok, string stdout, string stderr) RunSync( string exePath, IReadOnlyList args, int timeoutMs = 15_000) => RunAsync(exePath, args, CancellationToken.None, timeoutMs).GetAwaiter().GetResult(); public static async Task<(bool ok, string stdout, string stderr)> RunAsync( string exePath, IReadOnlyList args, CancellationToken ct = default, int timeoutMs = 15_000) { if (string.IsNullOrWhiteSpace(exePath) || !File.Exists(exePath)) return (false, "", "ectool executable not found."); var psi = new ProcessStartInfo { FileName = exePath, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }; foreach (var a in args) psi.ArgumentList.Add(a); using var proc = new Process { StartInfo = psi }; try { proc.Start(); } catch (Exception ex) { return (false, "", ex.Message); } var stdoutTask = proc.StandardOutput.ReadToEndAsync(ct); var stderrTask = proc.StandardError.ReadToEndAsync(ct); var waitTask = proc.WaitForExitAsync(ct); var completed = await Task.WhenAny(waitTask, Task.Delay(timeoutMs, ct)).ConfigureAwait(false); if (completed != waitTask) { try { proc.Kill(entireProcessTree: true); } catch { /* ignore */ } return (false, "", "ectool timed out."); } await waitTask.ConfigureAwait(false); var stdout = await stdoutTask.ConfigureAwait(false); var stderr = await stderrTask.ConfigureAwait(false); var ok = proc.ExitCode == 0; return (ok, stdout, stderr); } /// Pick the most plausible RPM (typically 500–20000) from ectool text output. public static int? TryParseFanRpm(string stdout) { if (string.IsNullOrWhiteSpace(stdout)) return null; int? best = null; foreach (Match m in NumberRegex.Matches(stdout)) { if (!int.TryParse(m.Value, out var n)) continue; if (n is < 400 or > 50_000) continue; // Prefer larger numbers in typical fan range (avoid fan index "0") if (n >= 800 && (best == null || n > best)) best = n; } if (best != null) return best; foreach (Match m in NumberRegex.Matches(stdout)) { if (int.TryParse(m.Value, out var n) && n is >= 0 and <= 50_000) return n; } return null; } /// Pick duty percent (0-100) from ectool text output. public static int? TryParseFanDuty(string stdout) { if (string.IsNullOrWhiteSpace(stdout)) return null; foreach (Match m in NumberRegex.Matches(stdout)) { if (int.TryParse(m.Value, out var n) && n is >= 0 and <= 100) return n; } return null; } }