116 lines
3.4 KiB
C#
116 lines
3.4 KiB
C#
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<string> 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<string> 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);
|
||
}
|
||
|
||
/// <summary>Pick the most plausible RPM (typically 500–20000) from ectool text output.</summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>Pick duty percent (0-100) from ectool text output.</summary>
|
||
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;
|
||
}
|
||
}
|