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 ExecuteAsync(InstallContext ctx, IProgress 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; } } }