Fix some claude session state.
This commit is contained in:
1 parent
b5af181a4d
commit
6f4492e3cb
3 files changed
+108
-8
No files matched your search
@@ -180,17 +180,21 @@ pub fn build(b: *std.Build) void {
|
|||||||
palette_tests.root_module.addImport("ghostty-vt", ghostty.module("ghostty-vt"));
|
palette_tests.root_module.addImport("ghostty-vt", ghostty.module("ghostty-vt"));
|
||||||
test_step.dependOn(&b.addRunArtifact(palette_tests).step);
|
test_step.dependOn(&b.addRunArtifact(palette_tests).step);
|
||||||
|
|
||||||
// herdr-core's frame format is its own root: the bytes of the wire
|
// herdr-core's client is its own root: the frame format (the bytes of the
|
||||||
// protocol, testable with no core to talk to. The connection on top of it
|
// wire protocol) and the pure rules about what a terminal's environment
|
||||||
|
// keeps, both testable with no core to talk to. The connection itself
|
||||||
// wants a socket and a main loop, and is checked by running it.
|
// wants a socket and a main loop, and is checked by running it.
|
||||||
const hc_frame_tests = b.addTest(.{
|
const hc_tests = b.addTest(.{
|
||||||
.root_module = b.createModule(.{
|
.root_module = b.createModule(.{
|
||||||
.root_source_file = b.path("src/hc/frame.zig"),
|
.root_source_file = b.path("src/hc.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize,
|
||||||
|
.link_libc = true,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
test_step.dependOn(&b.addRunArtifact(hc_frame_tests).step);
|
hc_tests.root_module.addImport("glib", gobject.module("glib2"));
|
||||||
|
hc_tests.root_module.addImport("glibunix", gobject.module("glibunix2"));
|
||||||
|
test_step.dependOn(&b.addRunArtifact(hc_tests).step);
|
||||||
|
|
||||||
// And a root for the settings file itself. `Settings.zig` reaches GLib for
|
// And a root for the settings file itself. `Settings.zig` reaches GLib for
|
||||||
// where the file lives, but reading and writing its *contents* is pure
|
// where the file lives, but reading and writing its *contents* is pure
|
||||||
|
|||||||
+2
-1
@@ -678,7 +678,8 @@ fn reattach(self: *Session, address: []const u8) !Remote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The environment a herdr-core shell is started with: this process's, as the
|
/// The environment a herdr-core shell is started with: this process's, as the
|
||||||
/// local pty would hand on, on top of the core's own.
|
/// local pty would hand on — and only that, since `hc.run` clears whatever the
|
||||||
|
/// core's own environment would otherwise add.
|
||||||
///
|
///
|
||||||
/// Passed whole rather than left to the core, whose environment is whatever
|
/// Passed whole rather than left to the core, whose environment is whatever
|
||||||
/// the session that first started it had — not necessarily the one this app
|
/// the session that first started it had — not necessarily the one this app
|
||||||
|
|||||||
+97
-2
@@ -152,7 +152,8 @@ pub const Attached = struct {
|
|||||||
pub const RunOptions = struct {
|
pub const RunOptions = struct {
|
||||||
argv: []const []const u8,
|
argv: []const []const u8,
|
||||||
cwd: ?[]const u8 = null,
|
cwd: ?[]const u8 = null,
|
||||||
/// `NAME=value`, set on top of the core's own environment.
|
/// `NAME=value`: the program's whole environment. Not merely added to the
|
||||||
|
/// core's — see `run`.
|
||||||
env: []const []const u8 = &.{},
|
env: []const []const u8 = &.{},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -174,6 +175,9 @@ const Core = struct {
|
|||||||
|
|
||||||
next_id: u64 = 1,
|
next_id: u64 = 1,
|
||||||
|
|
||||||
|
/// The core's process, asked for once per connection — see `coreEnviron`.
|
||||||
|
pid: ?i32 = null,
|
||||||
|
|
||||||
streams: std.AutoHashMapUnmanaged(u32, Stream) = .empty,
|
streams: std.AutoHashMapUnmanaged(u32, Stream) = .empty,
|
||||||
|
|
||||||
/// Set while `dispatch` runs, which must not nest.
|
/// Set while `dispatch` runs, which must not nest.
|
||||||
@@ -651,13 +655,40 @@ fn writeAddress(address: []const u8) Error![]u8 {
|
|||||||
/// is forgotten straight away instead of piling up in `hc ls --all`. It has
|
/// is forgotten straight away instead of piling up in `hc ls --all`. It has
|
||||||
/// no parent: a pane is nobody's child, and with none the core types no notes
|
/// no parent: a pane is nobody's child, and with none the core types no notes
|
||||||
/// about it into anything.
|
/// about it into anything.
|
||||||
|
///
|
||||||
|
/// The program gets exactly `opts.env`, the way a child of this process would.
|
||||||
|
/// The core only *adds* what it is given to its own environment, and its own is
|
||||||
|
/// whatever the shell that happened to start it had: start it from a Claude
|
||||||
|
/// Code session and every terminal it runs inherits that session's markers
|
||||||
|
/// (and so every `claude` in them believes it is a subagent), start it from a
|
||||||
|
/// nested compositor and they all inherit its `SWAYSOCK`. So the program is
|
||||||
|
/// started through `env -u`, unsetting each variable the core has and `opts`
|
||||||
|
/// does not — names only, so no value of the core's ends up in `hc ls`.
|
||||||
pub fn run(alloc: std.mem.Allocator, opts: RunOptions) Error!Started {
|
pub fn run(alloc: std.mem.Allocator, opts: RunOptions) Error!Started {
|
||||||
const self = connection() orelse return error.Unavailable;
|
const self = connection() orelse return error.Unavailable;
|
||||||
|
|
||||||
|
const environ = coreEnviron(self);
|
||||||
|
defer if (environ) |e| glib.free(e.ptr);
|
||||||
|
|
||||||
|
var argv: std.ArrayListUnmanaged([]const u8) = .empty;
|
||||||
|
defer argv.deinit(gpa);
|
||||||
|
if (environ) |e| {
|
||||||
|
var it = std.mem.splitScalar(u8, e, 0);
|
||||||
|
while (it.next()) |entry| {
|
||||||
|
const eq = std.mem.indexOfScalar(u8, entry, '=') orelse continue;
|
||||||
|
const name = entry[0..eq];
|
||||||
|
if (name.len == 0 or coreOwned(name) or setIn(name, opts.env)) continue;
|
||||||
|
try argv.appendSlice(gpa, &.{ "-u", name });
|
||||||
|
}
|
||||||
|
if (argv.items.len > 0) try argv.insert(gpa, 0, envBinary());
|
||||||
|
}
|
||||||
|
try argv.appendSlice(gpa, opts.argv);
|
||||||
|
|
||||||
var p: Json = undefined;
|
var p: Json = undefined;
|
||||||
p.init();
|
p.init();
|
||||||
defer p.deinit();
|
defer p.deinit();
|
||||||
writeRunParams(&p.j, opts) catch return error.OutOfMemory;
|
writeRunParams(&p.j, .{ .argv = argv.items, .cwd = opts.cwd, .env = opts.env }) catch
|
||||||
|
return error.OutOfMemory;
|
||||||
|
|
||||||
const reply = try call(self, "terminal.run", p.written());
|
const reply = try call(self, "terminal.run", p.written());
|
||||||
defer reply.deinit();
|
defer reply.deinit();
|
||||||
@@ -669,6 +700,55 @@ pub fn run(alloc: std.mem.Allocator, opts: RunOptions) Error!Started {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The core's own environment, NUL-separated, to be freed with `glib.free`.
|
||||||
|
/// Null when it can't be read, which leaves programs with the core's leftovers
|
||||||
|
/// as before rather than failing to start them.
|
||||||
|
///
|
||||||
|
/// Read out of procfs because the API has no call for it. The core always runs
|
||||||
|
/// on this machine here — nothing in this app starts terminals `on` another.
|
||||||
|
fn coreEnviron(self: *Core) ?[]u8 {
|
||||||
|
const pid = self.pid orelse blk: {
|
||||||
|
const reply = call(self, "server.status", "{}") catch return null;
|
||||||
|
defer reply.deinit();
|
||||||
|
const pid: i32 = std.math.cast(i32, intField(result(reply), "pid") orelse return null) orelse
|
||||||
|
return null;
|
||||||
|
self.pid = pid;
|
||||||
|
break :blk pid;
|
||||||
|
};
|
||||||
|
|
||||||
|
var path_buf: [64]u8 = undefined;
|
||||||
|
const path = std.fmt.bufPrintSentinel(&path_buf, "/proc/{d}/environ", .{pid}, 0) catch return null;
|
||||||
|
var contents: [*]u8 = undefined;
|
||||||
|
var length: usize = 0;
|
||||||
|
if (glib.fileGetContents(path.ptr, &contents, &length, null) == 0) {
|
||||||
|
std.log.warn("herdr-core: could not read the core's environment; terminals inherit it", .{});
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return contents[0..length];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Variables the core sets for each terminal itself, which must survive `env -u`
|
||||||
|
/// even when its own environment happens to hold them too.
|
||||||
|
fn coreOwned(name: []const u8) bool {
|
||||||
|
return std.mem.startsWith(u8, name, "HC_") or
|
||||||
|
std.mem.eql(u8, name, "TERM_PROGRAM") or
|
||||||
|
std.mem.eql(u8, name, "JOURNAL_STREAM");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether `env` (`NAME=value` entries) sets `name`.
|
||||||
|
fn setIn(name: []const u8, env: []const []const u8) bool {
|
||||||
|
for (env) |entry| {
|
||||||
|
if (entry.len > name.len and entry[name.len] == '=' and std.mem.startsWith(u8, entry, name)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `env` by path where it has one, so finding it doesn't depend on the PATH of
|
||||||
|
/// the very environment it is there to correct.
|
||||||
|
fn envBinary() []const u8 {
|
||||||
|
return if (c.access("/usr/bin/env", X_OK) == 0) "/usr/bin/env" else "env";
|
||||||
|
}
|
||||||
|
|
||||||
fn writeRunParams(j: *std.json.Stringify, opts: RunOptions) !void {
|
fn writeRunParams(j: *std.json.Stringify, opts: RunOptions) !void {
|
||||||
try j.beginObject();
|
try j.beginObject();
|
||||||
try j.objectField("argv");
|
try j.objectField("argv");
|
||||||
@@ -822,6 +902,21 @@ pub fn detach(channel: u32) void {
|
|||||||
sendFrame(self, channel, .close, &.{}) catch {};
|
sendFrame(self, channel, .close, &.{}) catch {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "a variable counts as set only by its exact name" {
|
||||||
|
const env = [_][]const u8{ "PATH=/bin", "CLAUDE=1", "EMPTY=" };
|
||||||
|
try std.testing.expect(setIn("PATH", &env));
|
||||||
|
try std.testing.expect(setIn("EMPTY", &env));
|
||||||
|
try std.testing.expect(!setIn("PAT", &env));
|
||||||
|
try std.testing.expect(!setIn("CLAUDECODE", &env));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "the core's own per-terminal variables are never unset" {
|
||||||
|
try std.testing.expect(coreOwned("HC_TERMINAL_ID"));
|
||||||
|
try std.testing.expect(coreOwned("TERM_PROGRAM"));
|
||||||
|
try std.testing.expect(!coreOwned("CLAUDE_CODE_CHILD_SESSION"));
|
||||||
|
try std.testing.expect(!coreOwned("SWAYSOCK"));
|
||||||
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
_ = frame;
|
_ = frame;
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user