diff --git a/src/ClaudeDo.Installer/Steps/InitDatabaseStep.cs b/src/ClaudeDo.Installer/Steps/InitDatabaseStep.cs index 007b36c..9c4ed98 100644 --- a/src/ClaudeDo.Installer/Steps/InitDatabaseStep.cs +++ b/src/ClaudeDo.Installer/Steps/InitDatabaseStep.cs @@ -1,3 +1,4 @@ +using System.IO; using ClaudeDo.Data; using ClaudeDo.Installer.Core; using Microsoft.EntityFrameworkCore; @@ -15,6 +16,10 @@ public sealed class InitDatabaseStep : IInstallStep var expandedPath = Paths.Expand(ctx.DbPath); progress.Report($"Initializing database at {expandedPath}"); + var parent = Path.GetDirectoryName(expandedPath); + if (!string.IsNullOrEmpty(parent)) + Directory.CreateDirectory(parent); + var options = new DbContextOptionsBuilder() .UseSqlite($"Data Source={expandedPath}") .Options; diff --git a/src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs b/src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs index 70baa28..6710925 100644 --- a/src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs +++ b/src/ClaudeDo.Installer/Steps/RegisterServiceStep.cs @@ -43,13 +43,21 @@ public sealed class RegisterServiceStep : IInstallStep // Create service var startType = ctx.AutoStart ? "auto" : "demand"; - var createArgs = $"create {ServiceName} binPath= \"{workerExe}\" start= {startType}"; 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) diff --git a/src/ClaudeDo.Installer/Steps/StartServiceStep.cs b/src/ClaudeDo.Installer/Steps/StartServiceStep.cs index 0b97ce7..3f59ed6 100644 --- a/src/ClaudeDo.Installer/Steps/StartServiceStep.cs +++ b/src/ClaudeDo.Installer/Steps/StartServiceStep.cs @@ -13,15 +13,26 @@ public sealed class StartServiceStep : IInstallStep progress.Report($"Starting {ServiceName}..."); var (exit, _) = await ProcessRunner.RunAsync("sc.exe", $"start {ServiceName}", null, progress, ct); - if (exit == 0) return StepResult.Ok(); + // 1056 = ERROR_SERVICE_ALREADY_RUNNING — fine, fall through to the readiness poll. + if (exit != 0 && exit != 1056) + return StepResult.Fail($"sc.exe start failed with exit code {exit}"); - // Exit 1056 = ERROR_SERVICE_ALREADY_RUNNING — that's fine too. if (exit == 1056) - { progress.Report("Service was already running."); - return StepResult.Ok(); + + // sc.exe start returns as soon as SCM accepts the command. Poll until the + // service actually reports RUNNING so downstream steps and SignalR clients + // don't race the worker's startup. + progress.Report("Waiting for service to reach RUNNING state..."); + for (var i = 0; i < 30; i++) + { + ct.ThrowIfCancellationRequested(); + var (q, output) = await ProcessRunner.RunAsync("sc.exe", $"query {ServiceName}", null, progress, ct); + if (q == 0 && output.Contains("RUNNING", StringComparison.OrdinalIgnoreCase)) + return StepResult.Ok(); + await Task.Delay(1000, ct); } - return StepResult.Fail($"sc.exe start failed with exit code {exit}"); + return StepResult.Fail("Service did not reach RUNNING state within 30 seconds."); } }