Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 152 additions & 12 deletions src/installer/orchestrator.zig
Original file line number Diff line number Diff line change
Expand Up @@ -281,27 +281,51 @@ pub fn install(
defer launch.deinit();
const effective_port = launch.effectiveHealthPort(runtime_port);

// 6. Register in state.json
s.addInstance(opts.component, opts.instance_name, .{
.version = version,
.auto_start = true,
.launch_mode = launch_command,
.verbose = false,
}) catch return error.StateError;
s.save() catch return error.StateError;

// 7. Start process via Manager
mgr.startInstance(
persistAndStartInstance(
s,
opts.component,
opts.instance_name,
version,
launch_command,
struct {
fn call(
ctx: *anyopaque,
component: []const u8,
name: []const u8,
binary_path: []const u8,
launch_args: []const []const u8,
port_arg: u16,
health: []const u8,
working_dir: []const u8,
config_path: []const u8,
primary_command: []const u8,
) anyerror!void {
const manager: *manager_mod.Manager = @ptrCast(@alignCast(ctx));
return manager.startInstance(
component,
name,
binary_path,
launch_args,
port_arg,
health,
working_dir,
config_path,
primary_command,
);
}
}.call,
@ptrCast(mgr),
bin_path,
launch.argv,
effective_port,
health_endpoint,
inst_dir,
"",
launch.primary_command,
) catch return error.StartFailed;
) catch |err| switch (err) {
error.StateError => return error.StateError,
error.StartFailed => return error.StartFailed,
};

return .{
.version = version,
Expand Down Expand Up @@ -342,6 +366,62 @@ fn resolveConfiguredPort(
return findNextAvailablePort(allocator, default_port, paths, state);
}

fn persistAndStartInstance(
s: *state_mod.State,
component: []const u8,
name: []const u8,
version: []const u8,
launch_mode: []const u8,
startFn: *const fn (
ctx: *anyopaque,
component: []const u8,
name: []const u8,
binary_path: []const u8,
launch_args: []const []const u8,
port: u16,
health_endpoint: []const u8,
working_dir: []const u8,
config_path: []const u8,
primary_command: []const u8,
) anyerror!void,
ctx: *anyopaque,
binary_path: []const u8,
launch_args: []const []const u8,
port: u16,
health_endpoint: []const u8,
working_dir: []const u8,
config_path: []const u8,
primary_command: []const u8,
) error{ StateError, StartFailed }!void {
s.addInstance(component, name, .{
.version = version,
.auto_start = true,
.launch_mode = launch_mode,
.verbose = false,
}) catch return error.StateError;
s.save() catch {
_ = s.removeInstance(component, name);
return error.StateError;
};

startFn(
ctx,
component,
name,
binary_path,
launch_args,
port,
health_endpoint,
working_dir,
config_path,
primary_command,
) catch {
_ = s.removeInstance(component, name);
s.save() catch return error.StateError;
return error.StartFailed;
};
}

fn findNextAvailablePort(
allocator: std.mem.Allocator,
start: u16,
Expand Down Expand Up @@ -992,3 +1072,63 @@ test "injectHomeField adds home to JSON object" {
try std.testing.expect(std.mem.indexOf(u8, result, "\"home\":\"/tmp/inst\"") != null);
try std.testing.expect(std.mem.indexOf(u8, result, "\"provider\":\"openrouter\"") != null);
}

test "persistAndStartInstance rolls back state entry when start fails" {
const allocator = std.testing.allocator;
var tmp_dir = std.testing.tmpDir(.{});
defer tmp_dir.cleanup();

const root = try std_compat.fs.Dir.wrap(tmp_dir.dir).realpathAlloc(allocator, ".");
defer allocator.free(root);

var p = try paths_mod.Paths.init(allocator, root);
defer p.deinit(allocator);

const state_path = try p.state(allocator);
defer allocator.free(state_path);
var s = state_mod.State.init(allocator, state_path);
defer s.deinit();

const instances_dir = try std.fs.path.join(allocator, &.{ root, "instances" });
defer allocator.free(instances_dir);
try std_compat.fs.makeDirAbsolute(instances_dir);

const start_result = persistAndStartInstance(
&s,
"nullclaw",
"demo",
"1.0.0",
"gateway",
struct {
fn call(
_: *anyopaque,
_: []const u8,
_: []const u8,
_: []const u8,
_: []const []const u8,
_: u16,
_: []const u8,
_: []const u8,
_: []const u8,
_: []const u8,
) anyerror!void {
return error.SpawnFailed;
}
}.call,
undefined,
"/tmp/fake-binary",
&.{"--help"},
0,
"/health",
root,
"",
"gateway",
);
try std.testing.expectError(error.StartFailed, start_result);

try std.testing.expect(s.getInstance("nullclaw", "demo") == null);

var reloaded = try state_mod.State.load(allocator, state_path);
defer reloaded.deinit();
try std.testing.expect(reloaded.getInstance("nullclaw", "demo") == null);
}