Skip to content

refactor: split CliBuilder into vertical slices under Commands/#34

Merged
woutervanranst merged 9 commits into
masterfrom
petal-glade
Mar 31, 2026
Merged

refactor: split CliBuilder into vertical slices under Commands/#34
woutervanranst merged 9 commits into
masterfrom
petal-glade

Conversation

@woutervanranst
Copy link
Copy Markdown
Owner

@woutervanranst woutervanranst commented Mar 31, 2026

Summary

  • Extracted each verb and its progress handlers into dedicated files under Commands/{Archive,Restore,Ls,Update}/ with sub-namespaces matching the folder structure
  • Slimmed CliBuilder.cs from ~1452 lines down to ~170 lines — only option factories, root-command assembly, resolution helpers, DI wiring, and audit logging
  • Deleted ProgressHandlers.cs (replaced by ArchiveProgressHandlers.cs and RestoreProgressHandlers.cs)
  • Moved shared display utilities (RenderProgressBar, TruncateAndLeftJustify, SplitSizePair) to DisplayHelpers.cs
  • Updated ProgressTests.cs with new using directives and renamed method references (CliBuilder.BuildArchiveDisplayArchiveVerb.BuildDisplay, etc.)
  • All 137 tests pass

Summary by CodeRabbit

  • New Features

    • archive: upload local data with tiering, removal/pointer options and a live progress UI.
    • restore: retrieve data with interactive rehydration prompts and live progress/confirmation flow.
    • ls: list snapshot contents with filtering and timestamps.
    • update: check for and install the latest application release.
  • Refactor

    • Reorganized command wiring and unified progress rendering for cleaner, more consistent live displays and helpers.

woutervanranst and others added 4 commits March 31, 2026 06:30
Move each verb and its progress handlers into dedicated files under
Commands/{Archive,Restore,Ls,Update}/ with sub-namespaces matching the
folder structure. CliBuilder.cs is slimmed down to option factories,
root-command assembly, resolution helpers, DI, and audit logging only.
ProgressHandlers.cs is deleted; update ProgressTests.cs references.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 31, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4ceabaaa-91e5-4c4b-82ef-b0e0e894d48e

📥 Commits

Reviewing files that changed from the base of the PR and between f38126e and 419d094.

📒 Files selected for processing (1)
  • src/Arius.Cli/Commands/Restore/RestoreVerb.cs
✅ Files skipped from review due to trivial changes (1)
  • src/Arius.Cli/Commands/Restore/RestoreVerb.cs

📝 Walkthrough

Walkthrough

Refactors CLI by extracting archive, restore, ls, and update command builders into dedicated verb classes; splits monolithic progress handlers into Archive- and Restore-specific handlers; adds DisplayHelpers; updates tests to use new BuildDisplay/DisplayHelpers APIs and removes the old consolidated ProgressHandlers file.

Changes

Cohort / File(s) Summary
Root CLI / command registration
src/Arius.Cli/CliBuilder.cs
Delegates subcommand registration to per-verb builders (ArchiveVerb, RestoreVerb, LsVerb, UpdateVerb); removed large in-file verb implementations and progress-display helpers.
Archive verb & progress
src/Arius.Cli/Commands/Archive/ArchiveVerb.cs, src/Arius.Cli/Commands/Archive/ArchiveProgressHandlers.cs
Adds ArchiveVerb.Build(...) and ArchiveVerb.BuildDisplay(ProgressState) plus MediatR handlers that update ProgressState for scanning, hashing, TAR bundling, uploading, and snapshot completion.
Restore verb & progress
src/Arius.Cli/Commands/Restore/RestoreVerb.cs, src/Arius.Cli/Commands/Restore/RestoreProgressHandlers.cs
Adds RestoreVerb.Build(...) and RestoreVerb.BuildDisplay(ProgressState) plus MediatR handlers for restore lifecycle, interactive confirmation callbacks, download progress tracking, and disposition tallies.
Ls verb
src/Arius.Cli/Commands/Ls/LsVerb.cs
Introduces ls subcommand builder that resolves credentials, invokes IMediator for listing, and renders a Spectre table of entries.
Update verb
src/Arius.Cli/Commands/Update/UpdateVerb.cs
Introduces update subcommand builder that checks GitHub Releases, downloads appropriate platform asset, replaces running executable, and reports status.
Display utilities
src/Arius.Cli/DisplayHelpers.cs
Adds DisplayHelpers with SplitSizePair, RenderProgressBar, and TruncateAndLeftJustify used by verb displays and tests.
Progress handlers removal
src/Arius.Cli/ProgressHandlers.cs
Deleted the prior monolithic progress handlers file; logic redistributed into archive/restore-specific handler files.
Tests updated / cleanups
src/Arius.Cli.Tests/ProgressTests.cs, src/Arius.Core.Tests/.../PricingConfigTests.cs, src/Arius.E2E.Tests/RehydrationE2ETests.cs, src/Arius.Integration.Tests/.../RehydrationStateTests.cs
Updated tests to call ArchiveVerb.BuildDisplay, RestoreVerb.BuildDisplay, and DisplayHelpers methods; removed unused using directives and adjusted <see cref="..."/> references.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant User as User (Terminal)
    participant CLI as CLI (Verb)
    participant DI as ServiceProvider (DI)
    participant Mediator as IMediator
    participant State as ProgressState
    participant Console as AnsiConsole / Recorder

    User->>CLI: run "archive"/"restore"/"ls"/"update"
    CLI->>DI: serviceProviderFactory(...)
    CLI->>Mediator: Send ArchiveCommand / RestoreCommand / LsCommand / Update logic
    Mediator->>State: publish progress notifications (scan, hash, tar, upload, download, restore)
    State-->>CLI: progress state mutated
    CLI->>Console: BuildDisplay(ProgressState) / refresh live UI
    Console-->>User: live progress UI and prompts
    User->>Console: interactive confirmations (restore)
    Console->>CLI: signal confirmations
    Mediator->>CLI: command completion result
    CLI->>User: final status output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and accurately summarizes the main refactoring work: splitting a monolithic CliBuilder class into focused vertical slice files organized under Commands/.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch petal-glade

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 31, 2026

Codecov Report

❌ Patch coverage is 69.95169% with 311 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.15%. Comparing base (700e2ab) to head (419d094).
⚠️ Report is 10 commits behind head on master.

Files with missing lines Patch % Lines
src/Arius.Cli/Commands/Restore/RestoreVerb.cs 64.17% 132 Missing and 7 partials ⚠️
src/Arius.Cli/Commands/Update/UpdateVerb.cs 36.48% 94 Missing ⚠️
src/Arius.Cli/Commands/Archive/ArchiveVerb.cs 81.78% 37 Missing and 10 partials ⚠️
src/Arius.Cli/Commands/Ls/LsVerb.cs 82.35% 14 Missing and 4 partials ⚠️
src/Arius.Cli/DisplayHelpers.cs 70.00% 2 Missing and 4 partials ⚠️
...us.Cli/Commands/Restore/RestoreProgressHandlers.cs 90.00% 5 Missing ⚠️
...us.Cli/Commands/Archive/ArchiveProgressHandlers.cs 96.87% 0 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master      #34      +/-   ##
==========================================
- Coverage   83.31%   83.15%   -0.17%     
==========================================
  Files          27       33       +6     
  Lines        3369     3342      -27     
  Branches      402      401       -1     
==========================================
- Hits         2807     2779      -28     
- Misses        471      472       +1     
  Partials       91       91              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (1)
src/Arius.Cli/Commands/Restore/RestoreVerb.cs (1)

142-145: Consider using async/await instead of ContinueWith for clearer exception handling.

The ContinueWith pattern works here since exceptions will propagate when pipelineTask is awaited later. However, the pattern silently swallows the result assignment on failure (t.IsCompletedSuccessfully ? t.Result : null). Consider restructuring for clarity:

Alternative approach
RestoreResult? result = null;
Exception? pipelineException = null;

var pipelineTask = Task.Run(async () =>
{
    try
    {
        result = await mediator.Send(new RestoreCommand(opts), ct);
    }
    catch (Exception ex)
    {
        pipelineException = ex;
        throw;
    }
});

This is a minor readability suggestion; the current code functions correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/Commands/Restore/RestoreVerb.cs` around lines 142 - 145, The
ContinueWith usage on mediator.Send(new RestoreCommand(opts), ct) hides result
assignment on failure and makes exception flow less clear; replace the
ContinueWith with an async/await wrapper: create RestoreResult? result = null
and start pipelineTask as an async Task that awaits mediator.Send(...), assigns
result on success, catches and stores/rethrows exceptions (or simply rethrows
after storing if you need the exception later), then await pipelineTask where
it’s consumed. Update references to pipelineTask, result and mediator.Send(new
RestoreCommand(opts), ct) so exception propagation and result assignment are
explicit and clearer.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Arius.Cli/Commands/Archive/ArchiveProgressHandlers.cs`:
- Around line 86-94: The current TOCTOU uses state.TrackedTars.Count and
Keys.Max() which can throw if another handler (TarBundleUploadedHandler) removes
the last entry; instead locate the active tar by selecting
state.TrackedTars.Values where TarState == TarState.Accumulating, order by
BundleNumber descending and take the first (null-checking) before computing
addedBytes and calling tar.AddEntry; mirror the safe selection pattern used in
TarBundleSealingHandler and keep updating notification.CurrentTarSize -
tar.AccumulatedBytes only when a tar was found.

In `@src/Arius.Cli/Commands/Archive/ArchiveVerb.cs`:
- Around line 232-243: The Uploading header logic incorrectly relies only on
chunksUploaded/tarsUploaded/queueDepth and misses in-flight
TrackedFile/TrackedTar uploads and the SnapshotComplete flag; update the
uploadActive boolean in ArchiveVerb.cs (the expression using chunksUploaded,
tarsUploaded, queueDepth, state.SnapshotComplete, state.ScanComplete) to also
consider any TrackedFile or TrackedTar that is currently uploading (e.g., any
trackedFiles.Any(f => f.IsUploading) || trackedTars.Any(t => t.IsUploading)) and
treat SnapshotComplete appropriately so the header is active while uploads are
in flight and turns green when state.SnapshotComplete indicates success.
- Around line 150-153: The continuation on the mediator.Send( new
ArchiveCommand(opts), ct ) call masks exceptions by using ContinueWith and
setting result = null on failures; change ArchiveVerb to await the original Task
returned by mediator.Send (or await AsTask()) instead of using ContinueWith so
exceptions propagate normally, and if you need to set result capture the awaited
return value into result or use a try/catch around the await to log/handle and
rethrow as appropriate; reference mediator.Send, ArchiveCommand, result and the
ArchiveVerb method to locate where to replace ContinueWith with an awaited call
and proper exception propagation.

In `@src/Arius.Cli/Commands/Restore/RestoreVerb.cs`:
- Around line 292-296: The null/empty error output when result is null should be
replaced with a meaningful message: update the block that checks `if (result is
null || !result.Success)` to determine the error text by first checking
`pipelineTask.IsFaulted` and using
`pipelineTask.Exception?.GetBaseException().Message`, then falling back to
`result?.ErrorMessage` and finally a default string like "Unknown error
occurred"; use that computed message in the `AnsiConsole.MarkupLine` call so
users never see an empty error message.
- Around line 98-108: The ConfirmRehydration and ConfirmCleanup callbacks ignore
their cancellationToken and can hang waiting on answerTcs/task completion;
update each callback (ConfirmRehydration and ConfirmCleanup) to register the
provided cancellationToken so that when cancelled it will unblock the waiting
TaskCompletionSource (questionTcs and cleanupQuestionTcs) and complete/ cancel
the corresponding answer Task (answerTcs and cleanupAnswerTcs) or return a safe
cancelled/false result; ensure you dispose the registration to avoid leaks and
only set the TCS once to avoid race conditions.

In `@src/Arius.Cli/Commands/Update/UpdateVerb.cs`:
- Around line 61-65: The code reads RuntimeInformation.RuntimeIdentifier into
rid and only checks OS substrings, causing mismatches; update the logic in the
UpdateVerb handling (the rid and assetName assignment) to parse the full RID
into OS and architecture components (e.g., split on '-' to get osPart and
archPart), map explicit supported combinations (like "win-x64" ->
"arius-win-x64", "osx-arm64" -> "arius-osx-arm64", "linux-x64" ->
"arius-linux-x64", etc.), and if the RID is not one of the supported combos
return or throw a clear error/exit instead of falling back to a default binary;
ensure assetName is set only for validated mappings so incompatible RIDs are
rejected early.
- Around line 90-125: Download the update into a staging file (use the existing
tempFile and the download logic in the StartAsync block), then verify
integrity/signature of that staging file (validate checksum or digital signature
against a trusted digest) before attempting to replace the running executable;
do not call File.Move(currentExe) while the process is running—instead, spawn or
write an updater helper (or re-exec self) that waits for the current process to
exit and then atomically replaces currentExe with the verified staging file (on
Windows use a separate updater process because OperatingSystem.IsWindows()
prevents in-place replacement, and on Unix you may apply File.SetUnixFileMode to
the staging file before final swap to preserve executable bits). Ensure you
surface verification failures and only proceed to replacement after successful
verification.
- Around line 33-45: The code in UpdateVerb.cs currently slices the raw GitHub
JSON string with IndexOf which is brittle; instead parse the response using
System.Text.Json (e.g., JsonDocument or JsonSerializer) to read the "tag_name"
property safely (use root.GetProperty("tag_name").GetString() with null checks)
and replace the tagStart/tagEnd/tag extraction logic in the method that calls
http.GetStringAsync; do the same fix for the later block (lines ~67-84) where
asset URLs are selected—parse the "assets" array and find the desired asset by
its "name" or "browser_download_url" property rather than string scanning, and
log/return a clear error if properties are missing.

---

Nitpick comments:
In `@src/Arius.Cli/Commands/Restore/RestoreVerb.cs`:
- Around line 142-145: The ContinueWith usage on mediator.Send(new
RestoreCommand(opts), ct) hides result assignment on failure and makes exception
flow less clear; replace the ContinueWith with an async/await wrapper: create
RestoreResult? result = null and start pipelineTask as an async Task that awaits
mediator.Send(...), assigns result on success, catches and stores/rethrows
exceptions (or simply rethrows after storing if you need the exception later),
then await pipelineTask where it’s consumed. Update references to pipelineTask,
result and mediator.Send(new RestoreCommand(opts), ct) so exception propagation
and result assignment are explicit and clearer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 76bbeb88-047d-463e-9de2-75921f3751f7

📥 Commits

Reviewing files that changed from the base of the PR and between 700e2ab and 544a4ea.

📒 Files selected for processing (13)
  • src/Arius.Cli.Tests/ProgressTests.cs
  • src/Arius.Cli/CliBuilder.cs
  • src/Arius.Cli/Commands/Archive/ArchiveProgressHandlers.cs
  • src/Arius.Cli/Commands/Archive/ArchiveVerb.cs
  • src/Arius.Cli/Commands/Ls/LsVerb.cs
  • src/Arius.Cli/Commands/Restore/RestoreProgressHandlers.cs
  • src/Arius.Cli/Commands/Restore/RestoreVerb.cs
  • src/Arius.Cli/Commands/Update/UpdateVerb.cs
  • src/Arius.Cli/DisplayHelpers.cs
  • src/Arius.Cli/ProgressHandlers.cs
  • src/Arius.Core.Tests/Restore/PricingConfigTests.cs
  • src/Arius.E2E.Tests/RehydrationE2ETests.cs
  • src/Arius.Integration.Tests/Pipeline/RehydrationStateTests.cs
💤 Files with no reviewable changes (4)
  • src/Arius.Core.Tests/Restore/PricingConfigTests.cs
  • src/Arius.E2E.Tests/RehydrationE2ETests.cs
  • src/Arius.Integration.Tests/Pipeline/RehydrationStateTests.cs
  • src/Arius.Cli/ProgressHandlers.cs

Comment thread src/Arius.Cli/Commands/Archive/ArchiveProgressHandlers.cs Outdated
Comment thread src/Arius.Cli/Commands/Archive/ArchiveVerb.cs Outdated
Comment on lines +232 to +243
var uploadActive = chunksUploaded > 0 || tarsUploaded > 0 || queueDepth > 0 || !state.SnapshotComplete && state.ScanComplete;
if (uploadActive)
{
var queuePart = queueDepth > 0 ? $" [dim][[{queueDepth} pending]][/]" : string.Empty;
var uploadDone = state.SnapshotComplete;
var uploadSymbol = uploadDone ? "[green]●[/]" : "[yellow]○[/]";
lines.Add(new Markup($" {uploadSymbol} Uploading [dim]{chunksUploaded:N0} unique chunks[/]{queuePart}"));
}
else
{
lines.Add(new Markup(" [grey] Uploading[/]"));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Derive the Uploading stage from live upload state as well.

This header stays grey when the first upload is already in flight but chunksUploaded == 0 and queueDepth == 0, and it also never turns green for fully deduped/empty successful runs. Include active TrackedFile/TrackedTar upload states and SnapshotComplete in this condition.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/Commands/Archive/ArchiveVerb.cs` around lines 232 - 243, The
Uploading header logic incorrectly relies only on
chunksUploaded/tarsUploaded/queueDepth and misses in-flight
TrackedFile/TrackedTar uploads and the SnapshotComplete flag; update the
uploadActive boolean in ArchiveVerb.cs (the expression using chunksUploaded,
tarsUploaded, queueDepth, state.SnapshotComplete, state.ScanComplete) to also
consider any TrackedFile or TrackedTar that is currently uploading (e.g., any
trackedFiles.Any(f => f.IsUploading) || trackedTars.Any(t => t.IsUploading)) and
treat SnapshotComplete appropriately so the header is active while uploads are
in flight and turns green when state.SnapshotComplete indicates success.

Comment thread src/Arius.Cli/Commands/Restore/RestoreVerb.cs
Comment on lines +292 to +296
if (result is null || !result.Success)
{
AnsiConsole.MarkupLine($"[red]Restore failed:[/] {result?.ErrorMessage}");
return 1;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error message could be empty when result is null.

When the pipeline task faults, result is set to null via the ContinueWith callback, but the exception propagates when awaiting pipelineTask. However, if result is null without an exception (edge case), the error message {result?.ErrorMessage} would display empty, which is unhelpful for users.

Proposed fix
                 if (result is null || !result.Success)
                 {
-                    AnsiConsole.MarkupLine($"[red]Restore failed:[/] {result?.ErrorMessage}");
+                    var errorMsg = result?.ErrorMessage ?? "Unknown error occurred";
+                    AnsiConsole.MarkupLine($"[red]Restore failed:[/] {errorMsg}");
                     return 1;
                 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (result is null || !result.Success)
{
AnsiConsole.MarkupLine($"[red]Restore failed:[/] {result?.ErrorMessage}");
return 1;
}
if (result is null || !result.Success)
{
var errorMsg = result?.ErrorMessage ?? "Unknown error occurred";
AnsiConsole.MarkupLine($"[red]Restore failed:[/] {errorMsg}");
return 1;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/Commands/Restore/RestoreVerb.cs` around lines 292 - 296, The
null/empty error output when result is null should be replaced with a meaningful
message: update the block that checks `if (result is null || !result.Success)`
to determine the error text by first checking `pipelineTask.IsFaulted` and using
`pipelineTask.Exception?.GetBaseException().Message`, then falling back to
`result?.ErrorMessage` and finally a default string like "Unknown error
occurred"; use that computed message in the `AnsiConsole.MarkupLine` call so
users never see an empty error message.

Comment thread src/Arius.Cli/Commands/Update/UpdateVerb.cs
Comment thread src/Arius.Cli/Commands/Update/UpdateVerb.cs Outdated
Comment on lines +90 to +125
await AnsiConsole.Progress()
.AutoClear(false)
.Columns(
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new SpinnerColumn())
.StartAsync(async ctx =>
{
var task = ctx.AddTask("[green]Downloading update[/]");
using var response = await http.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, ct);
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength ?? -1;
if (totalBytes > 0) task.MaxValue = totalBytes;

await using var stream = await response.Content.ReadAsStreamAsync(ct);
await using var file = File.Create(tempFile);
var buffer = new byte[81920];
long downloaded = 0;
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, ct)) > 0)
{
await file.WriteAsync(buffer.AsMemory(0, bytesRead), ct);
downloaded += bytesRead;
if (totalBytes > 0) task.Value = downloaded;
}
task.Value = task.MaxValue;
});

var currentExe = Environment.ProcessPath!;
File.Move(tempFile, currentExe, true);

if (!OperatingSystem.IsWindows())
{
File.SetUnixFileMode(currentExe, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Can a running .NET application on Windows replace its own executable with File.Move(..., overwrite: true)? What self-update handoff pattern is typically used instead?

💡 Result:

No, a running .NET application on Windows cannot replace its own executable using File.Move(..., overwrite: true). Windows locks the executable file while the process is running, preventing deletion, overwriting, or moving (if it involves replacing the locked file). File.Move fails with an IOException like "The process cannot access the file because it is being used by another process." The typical self-update handoff pattern is: 1. The main app downloads the new version (e.g., to a temp file). 2. Launches a separate small "updater" executable (passed arguments like process ID, paths). 3. Exits itself. 4. Updater waits for main process to terminate (using Process.WaitForExit or timeout). 5. Updater replaces files (rename old exe if possible, copy new one, clean up). 6. Launches new main app. 7. Updater exits. This is confirmed across StackOverflow, SuperUser, Visual Studio Magazine, and .NET docs. Libraries like AutoUpdater.NET implement this.

Citations:


🏁 Script executed:

cat -n src/Arius.Cli/Commands/Update/UpdateVerb.cs

Repository: woutervanranst/Arius7

Length of output: 7408


Stage and verify the replacement binary before swapping it in.

This implementation has two critical issues: (1) On Windows, File.Move will fail because the executable is locked while the process runs—the move happens before the current process exits, making this impossible without a separate updater. (2) There is no checksum, signature, or integrity verification of the downloaded binary from GitHub; the code trusts whatever asset is returned. Download to a staging location, verify the artifact against a trusted digest or signature, then use a separate updater process that waits for the current process to exit before replacing the executable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/Commands/Update/UpdateVerb.cs` around lines 90 - 125, Download
the update into a staging file (use the existing tempFile and the download logic
in the StartAsync block), then verify integrity/signature of that staging file
(validate checksum or digital signature against a trusted digest) before
attempting to replace the running executable; do not call File.Move(currentExe)
while the process is running—instead, spawn or write an updater helper (or
re-exec self) that waits for the current process to exit and then atomically
replaces currentExe with the verified staging file (on Windows use a separate
updater process because OperatingSystem.IsWindows() prevents in-place
replacement, and on Unix you may apply File.SetUnixFileMode to the staging file
before final swap to preserve executable bits). Ensure you surface verification
failures and only proceed to replacement after successful verification.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (2)
src/Arius.Cli/Commands/Update/UpdateVerb.cs (1)

139-145: ⚠️ Potential issue | 🔴 Critical

Self-update will fail on Windows; running executable is locked.

On Windows, the OS locks the executable file while the process runs. File.Move(tempFile, currentExe, true) will throw an IOException because the current process holds a lock on currentExe. This issue was previously identified and remains unaddressed.

The standard pattern is to:

  1. Download to a staging location (already done)
  2. Spawn a separate updater process or script
  3. Exit the current process
  4. Have the updater wait for termination, then replace the executable
  5. Launch the new version
Example approach for Windows
if (OperatingSystem.IsWindows())
{
    // Write a small batch script that waits and replaces
    var script = Path.Combine(tempDir, "update.cmd");
    File.WriteAllText(script, $"""
        `@echo` off
        timeout /t 2 /nobreak >nul
        move /y "{tempFile}" "{currentExe}"
        start "" "{currentExe}"
        del "%~f0"
        """);
    Process.Start(new ProcessStartInfo(script) { UseShellExecute = true, CreateNoWindow = true });
    Environment.Exit(0);
}
else
{
    File.Move(tempFile, currentExe, true);
    // Unix allows replacing running executables
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/Commands/Update/UpdateVerb.cs` around lines 139 - 145,
Self-update on Windows fails because File.Move(tempFile, currentExe, true)
attempts to overwrite the running executable (currentExe) which is locked;
update the Windows branch inside UpdateVerb (around the code using currentExe
and tempFile) to instead create and launch a short updater (e.g., a .cmd or
small helper exe) that waits for the current process to exit, moves tempFile to
currentExe, restarts the new executable, and cleans up the updater, then call
Environment.Exit(0) from the main process; keep the existing
File.Move/UnixFileMode path for non-Windows (OperatingSystem.IsWindows() check)
so Unix behavior is unchanged.
src/Arius.Cli/Commands/Restore/RestoreVerb.cs (1)

296-300: ⚠️ Potential issue | 🟡 Minor

Error message may be empty when result is null.

When the pipeline task faults, result stays null and {result?.ErrorMessage} produces an empty string. Provide a fallback message for better user feedback.

Proposed fix
             if (result is null || !result.Success)
             {
-                AnsiConsole.MarkupLine($"[red]Restore failed:[/] {result?.ErrorMessage}");
+                var errorMsg = result?.ErrorMessage ?? "An unexpected error occurred";
+                AnsiConsole.MarkupLine($"[red]Restore failed:[/] {errorMsg}");
                 return 1;
             }

Note: The root cause is the ContinueWith pattern swallowing exceptions (lines 146-149). Fixing that would allow surfacing the actual exception message here.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/Commands/Restore/RestoreVerb.cs` around lines 296 - 300, The
failure message can be empty when the pipeline task returns null; update the
RestoreVerb error branch to provide a fallback error string by using the local
result variable (result) and its ErrorMessage if present, otherwise a clear
default like "Unknown error occurred" or the task/exception message;
specifically modify the conditional in RestoreVerb where result is checked and
the AnsiConsole.MarkupLine is called so it prints result?.ErrorMessage ??
"Unknown error occurred" (or a similarly descriptive default) to ensure users
see a meaningful message when result is null.
🧹 Nitpick comments (3)
src/Arius.Cli/Commands/Archive/ArchiveVerb.cs (1)

167-171: Consider logging or exposing more detail on null result.

When result is null, the error message displays {result?.ErrorMessage} which will be empty. This could occur if the task completes unexpectedly. A more informative fallback message would help debugging.

Suggested improvement
 if (result is null || !result.Success)
 {
-    AnsiConsole.MarkupLine($"[red]Archive failed:[/] {result?.ErrorMessage}");
+    AnsiConsole.MarkupLine($"[red]Archive failed:[/] {result?.ErrorMessage ?? "Unknown error (no result returned)"}");
     return 1;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/Commands/Archive/ArchiveVerb.cs` around lines 167 - 171, The
current check in ArchiveVerb that logs "Archive failed" uses
{result?.ErrorMessage} which is empty when result is null; update the failure
logging in the ArchiveVerb command handler (the method that contains the result
null check) to include a clearer fallback message and any available context
(e.g., "result was null", operation name, input parameters or exception info if
available) so you don’t print a blank error; specifically, when result is null
print a descriptive message like "Archive failed: no result returned" and, if
possible, append result?.ErrorMessage and any caught exception details to aid
debugging.
src/Arius.Cli/Commands/Update/UpdateVerb.cs (2)

147-153: Consider logging cleanup failures instead of silent swallowing.

The empty catch block silently discards any cleanup errors. While a cleanup failure shouldn't fail the overall update, logging at a debug level helps diagnose issues like permission problems or locked files.

 try
 {
     Directory.Delete(tempDir, true);
 }
-catch
+catch (Exception ex)
 {
+    AnsiConsole.MarkupLine($"[dim]Note: Could not clean up temp directory: {ex.Message}[/]");
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/Commands/Update/UpdateVerb.cs` around lines 147 - 153, The
empty catch around Directory.Delete(tempDir, true) in UpdateVerb silently
swallows cleanup errors; change it to catch(Exception ex) and log the exception
at debug (or trace) level instead of doing nothing—use the UpdateVerb class's
existing logger instance (e.g., _logger or Logger) to call Debug/Trace with a
short message like "Failed to delete tempDir" and include ex to capture
permission/lock details so cleanup failures are visible without failing the
update.

67-79: Consider expanding supported platforms.

The explicit RID lookup table is a good improvement. However, win-arm64 and linux-arm64 are missing from the supported platforms. If release assets exist for these architectures, consider adding them:

 var ridToAsset = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
 {
     ["win-x64"]     = "arius-win-x64.exe",
+    ["win-arm64"]   = "arius-win-arm64.exe",
     ["osx-x64"]     = "arius-osx-x64",
     ["osx-arm64"]   = "arius-osx-arm64",
     ["linux-x64"]   = "arius-linux-x64",
+    ["linux-arm64"] = "arius-linux-arm64",
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/Commands/Update/UpdateVerb.cs` around lines 67 - 79, The
ridToAsset lookup in UpdateVerb.cs is missing mappings for arm64 RIDs; update
the ridToAsset Dictionary<string,string> (used to resolve assetName) to include
"win-arm64" (e.g., "arius-win-arm64.exe") and "linux-arm64" (e.g.,
"arius-linux-arm64") entries so those platforms are recognized by TryGetValue
and no longer hit the unsupported-platform branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Arius.Cli/Commands/Restore/RestoreVerb.cs`:
- Around line 146-149: The continuation using ContinueWith on mediator.Send(new
RestoreCommand(opts), ct) swallows exceptions; replace the ContinueWith pattern
with an awaited call to mediator.Send so exceptions propagate (match the
ArchiveVerb approach): await the Send call (use ConfigureAwait(false) if
applicable), assign its result to the existing result variable (result = await
mediator.Send(new RestoreCommand(opts), ct)...), and remove the ContinueWith
logic; if you need to preserve behavior of capturing and rethrowing, capture any
caught exception into a pipelineException variable and rethrow it after awaiting
the task, but prefer the direct await to ensure exceptions from mediator.Send
and RestoreCommand surface correctly.

In `@src/Arius.Cli/Commands/Update/UpdateVerb.cs`:
- Around line 106-137: The download routine that writes to tempFile after
http.GetAsync(downloadUrl, ...) in the Progress StartAsync block lacks integrity
verification; implement SHA256 checksum verification (or code signature
verification) immediately after download and before replacing the running
binary: fetch the expected checksum (e.g., from a .sha256 URL or release
metadata), compute the SHA256 of tempFile, compare and abort (and log an error)
on mismatch; if you cannot yet obtain a trusted checksum/source, at minimum add
a clear warning log entry where the download completes (after file is written
and before any install/replace step) indicating that integrity verification is
not performed.
- Around line 139-140: Environment.ProcessPath is being force-unwrapped into
currentExe and used in File.Move which can throw a NullReferenceException;
update the UpdateVerb code around the currentExe and File.Move(tempFile,
currentExe, true) usage to first check Environment.ProcessPath for null (assign
to currentExe only if non-null), and if null handle gracefully (e.g., log an
error via the same logger used in UpdateVerb and abort/return with a non-zero
exit or throw a clear exception) so you never call File.Move with a null path.

---

Duplicate comments:
In `@src/Arius.Cli/Commands/Restore/RestoreVerb.cs`:
- Around line 296-300: The failure message can be empty when the pipeline task
returns null; update the RestoreVerb error branch to provide a fallback error
string by using the local result variable (result) and its ErrorMessage if
present, otherwise a clear default like "Unknown error occurred" or the
task/exception message; specifically modify the conditional in RestoreVerb where
result is checked and the AnsiConsole.MarkupLine is called so it prints
result?.ErrorMessage ?? "Unknown error occurred" (or a similarly descriptive
default) to ensure users see a meaningful message when result is null.

In `@src/Arius.Cli/Commands/Update/UpdateVerb.cs`:
- Around line 139-145: Self-update on Windows fails because File.Move(tempFile,
currentExe, true) attempts to overwrite the running executable (currentExe)
which is locked; update the Windows branch inside UpdateVerb (around the code
using currentExe and tempFile) to instead create and launch a short updater
(e.g., a .cmd or small helper exe) that waits for the current process to exit,
moves tempFile to currentExe, restarts the new executable, and cleans up the
updater, then call Environment.Exit(0) from the main process; keep the existing
File.Move/UnixFileMode path for non-Windows (OperatingSystem.IsWindows() check)
so Unix behavior is unchanged.

---

Nitpick comments:
In `@src/Arius.Cli/Commands/Archive/ArchiveVerb.cs`:
- Around line 167-171: The current check in ArchiveVerb that logs "Archive
failed" uses {result?.ErrorMessage} which is empty when result is null; update
the failure logging in the ArchiveVerb command handler (the method that contains
the result null check) to include a clearer fallback message and any available
context (e.g., "result was null", operation name, input parameters or exception
info if available) so you don’t print a blank error; specifically, when result
is null print a descriptive message like "Archive failed: no result returned"
and, if possible, append result?.ErrorMessage and any caught exception details
to aid debugging.

In `@src/Arius.Cli/Commands/Update/UpdateVerb.cs`:
- Around line 147-153: The empty catch around Directory.Delete(tempDir, true) in
UpdateVerb silently swallows cleanup errors; change it to catch(Exception ex)
and log the exception at debug (or trace) level instead of doing nothing—use the
UpdateVerb class's existing logger instance (e.g., _logger or Logger) to call
Debug/Trace with a short message like "Failed to delete tempDir" and include ex
to capture permission/lock details so cleanup failures are visible without
failing the update.
- Around line 67-79: The ridToAsset lookup in UpdateVerb.cs is missing mappings
for arm64 RIDs; update the ridToAsset Dictionary<string,string> (used to resolve
assetName) to include "win-arm64" (e.g., "arius-win-arm64.exe") and
"linux-arm64" (e.g., "arius-linux-arm64") entries so those platforms are
recognized by TryGetValue and no longer hit the unsupported-platform branch.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6fa88c26-b863-4ff5-b976-22cae41dd59b

📥 Commits

Reviewing files that changed from the base of the PR and between 544a4ea and f38126e.

📒 Files selected for processing (4)
  • src/Arius.Cli/Commands/Archive/ArchiveProgressHandlers.cs
  • src/Arius.Cli/Commands/Archive/ArchiveVerb.cs
  • src/Arius.Cli/Commands/Restore/RestoreVerb.cs
  • src/Arius.Cli/Commands/Update/UpdateVerb.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Arius.Cli/Commands/Archive/ArchiveProgressHandlers.cs

Comment thread src/Arius.Cli/Commands/Restore/RestoreVerb.cs Outdated
Comment on lines +106 to +137
var tempDir = Path.Combine(Path.GetTempPath(), $"arius-update-{versionStr}");
var tempFile = Path.Combine(tempDir, assetName);
Directory.CreateDirectory(tempDir);

await AnsiConsole.Progress()
.AutoClear(false)
.Columns(
new TaskDescriptionColumn(),
new ProgressBarColumn(),
new PercentageColumn(),
new SpinnerColumn())
.StartAsync(async ctx =>
{
var task = ctx.AddTask("[green]Downloading update[/]");
using var response = await http.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead, ct);
response.EnsureSuccessStatusCode();
var totalBytes = response.Content.Headers.ContentLength ?? -1;
if (totalBytes > 0) task.MaxValue = totalBytes;

await using var stream = await response.Content.ReadAsStreamAsync(ct);
await using var file = File.Create(tempFile);
var buffer = new byte[81920];
long downloaded = 0;
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer, ct)) > 0)
{
await file.WriteAsync(buffer.AsMemory(0, bytesRead), ct);
downloaded += bytesRead;
if (totalBytes > 0) task.Value = downloaded;
}
task.Value = task.MaxValue;
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

No integrity verification of downloaded binary.

The download completes without verifying the downloaded file's integrity. A corrupted download, MITM attack, or compromised CDN could result in an invalid or malicious binary being installed. Consider:

  1. Verifying a SHA256 checksum against a value published in the release (e.g., from a .sha256 file or release body)
  2. Or, verifying a code signature after download

At minimum, log a warning that integrity verification is not performed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/Commands/Update/UpdateVerb.cs` around lines 106 - 137, The
download routine that writes to tempFile after http.GetAsync(downloadUrl, ...)
in the Progress StartAsync block lacks integrity verification; implement SHA256
checksum verification (or code signature verification) immediately after
download and before replacing the running binary: fetch the expected checksum
(e.g., from a .sha256 URL or release metadata), compute the SHA256 of tempFile,
compare and abort (and log an error) on mismatch; if you cannot yet obtain a
trusted checksum/source, at minimum add a clear warning log entry where the
download completes (after file is written and before any install/replace step)
indicating that integrity verification is not performed.

Comment on lines +139 to +140
var currentExe = Environment.ProcessPath!;
File.Move(tempFile, currentExe, true);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add null check for Environment.ProcessPath.

Environment.ProcessPath can return null in certain edge cases (e.g., some single-file deployment scenarios or when the process info is unavailable). The null-forgiving operator (!) could lead to a NullReferenceException.

-var currentExe = Environment.ProcessPath!;
+var currentExe = Environment.ProcessPath;
+if (string.IsNullOrEmpty(currentExe))
+{
+    AnsiConsole.MarkupLine("[red]Could not determine current executable path.[/]");
+    return 1;
+}
 File.Move(tempFile, currentExe, true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Arius.Cli/Commands/Update/UpdateVerb.cs` around lines 139 - 140,
Environment.ProcessPath is being force-unwrapped into currentExe and used in
File.Move which can throw a NullReferenceException; update the UpdateVerb code
around the currentExe and File.Move(tempFile, currentExe, true) usage to first
check Environment.ProcessPath for null (assign to currentExe only if non-null),
and if null handle gracefully (e.g., log an error via the same logger used in
UpdateVerb and abort/return with a non-zero exit or throw a clear exception) so
you never call File.Move with a null path.

@woutervanranst woutervanranst merged commit d8d0acc into master Mar 31, 2026
6 of 8 checks passed
@woutervanranst woutervanranst deleted the petal-glade branch March 31, 2026 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant