Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ Common settings include font family, theme colors, and grid font scale. The grid
* **App won't open (Gatekeeper)**: run `xattr -dr com.apple.quarantine Architect.app` after extracting the release.
* **Font not found**: ensure the font is installed and set `font.family` in `config.toml`. The app falls back to `SFNSMono` on macOS.
* **Reset configuration**: delete `~/.config/architect/config.toml` and `~/.config/architect/persistence.toml`.
* **Crash after closing a terminal**: update to the latest build; older builds could crash after terminal close events on macOS.
* **Known limitations**: emoji fallback is macOS-only; keybindings are currently fixed.

## Documentation
Expand Down
35 changes: 20 additions & 15 deletions src/session/state.zig
Original file line number Diff line number Diff line change
Expand Up @@ -156,9 +156,6 @@ pub const SessionState = struct {
var process = try xev.Process.init(shell.child_pid);
errdefer process.deinit();

if (self.process_wait_ctx) |ctx| {
self.allocator.destroy(ctx);
}
const wait_ctx = try self.allocator.create(WaitContext);
errdefer self.allocator.destroy(wait_ctx);
wait_ctx.* = .{
Expand Down Expand Up @@ -211,16 +208,16 @@ pub const SessionState = struct {
self.cwd_basename = null;
}

// Clean up process watcher. Deinit cancels the watch, so the callback won't fire.
// Free the context immediately rather than relying on the callback to clean up.
if (self.process_watcher) |*watcher| {
watcher.deinit();
self.process_watcher = null;
}
if (self.process_wait_ctx) |ctx| {
allocator.destroy(ctx);
self.process_wait_ctx = null;
if (ctx.completion.state() == .dead) {
allocator.destroy(ctx);
}
}
self.process_wait_ctx = null;
// Wrap intentionally: process_generation is a bounded counter and may overflow.
self.process_generation +%= 1;

Expand Down Expand Up @@ -270,26 +267,34 @@ pub const SessionState = struct {
const self = ctx.session;

// Ignore completions from stale watchers (after despawn/restart) or mismatched PID.
const shell = self.shell orelse return .disarm;
const shell = self.shell orelse {
const is_current = self.process_wait_ctx == ctx;
self.allocator.destroy(ctx);
if (is_current) self.process_wait_ctx = null;
return .disarm;
};
if (ctx.generation != self.process_generation or ctx.pid != shell.child_pid) {
const is_current = self.process_wait_ctx == ctx;
self.allocator.destroy(ctx);
if (self.process_wait_ctx == ctx) self.process_wait_ctx = null;
if (is_current) self.process_wait_ctx = null;
return .disarm;
}

const exit_code = r catch |err| {
log.err("process wait error for session {d}: {}", .{ self.id, err });
const is_current = self.process_wait_ctx == ctx;
self.allocator.destroy(ctx);
if (self.process_wait_ctx == ctx) self.process_wait_ctx = null;
if (is_current) self.process_wait_ctx = null;
return .disarm;
};

self.dead = true;
self.markDirty();
log.info("session {d} process exited with code {d}", .{ self.id, exit_code });

const is_current = self.process_wait_ctx == ctx;
self.allocator.destroy(ctx);
if (self.process_wait_ctx == ctx) self.process_wait_ctx = null;
if (is_current) self.process_wait_ctx = null;

return .disarm;
}
Expand Down Expand Up @@ -327,16 +332,16 @@ pub const SessionState = struct {
fn resetForRespawn(self: *SessionState) void {
self.clearTerminalSelection();
self.pending_write.clearAndFree(self.allocator);
// Clean up process watcher. Deinit cancels the watch and frees the context immediately.
// The generation bump ensures we ignore any stale exit events.
if (self.process_watcher) |*watcher| {
watcher.deinit();
self.process_watcher = null;
}
if (self.process_wait_ctx) |ctx| {
self.allocator.destroy(ctx);
self.process_wait_ctx = null;
if (ctx.completion.state() == .dead) {
self.allocator.destroy(ctx);
}
}
self.process_wait_ctx = null;
// Wrap intentionally: generation just invalidates prior watchers.
self.process_generation +%= 1;
if (self.spawned) {
Expand Down