feat(worker): run worker as per-user logon task instead of Windows service
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>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
1e5b3a6c3e
commit
26c4e5771b
@@ -0,0 +1,59 @@
|
||||
using System.IO;
|
||||
using System.Security.Principal;
|
||||
using ClaudeDo.Installer.Core;
|
||||
|
||||
namespace ClaudeDo.Installer.Steps;
|
||||
|
||||
public sealed class RegisterAutostartStep : IInstallStep
|
||||
{
|
||||
public const string TaskName = "ClaudeDoWorker";
|
||||
private const string LegacyServiceName = "ClaudeDoWorker";
|
||||
|
||||
public string Name => "Register Autostart";
|
||||
|
||||
public async Task<StepResult> ExecuteAsync(InstallContext ctx, IProgress<string> progress, CancellationToken ct)
|
||||
{
|
||||
var workerExe = Path.Combine(ctx.InstallDirectory, "worker", "ClaudeDo.Worker.exe");
|
||||
if (!File.Exists(workerExe))
|
||||
return StepResult.Fail($"Worker executable not found: {workerExe}");
|
||||
|
||||
// 1) Migrate away the legacy Windows service if present.
|
||||
progress.Report("Checking for legacy worker service...");
|
||||
var (queryExit, _) = await ProcessRunner.RunAsync("sc.exe", $"query {LegacyServiceName}", null, progress, ct);
|
||||
if (queryExit == 0)
|
||||
{
|
||||
progress.Report("Removing legacy worker service...");
|
||||
await ProcessRunner.RunAsync("sc.exe", $"stop {LegacyServiceName}", null, progress, ct);
|
||||
await ProcessRunner.RunAsync("sc.exe", $"delete {LegacyServiceName}", null, progress, ct);
|
||||
for (var i = 0; i < 30; i++)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
var (q, _) = await ProcessRunner.RunAsync("sc.exe", $"query {LegacyServiceName}", null, progress, ct);
|
||||
if (q != 0) break;
|
||||
await Task.Delay(1000, ct);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Register (or replace) the per-user logon task.
|
||||
var userId = WindowsIdentity.GetCurrent().Name;
|
||||
var minutes = Math.Max(1, ctx.RestartDelayMs / 60000);
|
||||
var xml = ScheduledTaskXml.Build(userId, workerExe, minutes);
|
||||
|
||||
var xmlPath = Path.Combine(Path.GetTempPath(), $"ClaudeDoWorker-{Guid.NewGuid():N}.xml");
|
||||
await File.WriteAllTextAsync(xmlPath, xml, new System.Text.UnicodeEncoding(false, true), ct);
|
||||
try
|
||||
{
|
||||
progress.Report("Registering logon task...");
|
||||
var (exit, output) = await ProcessRunner.RunAsync(
|
||||
"schtasks.exe", $"/Create /TN \"{TaskName}\" /XML \"{xmlPath}\" /F", null, progress, ct);
|
||||
if (exit != 0)
|
||||
return StepResult.Fail($"schtasks /Create failed (exit {exit}): {output}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
try { File.Delete(xmlPath); } catch { /* best effort */ }
|
||||
}
|
||||
|
||||
return StepResult.Ok();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user