feat(installer): harden database init and service setup steps

- InitDatabaseStep: create DbPath parent directory so custom paths work
- RegisterServiceStep: pass obj= argument so ServiceAccount is honoured
- StartServiceStep: poll for RUNNING state so downstream steps don't race
This commit is contained in:
mika kuns
2026-04-23 13:07:16 +02:00
parent cc01871407
commit 31218fc205
3 changed files with 30 additions and 6 deletions

View File

@@ -1,3 +1,4 @@
using System.IO;
using ClaudeDo.Data; using ClaudeDo.Data;
using ClaudeDo.Installer.Core; using ClaudeDo.Installer.Core;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -15,6 +16,10 @@ public sealed class InitDatabaseStep : IInstallStep
var expandedPath = Paths.Expand(ctx.DbPath); var expandedPath = Paths.Expand(ctx.DbPath);
progress.Report($"Initializing database at {expandedPath}"); progress.Report($"Initializing database at {expandedPath}");
var parent = Path.GetDirectoryName(expandedPath);
if (!string.IsNullOrEmpty(parent))
Directory.CreateDirectory(parent);
var options = new DbContextOptionsBuilder<ClaudeDoDbContext>() var options = new DbContextOptionsBuilder<ClaudeDoDbContext>()
.UseSqlite($"Data Source={expandedPath}") .UseSqlite($"Data Source={expandedPath}")
.Options; .Options;

View File

@@ -43,13 +43,21 @@ public sealed class RegisterServiceStep : IInstallStep
// Create service // Create service
var startType = ctx.AutoStart ? "auto" : "demand"; var startType = ctx.AutoStart ? "auto" : "demand";
var createArgs = $"create {ServiceName} binPath= \"{workerExe}\" start= {startType}";
if (ctx.ServiceAccount == "CurrentUser") if (ctx.ServiceAccount == "CurrentUser")
return StepResult.Fail( return StepResult.Fail(
"Service cannot run as Current User without a password. " + "Service cannot run as Current User without a password. " +
"Select 'Local System' or extend ServicePage to capture 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..."); progress.Report("Creating service...");
var (exitCode, output) = await RunSc(createArgs, ctx, progress, ct); var (exitCode, output) = await RunSc(createArgs, ctx, progress, ct);
if (exitCode == 1072) if (exitCode == 1072)

View File

@@ -13,15 +13,26 @@ public sealed class StartServiceStep : IInstallStep
progress.Report($"Starting {ServiceName}..."); progress.Report($"Starting {ServiceName}...");
var (exit, _) = await ProcessRunner.RunAsync("sc.exe", $"start {ServiceName}", null, progress, ct); 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)
// Exit 1056 = ERROR_SERVICE_ALREADY_RUNNING — that's fine too.
if (exit == 1056)
{
progress.Report("Service was already running.");
return StepResult.Ok();
}
return StepResult.Fail($"sc.exe start failed with exit code {exit}"); return StepResult.Fail($"sc.exe start failed with exit code {exit}");
if (exit == 1056)
progress.Report("Service was already running.");
// 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("Service did not reach RUNNING state within 30 seconds.");
} }
} }