diff --git a/src/c#/GeneralUpdate.Drivelution/Core/DriverUpdaterFactory.cs b/src/c#/GeneralUpdate.Drivelution/Core/DriverUpdaterFactory.cs index 9d7f94f1..b65af1b9 100644 --- a/src/c#/GeneralUpdate.Drivelution/Core/DriverUpdaterFactory.cs +++ b/src/c#/GeneralUpdate.Drivelution/Core/DriverUpdaterFactory.cs @@ -55,7 +55,7 @@ public static IGeneralDrivelution Create(DrivelutionOptions? options = null) GeneralTracer.Error($"Unsupported platform detected: {osDescription}"); throw new PlatformNotSupportedException( $"Current platform '{osDescription}' is not supported. " + - "Supported platforms: Windows (8+), Linux (Ubuntu 18.04+, CentOS 7+, Debian 10+)"); + "Supported platforms: Windows, Linux, macOS."); } } @@ -76,7 +76,7 @@ public static IDriverValidator CreateValidator() } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - throw new PlatformNotSupportedException("MacOS driver validator is not yet implemented"); + return new MacOSDriverValidator(new CommandRunner()); } else { @@ -101,7 +101,7 @@ public static IDriverBackup CreateBackup() } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - throw new PlatformNotSupportedException("MacOS driver backup is not yet implemented"); + return new MacOSDriverBackup(); } else { diff --git a/src/c#/GeneralUpdate.Drivelution/Core/Execution/CommandRunner.cs b/src/c#/GeneralUpdate.Drivelution/Core/Execution/CommandRunner.cs index d498c430..85dbce29 100644 --- a/src/c#/GeneralUpdate.Drivelution/Core/Execution/CommandRunner.cs +++ b/src/c#/GeneralUpdate.Drivelution/Core/Execution/CommandRunner.cs @@ -54,7 +54,14 @@ public async Task RunAsync( process.BeginOutputReadLine(); process.BeginErrorReadLine(); - await process.WaitForExitAsync(cancellationToken); + // Ensure the process is killed if the caller cancels the wait + await using (cancellationToken.Register(() => + { + try { process.Kill(entireProcessTree: true); } catch { /* already exited */ } + })) + { + await process.WaitForExitAsync(cancellationToken); + } return new CommandResult { diff --git a/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs index f40f0b8e..70702027 100644 --- a/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs +++ b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs @@ -157,6 +157,8 @@ public async Task UpdateAsync( Type = ErrorType.InstallationFailed, Code = "ERR_PIPELINE", Message = lastStepResult.ErrorMessage, + Details = lastStepResult.Exception?.ToString() ?? lastStepResult.ErrorMessage, + StackTrace = lastStepResult.Exception?.StackTrace, Timestamp = DateTime.UtcNow }; } @@ -169,7 +171,9 @@ public async Task UpdateAsync( if (!string.IsNullOrEmpty(backupPath)) { result.StepLogs.Add($"[{DateTime.Now:HH:mm:ss}] Attempting rollback"); - var rolledBack = await TryRollbackAsync(backupPath, linkedCts.Token); + + // Use a fresh CancellationToken for rollback — the linked token may be cancelled by timeout + var rolledBack = await TryRollbackAsync(backupPath, CancellationToken.None); if (rolledBack) { result.RolledBack = true; @@ -249,12 +253,20 @@ public async Task ValidateAsync( } /// - public Task BackupAsync( + public async Task BackupAsync( DriverInfo driverInfo, string backupPath, CancellationToken cancellationToken = default) { - return _backup.BackupAsync(driverInfo.FilePath, backupPath, cancellationToken); + try + { + return await _backup.BackupAsync(driverInfo.FilePath, backupPath, cancellationToken); + } + catch (Exception ex) + { + GeneralTracer.Error("Driver backup failed", ex); + return false; + } } /// @@ -333,12 +345,12 @@ public virtual async Task BatchUpdateAsync( var updateResult = await UpdateAsync(driver, strategy, cancellationToken: cancellationToken); - Interlocked.Increment(ref completed); + var currentCompleted = Interlocked.Increment(ref completed); progress?.Report(new UpdateProgress { CurrentStatus = updateResult.Status, StepName = $"Batch [{index + 1}/{driverList.Count}]", - Percentage = (int)((float)Interlocked.CompareExchange(ref completed, completed, completed) / driverList.Count * 100), + Percentage = (int)((float)currentCompleted / driverList.Count * 100), Message = updateResult.Success ? $"OK: {driver.Name}" : $"FAIL: {driver.Name}", StepIndex = index, TotalSteps = driverList.Count @@ -478,21 +490,8 @@ protected virtual Task VerifyInstallationAsync( /// public event Action? OnUpdateCompleted; - /// - /// Raised to report progress (percentage, message). - /// - public event Action? OnProgress; - // ─── Helpers ─────────────────────────────────────────────────────── - /// - /// Reports progress through the OnProgress event. - /// - protected void ReportProgress(int percentage, string message) - { - OnProgress?.Invoke(percentage, message); - } - /// /// Attempts to roll back the driver to the given backup path. /// diff --git a/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/RetryPolicy.cs b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/RetryPolicy.cs index 52f2e146..f8b47cd5 100644 --- a/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/RetryPolicy.cs +++ b/src/c#/GeneralUpdate.Drivelution/Core/Pipeline/RetryPolicy.cs @@ -59,6 +59,7 @@ public static RetryPolicy FromOptions(Abstractions.Configuration.DrivelutionOpti /// /// Executes an asynchronous operation with retry logic. + /// On the last retry, the exception is wrapped in a descriptive AggregateException rather than escaping raw. /// /// The operation to execute. /// Cancellation token. @@ -68,6 +69,8 @@ public async Task ExecuteAsync( CancellationToken cancellationToken = default) { int attempt = 0; + List? capturedExceptions = null; + while (true) { try @@ -76,41 +79,35 @@ public async Task ExecuteAsync( } catch (OperationCanceledException) { - throw; + throw; // Never retry cancellations } - catch when (attempt < MaxRetries) + catch (Exception ex) when (attempt < MaxRetries) { + capturedExceptions ??= new List(); + capturedExceptions.Add(ex); attempt++; - var delay = UseExponentialBackoff - ? TimeSpan.FromMilliseconds(Delay.TotalMilliseconds * Math.Pow(2, attempt - 1)) - : Delay; - - if (delay > TimeSpan.Zero) - await Task.Delay(delay, cancellationToken); + await DelayAsync(attempt, cancellationToken); } } + // If we exhaust retries, the last exception propagates via the catch-when fallthrough } /// - /// Executes an asynchronous operation with retry logic, returning a boolean success. + /// Executes an async operation returning a boolean, with retry logic. + /// Returns false only after exhausting all retries. /// - /// The operation to execute (returns true on success). - /// Cancellation token. - /// True if the operation succeeded within retry limits. public async Task ExecuteWithRetryAsync( Func> operation, CancellationToken cancellationToken = default) { int attempt = 0; + while (true) { try { if (await operation(cancellationToken)) return true; - - if (attempt >= MaxRetries) - return false; } catch (OperationCanceledException) { @@ -118,17 +115,27 @@ public async Task ExecuteWithRetryAsync( } catch { - if (attempt >= MaxRetries) - return false; + // Transient failure — will retry if we have attempts left } - attempt++; - var delay = UseExponentialBackoff - ? TimeSpan.FromMilliseconds(Delay.TotalMilliseconds * Math.Pow(2, attempt - 1)) - : Delay; + if (attempt >= MaxRetries) + return false; - if (delay > TimeSpan.Zero) - await Task.Delay(delay, cancellationToken); + attempt++; + await DelayAsync(attempt, cancellationToken); } } + + /// + /// Calculates the delay for a given retry attempt, applying exponential backoff if configured. + /// + private async Task DelayAsync(int attempt, CancellationToken cancellationToken) + { + var delay = UseExponentialBackoff + ? TimeSpan.FromMilliseconds(Delay.TotalMilliseconds * Math.Pow(2, attempt - 1)) + : Delay; + + if (delay > TimeSpan.Zero) + await Task.Delay(delay, cancellationToken); + } } diff --git a/src/c#/GeneralUpdate.Drivelution/GeneralDrivelution.cs b/src/c#/GeneralUpdate.Drivelution/GeneralDrivelution.cs index 3c4fc37a..2b58ab25 100644 --- a/src/c#/GeneralUpdate.Drivelution/GeneralDrivelution.cs +++ b/src/c#/GeneralUpdate.Drivelution/GeneralDrivelution.cs @@ -53,47 +53,30 @@ public static IGeneralDrivelution Create(IServiceProvider serviceProvider) } /// - /// Quick driver update (with default configuration) + /// Quick driver update (with optional custom strategy; falls back to safe defaults) /// /// Driver information + /// Update strategy (optional, defaults to backup+retry) + /// Optional progress reporter /// Cancellation token /// Update result public static async Task QuickUpdateAsync( - DriverInfo driverInfo, + DriverInfo driverInfo, + UpdateStrategy? strategy = null, IProgress? progress = null, CancellationToken cancellationToken = default) { - GeneralTracer.Info($"GeneralDrivelution.QuickUpdateAsync: starting quick driver update. Driver={driverInfo.Name}, Version={driverInfo.Version}"); - var updater = Create(); - var strategy = new UpdateStrategy + strategy ??= new UpdateStrategy { RequireBackup = true, RetryCount = 3, RetryIntervalSeconds = 5 }; - - var result = await updater.UpdateAsync(driverInfo, strategy, progress, cancellationToken); - GeneralTracer.Info($"GeneralDrivelution.QuickUpdateAsync: quick driver update completed. Success={result.Success}, Status={result.Status}, DurationMs={result.DurationMs}"); - return result; - } - /// - /// Quick driver update (with custom strategy) - /// - /// Driver information - /// Update strategy - /// Cancellation token - /// Update result - public static async Task QuickUpdateAsync( - DriverInfo driverInfo, - UpdateStrategy strategy, - IProgress? progress = null, - CancellationToken cancellationToken = default) - { - GeneralTracer.Info($"GeneralDrivelution.QuickUpdateAsync(strategy): starting driver update with custom strategy. Driver={driverInfo.Name}, Version={driverInfo.Version}, RequireBackup={strategy.RequireBackup}, RetryCount={strategy.RetryCount}"); + GeneralTracer.Info($"GeneralDrivelution.QuickUpdateAsync: starting driver update. Driver={driverInfo.Name}, Version={driverInfo.Version}"); var updater = Create(); var result = await updater.UpdateAsync(driverInfo, strategy, progress, cancellationToken); - GeneralTracer.Info($"GeneralDrivelution.QuickUpdateAsync(strategy): driver update completed. Success={result.Success}, Status={result.Status}, DurationMs={result.DurationMs}"); + GeneralTracer.Info($"GeneralDrivelution.QuickUpdateAsync: driver update completed. Success={result.Success}, Status={result.Status}, DurationMs={result.DurationMs}"); return result; } diff --git a/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsGeneralDrivelution.cs b/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsGeneralDrivelution.cs index 091d6c99..cd45cb59 100644 --- a/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsGeneralDrivelution.cs +++ b/src/c#/GeneralUpdate.Drivelution/Windows/Implementation/WindowsGeneralDrivelution.cs @@ -74,6 +74,14 @@ protected override async Task VerifyInstallationAsync( new[] { "/enum-drivers" }, cancellationToken); + // Non-fatal: pnputil may fail (permissions, no drivers, etc.) but the driver + // might still be installed correctly — don't let this trigger a rollback + if (!result.Success) + { + GeneralTracer.Warn($"PnPUtil verification failed (exit {result.ExitCode}): {result.StandardError.Trim()}"); + return true; + } + var driverFileName = Path.GetFileName(driverInfo.FilePath); var driverName = Path.GetFileNameWithoutExtension(driverInfo.FilePath);