using System.Diagnostics; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; using TuneRipper.Api.Configuration; namespace TuneRipper.Api.Health; public sealed class ExternalToolsHealthCheck(IOptions options) : IHealthCheck { public async Task CheckHealthAsync( HealthCheckContext context, CancellationToken cancellationToken = default) { var ytDlp = await ProbeAsync(options.Value.ExecutablePath, "--version", cancellationToken); if (!ytDlp.Success) { return HealthCheckResult.Unhealthy($"yt-dlp is unavailable: {ytDlp.Message}"); } var ffmpeg = await ProbeAsync(options.Value.FfmpegExecutablePath, "-version", cancellationToken); if (!ffmpeg.Success) { return HealthCheckResult.Unhealthy($"ffmpeg is unavailable: {ffmpeg.Message}"); } return HealthCheckResult.Healthy($"yt-dlp {ytDlp.Message}; ffmpeg available"); } private static async Task<(bool Success, string Message)> ProbeAsync( string executable, string argument, CancellationToken cancellationToken) { try { var startInfo = new ProcessStartInfo { FileName = executable, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true }; startInfo.ArgumentList.Add(argument); using var process = Process.Start(startInfo); if (process is null) { return (false, "process could not be started"); } using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeout.CancelAfter(TimeSpan.FromSeconds(10)); var outputTask = process.StandardOutput.ReadLineAsync(timeout.Token).AsTask(); await process.WaitForExitAsync(timeout.Token); var output = (await outputTask)?.Trim(); return process.ExitCode == 0 ? (true, string.IsNullOrWhiteSpace(output) ? "available" : output) : (false, $"exit code {process.ExitCode}"); } catch (Exception ex) { return (false, ex.Message); } } }