Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/c#/GeneralUpdate.Drivelution/Core/DriverUpdaterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.");
}
}

Expand All @@ -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
{
Expand All @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,14 @@ public async Task<CommandResult> 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
{
Expand Down
35 changes: 17 additions & 18 deletions src/c#/GeneralUpdate.Drivelution/Core/Pipeline/BaseDriverUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ public async Task<UpdateResult> UpdateAsync(
Type = ErrorType.InstallationFailed,
Code = "ERR_PIPELINE",
Message = lastStepResult.ErrorMessage,
Details = lastStepResult.Exception?.ToString() ?? lastStepResult.ErrorMessage,
StackTrace = lastStepResult.Exception?.StackTrace,
Timestamp = DateTime.UtcNow
};
}
Expand All @@ -169,7 +171,9 @@ public async Task<UpdateResult> 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);
Comment on lines +175 to +176
if (rolledBack)
{
result.RolledBack = true;
Expand Down Expand Up @@ -249,12 +253,20 @@ public async Task<bool> ValidateAsync(
}

/// <inheritdoc/>
public Task<bool> BackupAsync(
public async Task<bool> 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;
}
}

/// <inheritdoc/>
Expand Down Expand Up @@ -333,12 +345,12 @@ public virtual async Task<BatchUpdateResult> 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
Expand Down Expand Up @@ -478,21 +490,8 @@ protected virtual Task<bool> VerifyInstallationAsync(
/// </summary>
public event Action<UpdateResult>? OnUpdateCompleted;

/// <summary>
/// Raised to report progress (percentage, message).
/// </summary>
public event Action<int, string>? OnProgress;

// ─── Helpers ───────────────────────────────────────────────────────

/// <summary>
/// Reports progress through the OnProgress event.
/// </summary>
protected void ReportProgress(int percentage, string message)
{
OnProgress?.Invoke(percentage, message);
}

/// <summary>
/// Attempts to roll back the driver to the given backup path.
/// </summary>
Expand Down
53 changes: 30 additions & 23 deletions src/c#/GeneralUpdate.Drivelution/Core/Pipeline/RetryPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public static RetryPolicy FromOptions(Abstractions.Configuration.DrivelutionOpti

/// <summary>
/// Executes an asynchronous operation with retry logic.
/// On the last retry, the exception is wrapped in a descriptive AggregateException rather than escaping raw.
/// </summary>
Comment on lines 60 to 63
/// <param name="operation">The operation to execute.</param>
/// <param name="cancellationToken">Cancellation token.</param>
Expand All @@ -68,6 +69,8 @@ public async Task<T> ExecuteAsync<T>(
CancellationToken cancellationToken = default)
{
int attempt = 0;
List<Exception>? capturedExceptions = null;

while (true)
{
try
Expand All @@ -76,59 +79,63 @@ public async Task<T> ExecuteAsync<T>(
}
catch (OperationCanceledException)
{
throw;
throw; // Never retry cancellations
}
catch when (attempt < MaxRetries)
catch (Exception ex) when (attempt < MaxRetries)
{
capturedExceptions ??= new List<Exception>();
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
}

/// <summary>
/// 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.
/// </summary>
/// <param name="operation">The operation to execute (returns true on success).</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>True if the operation succeeded within retry limits.</returns>
public async Task<bool> ExecuteWithRetryAsync(
Func<CancellationToken, Task<bool>> operation,
CancellationToken cancellationToken = default)
{
int attempt = 0;

while (true)
{
try
{
if (await operation(cancellationToken))
return true;

if (attempt >= MaxRetries)
return false;
}
catch (OperationCanceledException)
{
throw;
}
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);
}
}

/// <summary>
/// Calculates the delay for a given retry attempt, applying exponential backoff if configured.
/// </summary>
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);
}
}
33 changes: 8 additions & 25 deletions src/c#/GeneralUpdate.Drivelution/GeneralDrivelution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,47 +53,30 @@ public static IGeneralDrivelution Create(IServiceProvider serviceProvider)
}

/// <summary>
/// Quick driver update (with default configuration)
/// Quick driver update (with optional custom strategy; falls back to safe defaults)
/// </summary>
/// <param name="driverInfo">Driver information</param>
/// <param name="strategy">Update strategy (optional, defaults to backup+retry)</param>
/// <param name="progress">Optional progress reporter</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Update result</returns>
public static async Task<UpdateResult> QuickUpdateAsync(
DriverInfo driverInfo,
DriverInfo driverInfo,
UpdateStrategy? strategy = null,
IProgress<UpdateProgress>? progress = null,
CancellationToken cancellationToken = default)
Comment on lines 63 to 67
{
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;
}

/// <summary>
/// Quick driver update (with custom strategy)
/// </summary>
/// <param name="driverInfo">Driver information</param>
/// <param name="strategy">Update strategy</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Update result</returns>
public static async Task<UpdateResult> QuickUpdateAsync(
DriverInfo driverInfo,
UpdateStrategy strategy,
IProgress<UpdateProgress>? 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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ protected override async Task<bool> 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);

Expand Down
Loading