A LocalSystem Windows service can't see the logged-in user's Claude CLI authentication, so the worker now runs as the current user via a hidden per-user logon Scheduled Task with restart-on-failure. - Worker is WinExe (no console window) with a Serilog rolling file sink and a single-instance mutex so the logon task, app ensure-running, and Restart button can't fight over the SignalR port. - Installer replaces the service steps (register/start/stop) with autostart task steps, migrates the legacy ClaudeDoWorker service away on update, and removes the task on uninstall. ServicePage drops the service-account UI. - UI gains a WorkerLocator; the app ensures the worker is running at startup and the Restart button kills+relaunches this install's worker process. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
49 lines
1.7 KiB
C#
49 lines
1.7 KiB
C#
using System.Diagnostics;
|
|
using System.IO;
|
|
using ClaudeDo.Installer.Core;
|
|
|
|
namespace ClaudeDo.Installer.Steps;
|
|
|
|
public sealed class StopWorkerStep : IInstallStep
|
|
{
|
|
public const string TaskName = "ClaudeDoWorker";
|
|
public const string ProcessName = "ClaudeDo.Worker";
|
|
|
|
public string Name => "Stop Worker";
|
|
|
|
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
|
{
|
|
progress.Report("Stopping worker task (if running)...");
|
|
await ProcessRunner.RunAsync("schtasks.exe", $"/End /TN \"{TaskName}\"", null, progress, ct);
|
|
|
|
progress.Report("Stopping worker process (if running)...");
|
|
var installDir = ctx.InstallDirectory;
|
|
foreach (var p in Process.GetProcessesByName(ProcessName))
|
|
{
|
|
try
|
|
{
|
|
var path = p.MainModule?.FileName;
|
|
if (path is not null && !IsUnder(path, installDir)) continue;
|
|
p.Kill(entireProcessTree: true);
|
|
p.WaitForExit(10000);
|
|
}
|
|
catch { /* process may have exited or be inaccessible */ }
|
|
finally { p.Dispose(); }
|
|
}
|
|
await Task.CompletedTask;
|
|
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; }
|
|
}
|
|
}
|