Files
ClaudeDo/src/ClaudeDo.Installer/Steps/StopWorkerStep.cs
T
mika kuns 95918414a0
Changelog / changelog (push) Successful in 1s
Release / release (push) Successful in 38s
fix(installer): retry stashing app/worker through transient file locks on update
The update pipeline moved app/ and worker/ to .bak before extraction with no
retry, so a just-killed worker whose file handles had not been released yet
(WaitForExit returns before the OS flushes them) caused Directory.Move to throw
ERROR_SHARING_VIOLATION, stopping the pipeline on a cryptic error screen.

- DownloadAndExtractStep: stash/rollback moves+deletes now retry through
  transient IO/access errors (~5s); a persistent lock returns an actionable
  message instead of the raw error.
- StopWorkerStep: reading MainModule no longer skips Kill on failure, and a
  short settle follows the kill so handles are released before extraction.
2026-07-24 14:49:15 +02:00

64 lines
2.6 KiB
C#

using System.Diagnostics;
using System.IO;
using ClaudeDo.Installer.Core;
namespace ClaudeDo.Installer.Steps;
public sealed class StopWorkerStep : IInstallStep
{
public const string LegacyTaskName = "ClaudeDoWorker";
// Both must be stopped before the install dir is touched: a running app/worker
// exe locks its directory, so Directory.Move during extraction would otherwise
// fail with "Access to the path '...\app' is denied".
private static readonly string[] ProcessNames = { "ClaudeDo.Worker", "ClaudeDo.App" };
public string Name => "Stop Worker";
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
{
progress.Report("Stopping ClaudeDo processes (if running)...");
var installDir = ctx.InstallDirectory;
var killedAny = false;
foreach (var name in ProcessNames)
{
foreach (var p in Process.GetProcessesByName(name))
{
try
{
// Scope to THIS install when the module path is readable; if it
// can't be read (access race / exiting process), fall through and
// kill anyway — a survivor would lock the install dir during
// extraction. Reading MainModule must not skip the Kill.
string? path = null;
try { path = p.MainModule?.FileName; } catch { /* unreadable — kill anyway */ }
if (path is not null && !IsUnder(path, installDir)) continue;
p.Kill(entireProcessTree: true);
p.WaitForExit(10000);
killedAny = true;
}
catch { /* process may have exited or be inaccessible */ }
finally { p.Dispose(); }
}
}
// WaitForExit returns before the OS releases the process's file handles.
// Give it a moment so DownloadAndExtractStep's Directory.Move doesn't race
// a still-open handle. (That step also retries, this just avoids the churn.)
if (killedAny) await Task.Delay(1500, ct);
return StepResult.Ok();
}
private static bool IsUnder(string filePath, string dir)
{
try
{
if (string.IsNullOrWhiteSpace(dir)) return true; // can't scope — be permissive
var full = Path.GetFullPath(filePath);
var root = Path.GetFullPath(dir).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
return full.StartsWith(root, StringComparison.OrdinalIgnoreCase);
}
catch { return false; }
}
}