using System.IO; using ClaudeDo.Installer.Core; namespace ClaudeDo.Installer.Steps; public sealed class RegisterServiceStep : IInstallStep { private const string ServiceName = "ClaudeDoWorker"; public string Name => "Register Windows Service"; public async Task ExecuteAsync(InstallContext ctx, IProgress 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}"); // Stop existing service (ignore errors — may not exist) progress.Report("Stopping existing service (if any)..."); await RunSc($"stop {ServiceName}", ctx, progress, ct, ignoreErrors: true); // Delete existing service (ignore errors) progress.Report("Removing existing service registration (if any)..."); await RunSc($"delete {ServiceName}", ctx, progress, ct, ignoreErrors: true); // Wait for the service to actually disappear from SCM. `sc delete` returns // immediately but the service stays "marked for deletion" until every open // handle (services.msc, Task Manager, a prior sc query process) is closed. // Poll up to 30s — then fail with actionable guidance if it's still there. progress.Report("Waiting for prior service registration to clear..."); for (var i = 0; i < 30; i++) { ct.ThrowIfCancellationRequested(); var (queryExit, _) = await RunSc($"query {ServiceName}", ctx, progress, ct, ignoreErrors: true); if (queryExit != 0) break; // service no longer registered — good if (i == 29) return StepResult.Fail( $"Service '{ServiceName}' is marked for deletion but hasn't cleared after 30s. " + "Close any open Services console (services.msc), Task Manager Services tab, or " + "Event Viewer showing the service, then retry. A reboot will also clear it."); await Task.Delay(1000, ct); } // Create service var startType = ctx.AutoStart ? "auto" : "demand"; if (ctx.ServiceAccount == "CurrentUser") return StepResult.Fail( "Service cannot run as Current User without a password. " + "Select 'Local System' or extend ServicePage to capture a password."); var objArg = ctx.ServiceAccount switch { "LocalSystem" => " obj= LocalSystem", "NetworkService" => " obj= \"NT AUTHORITY\\NetworkService\"", "LocalService" => " obj= \"NT AUTHORITY\\LocalService\"", _ => "", }; var createArgs = $"create {ServiceName} binPath= \"{workerExe}\" start= {startType}{objArg}"; progress.Report("Creating service..."); var (exitCode, output) = await RunSc(createArgs, ctx, progress, ct); if (exitCode == 1072) return StepResult.Fail( $"Service '{ServiceName}' is still marked for deletion. " + "Close services.msc / Task Manager / Event Viewer and retry, or reboot."); if (exitCode != 0) return StepResult.Fail($"sc.exe create failed (exit {exitCode}): {output}"); // Configure restart policy var delay = ctx.RestartDelayMs; var failureArgs = $"failure {ServiceName} reset= 86400 actions= restart/{delay}/restart/{delay}/restart/{delay}"; progress.Report("Configuring restart policy..."); var (failExit, failOutput) = await RunSc(failureArgs, ctx, progress, ct); if (failExit != 0) progress.Report($"Warning: failed to set restart policy (exit {failExit})"); return StepResult.Ok(); } private static async Task<(int ExitCode, string Output)> RunSc( string arguments, InstallContext ctx, IProgress progress, CancellationToken ct, bool ignoreErrors = false) { var result = await ProcessRunner.RunAsync("sc.exe", arguments, null, progress, ct); return result; } }