diff --git a/src/Command.zig b/src/Command.zig index f28d8bb9d..3a40143b9 100644 --- a/src/Command.zig +++ b/src/Command.zig @@ -18,6 +18,7 @@ const Command = @This(); const std = @import("std"); const builtin = @import("builtin"); +const configpkg = @import("config.zig"); const global_state = &@import("global.zig").state; const internal_os = @import("os/main.zig"); const windows = internal_os.windows; @@ -30,8 +31,20 @@ const testing = std.testing; const Allocator = std.mem.Allocator; const File = std.fs.File; const EnvMap = std.process.EnvMap; +const apprt = @import("apprt.zig"); -const PreExecFn = fn (*Command) void; +/// Function prototype for a function executed /in the child process/ after the +/// fork, but before exec'ing the command. If the function returns a u8, the +/// child process will be exited with that error code. +const PreExecFn = fn (*Command) ?u8; + +/// Allowable set of errors that can be returned by a post fork function. Any +/// errors will result in the failure to create the surface. +pub const PostForkError = error{PostForkError}; + +/// Function prototype for a function executed /in the parent process/ +/// after the fork. +const PostForkFn = fn (*Command) PostForkError!void; /// Path to the command to run. This doesn't have to be an absolute path, /// because use exec functions that search the PATH, if necessary. @@ -63,9 +76,25 @@ stderr: ?File = null, /// If set, this will be executed /in the child process/ after fork but /// before exec. This is useful to setup some state in the child before the /// exec process takes over, such as signal handlers, setsid, setuid, etc. -pre_exec: ?*const PreExecFn = null, +os_pre_exec: ?*const PreExecFn, -linux_cgroup: LinuxCgroup = linux_cgroup_default, +/// If set, this will be executed /in the child process/ after fork but +/// before exec. This is useful to setup some state in the child before the +/// exec process takes over, such as signal handlers, setsid, setuid, etc. +rt_pre_exec: ?*const PreExecFn, + +/// Configuration information needed by the apprt pre exec function. Note +/// that this should be a trivially copyable struct and not require any +/// allocation/deallocation. +rt_pre_exec_info: RtPreExecInfo, + +/// If set, this will be executed in the /in the parent process/ after the fork. +rt_post_fork: ?*const PostForkFn, + +/// Configuration information needed by the apprt post fork function. Note +/// that this should be a trivially copyable struct and not require any +/// allocation/deallocation. +rt_post_fork_info: RtPostForkInfo, /// If set, then the process will be created attached to this pseudo console. /// `stdin`, `stdout`, and `stderr` will be ignored if set. @@ -79,11 +108,6 @@ data: ?*anyopaque = null, /// Process ID is set after start is called. pid: ?posix.pid_t = null, -/// LinuxCGroup type depends on our target OS -pub const LinuxCgroup = if (builtin.os.tag == .linux) ?[]const u8 else void; -pub const linux_cgroup_default = if (LinuxCgroup == void) -{} else null; - /// The various methods a process may exit. pub const Exit = if (builtin.os.tag == .windows) union(enum) { Exited: u32, @@ -112,6 +136,24 @@ pub const Exit = if (builtin.os.tag == .windows) union(enum) { } }; +/// Configuration information needed by the apprt pre exec function. Note +/// that this should be a trivially copyable struct and not require any +/// allocation/deallocation. +pub const RtPreExecInfo = if (@hasDecl(apprt.runtime, "pre_exec")) apprt.runtime.pre_exec.PreExecInfo else struct { + pub inline fn init(_: *const configpkg.Config) @This() { + return .{}; + } +}; + +/// Configuration information needed by the apprt post fork function. Note +/// that this should be a trivially copyable struct and not require any +/// allocation/deallocation. +pub const RtPostForkInfo = if (@hasDecl(apprt.runtime, "post_fork")) apprt.runtime.post_fork.PostForkInfo else struct { + pub inline fn init(_: *const configpkg.Config) @This() { + return .{}; + } +}; + /// Start the subprocess. This returns immediately once the child is started. /// /// After this is successful, self.pid is available. @@ -143,19 +185,13 @@ fn startPosix(self: *Command, arena: Allocator) !void { else @compileError("missing env vars"); - // Fork. If we have a cgroup specified on Linxu then we use clone - const pid: posix.pid_t = switch (builtin.os.tag) { - .linux => if (self.linux_cgroup) |cgroup| - try internal_os.cgroup.cloneInto(cgroup) - else - try posix.fork(), - - else => try posix.fork(), - }; + // Fork. + const pid = try posix.fork(); if (pid != 0) { // Parent, return immediately. self.pid = @intCast(pid); + if (self.rt_post_fork) |f| try f(self); return; } @@ -182,8 +218,9 @@ fn startPosix(self: *Command, arena: Allocator) !void { // any failures are ignored (its best effort). global_state.rlimits.restore(); - // If the user requested a pre exec callback, call it now. - if (self.pre_exec) |f| f(self); + // If there are pre exec callbacks, call them now. + if (self.os_pre_exec) |f| if (f(self)) |exitcode| posix.exit(exitcode); + if (self.rt_pre_exec) |f| if (f(self)) |exitcode| posix.exit(exitcode); // Finally, replace our process. // Note: we must use the "p"-variant of exec here because we @@ -533,18 +570,22 @@ test "createNullDelimitedEnvMap" { } } -test "Command: pre exec" { +test "Command: os pre exec 1" { if (builtin.os.tag == .windows) return error.SkipZigTest; var cmd: Command = .{ .path = "/bin/sh", .args = &.{ "/bin/sh", "-v" }, - .pre_exec = (struct { - fn do(_: *Command) void { + .os_pre_exec = (struct { + fn do(_: *Command) ?u8 { // This runs in the child, so we can exit and it won't // kill the test runner. posix.exit(42); } }).do, + .rt_pre_exec = null, + .rt_post_fork = null, + .rt_pre_exec_info = undefined, + .rt_post_fork_info = undefined, }; try cmd.testingStart(); @@ -554,6 +595,100 @@ test "Command: pre exec" { try testing.expect(exit.Exited == 42); } +test "Command: os pre exec 2" { + if (builtin.os.tag == .windows) return error.SkipZigTest; + var cmd: Command = .{ + .path = "/bin/sh", + .args = &.{ "/bin/sh", "-v" }, + .os_pre_exec = (struct { + fn do(_: *Command) ?u8 { + // This runs in the child, so we can exit and it won't + // kill the test runner. + return 42; + } + }).do, + .rt_pre_exec = null, + .rt_post_fork = null, + .rt_pre_exec_info = undefined, + .rt_post_fork_info = undefined, + }; + + try cmd.testingStart(); + try testing.expect(cmd.pid != null); + const exit = try cmd.wait(true); + try testing.expect(exit == .Exited); + try testing.expect(exit.Exited == 42); +} + +test "Command: rt pre exec 1" { + if (builtin.os.tag == .windows) return error.SkipZigTest; + var cmd: Command = .{ + .path = "/bin/sh", + .args = &.{ "/bin/sh", "-v" }, + .os_pre_exec = null, + .rt_pre_exec = (struct { + fn do(_: *Command) ?u8 { + // This runs in the child, so we can exit and it won't + // kill the test runner. + posix.exit(42); + } + }).do, + .rt_post_fork = null, + .rt_pre_exec_info = undefined, + .rt_post_fork_info = undefined, + }; + + try cmd.testingStart(); + try testing.expect(cmd.pid != null); + const exit = try cmd.wait(true); + try testing.expect(exit == .Exited); + try testing.expect(exit.Exited == 42); +} + +test "Command: rt pre exec 2" { + if (builtin.os.tag == .windows) return error.SkipZigTest; + var cmd: Command = .{ + .path = "/bin/sh", + .args = &.{ "/bin/sh", "-v" }, + .os_pre_exec = null, + .rt_pre_exec = (struct { + fn do(_: *Command) ?u8 { + // This runs in the child, so we can exit and it won't + // kill the test runner. + return 42; + } + }).do, + .rt_post_fork = null, + .rt_pre_exec_info = undefined, + .rt_post_fork_info = undefined, + }; + + try cmd.testingStart(); + try testing.expect(cmd.pid != null); + const exit = try cmd.wait(true); + try testing.expect(exit == .Exited); + try testing.expect(exit.Exited == 42); +} + +test "Command: rt post fork 1" { + if (builtin.os.tag == .windows) return error.SkipZigTest; + var cmd: Command = .{ + .path = "/bin/sh", + .args = &.{ "/bin/sh", "-c", "sleep 1" }, + .os_pre_exec = null, + .rt_pre_exec = null, + .rt_post_fork = (struct { + fn do(_: *Command) PostForkError!void { + return error.PostForkError; + } + }).do, + .rt_pre_exec_info = undefined, + .rt_post_fork_info = undefined, + }; + + try testing.expectError(error.PostForkError, cmd.testingStart()); +} + fn createTestStdout(dir: std.fs.Dir) !File { const file = try dir.createFile("stdout.txt", .{ .read = true }); if (builtin.os.tag == .windows) { @@ -567,6 +702,19 @@ fn createTestStdout(dir: std.fs.Dir) !File { return file; } +fn createTestStderr(dir: std.fs.Dir) !File { + const file = try dir.createFile("stderr.txt", .{ .read = true }); + if (builtin.os.tag == .windows) { + try windows.SetHandleInformation( + file.handle, + windows.HANDLE_FLAG_INHERIT, + windows.HANDLE_FLAG_INHERIT, + ); + } + + return file; +} + test "Command: redirect stdout to file" { var td = try TempDir.init(); defer td.deinit(); @@ -581,6 +729,11 @@ test "Command: redirect stdout to file" { .path = "/bin/sh", .args = &.{ "/bin/sh", "-c", "echo hello" }, .stdout = stdout, + .os_pre_exec = null, + .rt_pre_exec = null, + .rt_post_fork = null, + .rt_pre_exec_info = undefined, + .rt_post_fork_info = undefined, }; try cmd.testingStart(); @@ -611,11 +764,21 @@ test "Command: custom env vars" { .args = &.{ "C:\\Windows\\System32\\cmd.exe", "/C", "echo %VALUE%" }, .stdout = stdout, .env = &env, + .os_pre_exec = null, + .rt_pre_exec = null, + .rt_post_fork = null, + .rt_pre_exec_info = undefined, + .rt_post_fork_info = undefined, } else .{ .path = "/bin/sh", .args = &.{ "/bin/sh", "-c", "echo $VALUE" }, .stdout = stdout, .env = &env, + .os_pre_exec = null, + .rt_pre_exec = null, + .rt_post_fork = null, + .rt_pre_exec_info = undefined, + .rt_post_fork_info = undefined, }; try cmd.testingStart(); @@ -647,11 +810,21 @@ test "Command: custom working directory" { .args = &.{ "C:\\Windows\\System32\\cmd.exe", "/C", "cd" }, .stdout = stdout, .cwd = "C:\\Windows\\System32", + .os_pre_exec = null, + .rt_pre_exec = null, + .rt_post_fork = null, + .rt_pre_exec_info = undefined, + .rt_post_fork_info = undefined, } else .{ .path = "/bin/sh", .args = &.{ "/bin/sh", "-c", "pwd" }, .stdout = stdout, .cwd = "/tmp", + .os_pre_exec = null, + .rt_pre_exec = null, + .rt_post_fork = null, + .rt_pre_exec_info = undefined, + .rt_post_fork_info = undefined, }; try cmd.testingStart(); @@ -688,12 +861,20 @@ test "Command: posix fork handles execveZ failure" { defer td.deinit(); var stdout = try createTestStdout(td.dir); defer stdout.close(); + var stderr = try createTestStderr(td.dir); + defer stderr.close(); var cmd: Command = .{ .path = "/not/a/binary", .args = &.{ "/not/a/binary", "" }, .stdout = stdout, + .stderr = stderr, .cwd = "/bin", + .os_pre_exec = null, + .rt_pre_exec = null, + .rt_post_fork = null, + .rt_pre_exec_info = undefined, + .rt_post_fork_info = undefined, }; try cmd.testingStart(); diff --git a/src/Surface.zig b/src/Surface.zig index e5e7d284d..588d52968 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -636,16 +636,8 @@ pub fn init( .working_directory = config.@"working-directory", .resources_dir = global_state.resources_dir.host(), .term = config.term, - - // Get the cgroup if we're on linux and have the decl. I'd love - // to change this from a decl to a surface options struct because - // then we can do memory management better (don't need to retain - // the string around). - .linux_cgroup = if (comptime builtin.os.tag == .linux and - @hasDecl(apprt.runtime.Surface, "cgroup")) - rt_surface.cgroup() - else - Command.linux_cgroup_default, + .rt_pre_exec_info = .init(config), + .rt_post_fork_info = .init(config), }); errdefer io_exec.deinit(); diff --git a/src/apprt/gtk.zig b/src/apprt/gtk.zig index 07b4eb0e7..36a9290fb 100644 --- a/src/apprt/gtk.zig +++ b/src/apprt/gtk.zig @@ -6,6 +6,8 @@ pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir; // The exported API, custom for the apprt. pub const class = @import("gtk/class.zig"); pub const WeakRef = @import("gtk/weak_ref.zig").WeakRef; +pub const pre_exec = @import("gtk/pre_exec.zig"); +pub const post_fork = @import("gtk/post_fork.zig"); test { @import("std").testing.refAllDecls(@This()); diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig index 654c1e1ac..868aa268d 100644 --- a/src/apprt/gtk/cgroup.zig +++ b/src/apprt/gtk/cgroup.zig @@ -1,7 +1,8 @@ -/// Contains all the logic for putting the Ghostty process and -/// each individual surface into its own cgroup. +/// Contains all the logic for putting individual surfaces into +/// transient systemd scopes. const std = @import("std"); const Allocator = std.mem.Allocator; +const assert = @import("../../quirks.zig").inlineAssert; const gio = @import("gio"); const glib = @import("glib"); @@ -12,125 +13,27 @@ const log = std.log.scoped(.gtk_systemd_cgroup); pub const Options = struct { memory_high: ?u64 = null, - pids_max: ?u64 = null, + tasks_max: ?u64 = null, }; -/// Initialize the cgroup for the app. This will create our -/// transient scope, initialize the cgroups we use for the app, -/// configure them, and return the cgroup path for the app. -/// -/// Returns the path of the current cgroup for the app, which is -/// allocated with the given allocator. -pub fn init( - alloc: Allocator, - dbus: *gio.DBusConnection, - opts: Options, -) ![]const u8 { - const pid = std.os.linux.getpid(); +pub fn fmtScope(buf: []u8, pid: u32) [:0]const u8 { + const fmt = "app-ghostty-surface-transient-{}.scope"; - // Get our initial cgroup. We need this so we can compare - // and detect when we've switched to our transient group. - const original = try internal_os.cgroup.current( - alloc, - pid, - ) orelse ""; - defer alloc.free(original); + assert(buf.len >= fmt.len - 2 + std.math.log10_int(@as(usize, std.math.maxInt(@TypeOf(pid)))) + 1); - // Create our transient scope. If this succeeds then the unit - // was created, but we may not have moved into it yet, so we need - // to do a dumb busy loop to wait for the move to complete. - try createScope(dbus, pid); - const transient = transient: while (true) { - const current = try internal_os.cgroup.current( - alloc, - pid, - ) orelse ""; - if (!std.mem.eql(u8, original, current)) break :transient current; - alloc.free(current); - std.Thread.sleep(25 * std.time.ns_per_ms); - }; - errdefer alloc.free(transient); - log.info("transient scope created cgroup={s}", .{transient}); - - // Create the app cgroup and put ourselves in it. This is - // required because controllers can't be configured while a - // process is in a cgroup. - try internal_os.cgroup.create(transient, "app", pid); - - // Create a cgroup that will contain all our surfaces. We will - // enable the controllers and configure resource limits for surfaces - // only on this cgroup so that it doesn't affect our main app. - try internal_os.cgroup.create(transient, "surfaces", null); - const surfaces = try std.fmt.allocPrint(alloc, "{s}/surfaces", .{transient}); - defer alloc.free(surfaces); - - // Enable all of our cgroup controllers. If these fail then - // we just log. We can't reasonably undo what we've done above - // so we log the warning and still return the transient group. - // I don't know a scenario where this fails yet. - try enableControllers(alloc, transient); - try enableControllers(alloc, surfaces); - - // Configure the "high" memory limit. This limit is used instead - // of "max" because it's a soft limit that can be exceeded and - // can be monitored by things like systemd-oomd to kill if needed, - // versus an instant hard kill. - if (opts.memory_high) |limit| { - try internal_os.cgroup.configureLimit(surfaces, .{ - .memory_high = limit, - }); - } - - // Configure the "max" pids limit. This is a hard limit and cannot be - // exceeded. - if (opts.pids_max) |limit| { - try internal_os.cgroup.configureLimit(surfaces, .{ - .pids_max = limit, - }); - } - - return transient; + return std.fmt.bufPrintZ(buf, fmt, .{pid}) catch unreachable; } -/// Enable all the cgroup controllers for the given cgroup. -fn enableControllers(alloc: Allocator, cgroup: []const u8) !void { - const raw = try internal_os.cgroup.controllers(alloc, cgroup); - defer alloc.free(raw); - - // Build our string builder for enabling all controllers - var builder: std.Io.Writer.Allocating = .init(alloc); - defer builder.deinit(); - - // Controllers are space-separated - var it = std.mem.splitScalar(u8, raw, ' '); - while (it.next()) |controller| { - try builder.writer.writeByte('+'); - try builder.writer.writeAll(controller); - if (it.rest().len > 0) try builder.writer.writeByte(' '); - } - - // Enable them all - try internal_os.cgroup.configureControllers( - cgroup, - builder.written(), - ); -} - -/// Create a transient systemd scope unit for the current process and -/// move our process into it. -fn createScope( +/// Create a transient systemd scope unit for the given process and +/// move the process into it. +pub fn createScope( dbus: *gio.DBusConnection, - pid_: std.os.linux.pid_t, -) !void { - const pid: u32 = @intCast(pid_); - - // The unit name needs to be unique. We use the pid for this. + pid: u32, + options: Options, +) error{DbusCallFailed}!void { + // The unit name needs to be unique. We use the PID for this. var name_buf: [256]u8 = undefined; - const name = std.fmt.bufPrintZ( - &name_buf, - "app-ghostty-transient-{}.scope", - .{pid}, - ) catch unreachable; + const name = fmtScope(&name_buf, pid); const builder_type = glib.VariantType.new("(ssa(sv)a(sa(sv)))"); defer glib.free(builder_type); @@ -150,16 +53,18 @@ fn createScope( builder.open(properties_type); defer builder.close(); + if (options.memory_high) |value| { + builder.add("(sv)", "MemoryHigh", glib.Variant.newUint64(value)); + } + + if (options.tasks_max) |value| { + builder.add("(sv)", "TasksMax", glib.Variant.newUint64(value)); + } + // https://www.freedesktop.org/software/systemd/man/latest/systemd-oomd.service.html - const pressure_value = glib.Variant.newString("kill"); + builder.add("(sv)", "ManagedOOMMemoryPressure", glib.Variant.newString("kill")); - builder.add("(sv)", "ManagedOOMMemoryPressure", pressure_value); - - // Delegate - const delegate_value = glib.Variant.newBoolean(1); - builder.add("(sv)", "Delegate", delegate_value); - - // Pid to move into the unit + // PID to move into the unit const pids_value_type = glib.VariantType.new("u"); defer glib.free(pids_value_type); @@ -169,7 +74,7 @@ fn createScope( } { - // Aux + // Aux - unused but must be present const aux_type = glib.VariantType.new("a(sa(sv))"); defer glib.free(aux_type); diff --git a/src/apprt/gtk/class/application.zig b/src/apprt/gtk/class/application.zig index 5030236e5..c24352c18 100644 --- a/src/apprt/gtk/class/application.zig +++ b/src/apprt/gtk/class/application.zig @@ -12,7 +12,6 @@ const build_config = @import("../../../build_config.zig"); const state = &@import("../../../global.zig").state; const i18n = @import("../../../os/main.zig").i18n; const apprt = @import("../../../apprt.zig"); -const cgroup = @import("../cgroup.zig"); const CoreApp = @import("../../../App.zig"); const configpkg = @import("../../../config.zig"); const input = @import("../../../input.zig"); @@ -176,11 +175,6 @@ pub const Application = extern struct { /// The global shortcut logic. global_shortcuts: *GlobalShortcuts, - /// The base path of the transient cgroup used to put all surfaces - /// into their own cgroup. This is only set if cgroups are enabled - /// and initialization was successful. - transient_cgroup_base: ?[]const u8 = null, - /// This is set to true so long as we request a window exactly /// once. This prevents quitting the app before we've shown one /// window. @@ -438,7 +432,6 @@ pub const Application = extern struct { priv.config.unref(); priv.winproto.deinit(alloc); priv.global_shortcuts.unref(); - if (priv.transient_cgroup_base) |base| alloc.free(base); if (priv.saved_language) |language| alloc.free(language); if (gdk.Display.getDefault()) |display| { gtk.StyleContext.removeProviderForDisplay( @@ -809,11 +802,6 @@ pub const Application = extern struct { return &self.private().winproto; } - /// Returns the cgroup base (if any). - pub fn cgroupBase(self: *Self) ?[]const u8 { - return self.private().transient_cgroup_base; - } - /// This will get called when there are no more open surfaces. fn startQuitTimer(self: *Self) void { const priv = self.private(); @@ -1312,22 +1300,6 @@ pub const Application = extern struct { // Setup our global shortcuts self.startupGlobalShortcuts(); - // Setup our cgroup for the application. - self.startupCgroup() catch |err| { - log.warn("cgroup initialization failed err={}", .{err}); - - // Add it to our config diagnostics so it shows up in a GUI dialog. - // Admittedly this has two issues: (1) we shuldn't be using the - // config errors dialog for this long term and (2) using a mut - // ref to the config wouldn't propagate changes to UI properly, - // but we're in startup mode so its okay. - const config = self.private().config.getMut(); - config.addDiagnosticFmt( - "cgroup initialization failed: {}", - .{err}, - ) catch {}; - }; - // If we have any config diagnostics from loading, then we // show the diagnostics dialog. We show this one as a general // modal (not to any specific window) because we don't even @@ -1461,72 +1433,6 @@ pub const Application = extern struct { ); } - const CgroupError = error{ - DbusConnectionFailed, - CgroupInitFailed, - }; - - /// Setup our cgroup for the application, if enabled. - /// - /// The setup for cgroups involves creating the cgroup for our - /// application, moving ourselves into it, and storing the base path - /// so that created surfaces can also have their own cgroups. - fn startupCgroup(self: *Self) CgroupError!void { - const priv = self.private(); - const config = priv.config.get(); - - // If cgroup isolation isn't enabled then we don't do this. - if (!switch (config.@"linux-cgroup") { - .never => false, - .always => true, - .@"single-instance" => single: { - const flags = self.as(gio.Application).getFlags(); - break :single !flags.non_unique; - }, - }) { - log.info( - "cgroup isolation disabled via config={}", - .{config.@"linux-cgroup"}, - ); - return; - } - - // We need a dbus connection to do anything else - const dbus = self.as(gio.Application).getDbusConnection() orelse { - if (config.@"linux-cgroup-hard-fail") { - log.err("dbus connection required for cgroup isolation, exiting", .{}); - return error.DbusConnectionFailed; - } - - return; - }; - - const alloc = priv.core_app.alloc; - const path = cgroup.init(alloc, dbus, .{ - .memory_high = config.@"linux-cgroup-memory-limit", - .pids_max = config.@"linux-cgroup-processes-limit", - }) catch |err| { - // If we can't initialize cgroups then that's okay. We - // want to continue to run so we just won't isolate surfaces. - // NOTE(mitchellh): do we want a config to force it? - log.warn( - "failed to initialize cgroups, terminals will not be isolated err={}", - .{err}, - ); - - // If we have hard fail enabled then we exit now. - if (config.@"linux-cgroup-hard-fail") { - log.err("linux-cgroup-hard-fail enabled, exiting", .{}); - return error.CgroupInitFailed; - } - - return; - }; - - log.info("cgroup isolation enabled base={s}", .{path}); - priv.transient_cgroup_base = path; - } - fn activate(self: *Self) callconv(.c) void { log.debug("activate", .{}); diff --git a/src/apprt/gtk/class/surface.zig b/src/apprt/gtk/class/surface.zig index fb87cdd8f..7627470a5 100644 --- a/src/apprt/gtk/class/surface.zig +++ b/src/apprt/gtk/class/surface.zig @@ -551,10 +551,6 @@ pub const Surface = extern struct { /// The configuration that this surface is using. config: ?*Config = null, - /// The cgroup created for this surface. This will be created - /// if `Application.transient_cgroup_base` is set. - cgroup_path: ?[]const u8 = null, - /// The default size for a window that embeds this surface. default_size: ?*Size = null, @@ -1433,63 +1429,6 @@ pub const Surface = extern struct { }; } - /// Initialize the cgroup for this surface if it hasn't been - /// already. While this is `init`-prefixed, we prefer to call this - /// in the realize function because we don't need to create a cgroup - /// if we don't init a surface. - fn initCgroup(self: *Self) void { - const priv = self.private(); - - // If we already have a cgroup path then we don't do it again. - if (priv.cgroup_path != null) return; - - const app = Application.default(); - const alloc = app.allocator(); - const base = app.cgroupBase() orelse return; - - // For the unique group name we use the self pointer. This may - // not be a good idea for security reasons but not sure yet. We - // may want to change this to something else eventually to be safe. - var buf: [256]u8 = undefined; - const name = std.fmt.bufPrint( - &buf, - "surfaces/{X}.scope", - .{@intFromPtr(self)}, - ) catch unreachable; - - // Create the cgroup. If it fails, no big deal... just ignore. - internal_os.cgroup.create(base, name, null) catch |err| { - log.warn("failed to create surface cgroup err={}", .{err}); - return; - }; - - // Success, save the cgroup path. - priv.cgroup_path = std.fmt.allocPrint( - alloc, - "{s}/{s}", - .{ base, name }, - ) catch null; - } - - /// Deletes the cgroup if set. - fn clearCgroup(self: *Self) void { - const priv = self.private(); - const path = priv.cgroup_path orelse return; - - internal_os.cgroup.remove(path) catch |err| { - // We don't want this to be fatal in any way so we just log - // and continue. A dangling empty cgroup is not a big deal - // and this should be rare. - log.warn( - "failed to remove cgroup for surface path={s} err={}", - .{ path, err }, - ); - }; - - Application.default().allocator().free(path); - priv.cgroup_path = null; - } - //--------------------------------------------------------------- // Libghostty Callbacks @@ -1525,10 +1464,6 @@ pub const Surface = extern struct { return true; } - pub fn cgroupPath(self: *Self) ?[]const u8 { - return self.private().cgroup_path; - } - pub fn getContentScale(self: *Self) apprt.ContentScale { const priv = self.private(); const gl_area = priv.gl_area; @@ -1968,8 +1903,6 @@ pub const Surface = extern struct { for (priv.key_tables.items) |s| alloc.free(s); priv.key_tables.deinit(alloc); - self.clearCgroup(); - gobject.Object.virtual_methods.finalize.call( Class.parent, self.as(Parent), @@ -3331,10 +3264,6 @@ pub const Surface = extern struct { const app = Application.default(); const alloc = app.allocator(); - // Initialize our cgroup if we can. - self.initCgroup(); - errdefer self.clearCgroup(); - // Make our pointer to store our surface const surface = try alloc.create(CoreSurface); errdefer alloc.destroy(surface); diff --git a/src/apprt/gtk/post_fork.zig b/src/apprt/gtk/post_fork.zig new file mode 100644 index 000000000..ff0219508 --- /dev/null +++ b/src/apprt/gtk/post_fork.zig @@ -0,0 +1,121 @@ +const std = @import("std"); + +const gio = @import("gio"); +const glib = @import("glib"); + +const log = std.log.scoped(.gtk_post_fork); + +const configpkg = @import("../../config.zig"); +const internal_os = @import("../../os/main.zig"); +const Command = @import("../../Command.zig"); +const cgroup = @import("./cgroup.zig"); + +const Application = @import("class/application.zig").Application; + +pub const PostForkInfo = struct { + gtk_single_instance: configpkg.Config.GtkSingleInstance, + linux_cgroup: configpkg.Config.LinuxCgroup, + linux_cgroup_hard_fail: bool, + linux_cgroup_memory_limit: ?u64, + linux_cgroup_processes_limit: ?u64, + + pub fn init(cfg: *const configpkg.Config) PostForkInfo { + return .{ + .gtk_single_instance = cfg.@"gtk-single-instance", + .linux_cgroup = cfg.@"linux-cgroup", + .linux_cgroup_hard_fail = cfg.@"linux-cgroup-hard-fail", + .linux_cgroup_memory_limit = cfg.@"linux-cgroup-memory-limit", + .linux_cgroup_processes_limit = cfg.@"linux-cgroup-processes-limit", + }; + } +}; + +/// If we are configured to do so, tell `systemd` to move the new child PID into +/// a transient `systemd` scope with the configured resource limits. +/// +/// If we are configured to hard fail, log an error message and return an error +/// code if we don't detect the move in time. +pub fn postFork(cmd: *Command) Command.PostForkError!void { + switch (cmd.rt_post_fork_info.linux_cgroup) { + .always => {}, + .never => return, + .@"single-instance" => switch (cmd.rt_post_fork_info.gtk_single_instance) { + .true => {}, + .false => return, + .detect => { + log.err("gtk-single-instance is set to detect which should be impossible!", .{}); + return error.PostForkError; + }, + }, + } + + const pid: u32 = @intCast(cmd.pid orelse { + log.err("PID of child not known!", .{}); + return error.PostForkError; + }); + + var expected_cgroup_buf: [256]u8 = undefined; + const expected_cgroup = cgroup.fmtScope(&expected_cgroup_buf, pid); + + log.debug("beginning transition to transient systemd scope {s}", .{expected_cgroup}); + + const app = Application.default(); + + const dbus = app.as(gio.Application).getDbusConnection() orelse { + if (cmd.rt_post_fork_info.linux_cgroup_hard_fail) { + log.err("dbus connection required for cgroup isolation, exiting", .{}); + return error.PostForkError; + } + return; + }; + + cgroup.createScope( + dbus, + pid, + .{ + .memory_high = cmd.rt_post_fork_info.linux_cgroup_memory_limit, + .tasks_max = cmd.rt_post_fork_info.linux_cgroup_processes_limit, + }, + ) catch |err| { + if (cmd.rt_post_fork_info.linux_cgroup_hard_fail) { + log.err("unable to create transient systemd scope {s}: {t}", .{ expected_cgroup, err }); + return error.PostForkError; + } + log.warn("unable to create transient systemd scope {s}: {t}", .{ expected_cgroup, err }); + return; + }; + + const start = std.time.Instant.now() catch unreachable; + + loop: while (true) { + const now = std.time.Instant.now() catch unreachable; + + if (now.since(start) > 250 * std.time.ns_per_ms) { + if (cmd.rt_pre_exec_info.linux_cgroup_hard_fail) { + log.err("transition to new transient systemd scope {s} took too long", .{expected_cgroup}); + return error.PostForkError; + } + log.warn("transition to transient systemd scope {s} took too long", .{expected_cgroup}); + break :loop; + } + + not_found: { + var current_cgroup_buf: [4096]u8 = undefined; + + const current_cgroup_raw = internal_os.cgroup.current( + ¤t_cgroup_buf, + @intCast(pid), + ) orelse break :not_found; + + const index = std.mem.lastIndexOfScalar(u8, current_cgroup_raw, '/') orelse break :not_found; + const current_cgroup = current_cgroup_raw[index + 1 ..]; + + if (std.mem.eql(u8, current_cgroup, expected_cgroup)) { + log.debug("transition to transient systemd scope {s} complete", .{expected_cgroup}); + break :loop; + } + } + + std.Thread.sleep(25 * std.time.ns_per_ms); + } +} diff --git a/src/apprt/gtk/pre_exec.zig b/src/apprt/gtk/pre_exec.zig new file mode 100644 index 000000000..6f6a9ed51 --- /dev/null +++ b/src/apprt/gtk/pre_exec.zig @@ -0,0 +1,81 @@ +const std = @import("std"); + +const log = std.log.scoped(.gtk_pre_exec); + +const configpkg = @import("../../config.zig"); + +const internal_os = @import("../../os/main.zig"); +const Command = @import("../../Command.zig"); +const cgroup = @import("./cgroup.zig"); + +pub const PreExecInfo = struct { + gtk_single_instance: configpkg.Config.GtkSingleInstance, + linux_cgroup: configpkg.Config.LinuxCgroup, + linux_cgroup_hard_fail: bool, + + pub fn init(cfg: *const configpkg.Config) PreExecInfo { + return .{ + .gtk_single_instance = cfg.@"gtk-single-instance", + .linux_cgroup = cfg.@"linux-cgroup", + .linux_cgroup_hard_fail = cfg.@"linux-cgroup-hard-fail", + }; + } +}; + +/// If we are expecting to be moved to a transient systemd scope, wait to see if +/// that happens by checking for the correct name of the current cgroup. Wait at +/// most 250ms so that we don't overly delay the soft-fail scenario. +/// +/// If we are configured to hard fail, log an error message and return an error +/// code if we don't detect the move in time. +pub fn preExec(cmd: *Command) ?u8 { + switch (cmd.rt_pre_exec_info.linux_cgroup) { + .always => {}, + .never => return null, + .@"single-instance" => switch (cmd.rt_pre_exec_info.gtk_single_instance) { + .true => {}, + .false => return null, + .detect => { + log.err("gtk-single-instance is set to detect", .{}); + return 127; + }, + }, + } + + const pid: u32 = @intCast(std.os.linux.getpid()); + + var expected_cgroup_buf: [256]u8 = undefined; + const expected_cgroup = cgroup.fmtScope(&expected_cgroup_buf, pid); + + const start = std.time.Instant.now() catch unreachable; + + while (true) { + const now = std.time.Instant.now() catch unreachable; + + if (now.since(start) > 250 * std.time.ns_per_ms) { + if (cmd.rt_pre_exec_info.linux_cgroup_hard_fail) { + log.err("transition to new transient systemd scope took too long", .{}); + return 127; + } + break; + } + + not_found: { + var current_cgroup_buf: [4096]u8 = undefined; + + const current_cgroup_raw = internal_os.cgroup.current( + ¤t_cgroup_buf, + @intCast(pid), + ) orelse break :not_found; + + const index = std.mem.lastIndexOfScalar(u8, current_cgroup_raw, '/') orelse break :not_found; + const current_cgroup = current_cgroup_raw[index + 1 ..]; + + if (std.mem.eql(u8, current_cgroup, expected_cgroup)) return null; + } + + std.Thread.sleep(25 * std.time.ns_per_ms); + } + + return null; +} diff --git a/src/config/Config.zig b/src/config/Config.zig index bc25fd5b2..bb86b6bd5 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -3392,13 +3392,12 @@ keybind: Keybinds = .{}, /// Available since: 1.2.0 @"macos-shortcuts": MacShortcuts = .ask, -/// Put every surface (tab, split, window) into a dedicated Linux cgroup. +/// Put every surface (tab, split, window) into a transient `systemd` scope. /// -/// This makes it so that resource management can be done on a per-surface -/// granularity. For example, if a shell program is using too much memory, -/// only that shell will be killed by the oom monitor instead of the entire -/// Ghostty process. Similarly, if a shell program is using too much CPU, -/// only that surface will be CPU-throttled. +/// This allows per-surface resource management. For example, if a shell program +/// is using too much memory, only that shell will be killed by the oom monitor +/// instead of the entire Ghostty process. Similarly, if a shell program is +/// using too much CPU, only that surface will be CPU-throttled. /// /// This will cause startup times to be slower (a hundred milliseconds or so), /// so the default value is "single-instance." In single-instance mode, only @@ -3407,9 +3406,12 @@ keybind: Keybinds = .{}, /// more likely to have many windows, tabs, etc. so cgroup isolation is a /// big benefit. /// -/// This feature requires systemd. If systemd is unavailable, cgroup -/// initialization will fail. By default, this will not prevent Ghostty -/// from working (see linux-cgroup-hard-fail). +/// This feature requires `systemd`. If `systemd` is unavailable, cgroup +/// initialization will fail. By default, this will not prevent Ghostty from +/// working (see `linux-cgroup-hard-fail`). +/// +/// Changing this value and reloading the config will not affect existing +/// surfaces. /// /// Valid values are: /// @@ -3425,30 +3427,42 @@ else /// Memory limit for any individual terminal process (tab, split, window, /// etc.) in bytes. If this is unset then no memory limit will be set. /// -/// Note that this sets the "memory.high" configuration for the memory -/// controller, which is a soft limit. You should configure something like -/// systemd-oom to handle killing processes that have too much memory +/// Note that this sets the `MemoryHigh` setting on the transient `systemd` +/// scope, which is a soft limit. You should configure something like +/// `systemd-oom` to handle killing processes that have too much memory /// pressure. +/// +/// Changing this value and reloading the config will not affect existing +/// surfaces. +/// +/// See the `systemd.resource-control` manual page for more information: +/// https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html @"linux-cgroup-memory-limit": ?u64 = null, /// Number of processes limit for any individual terminal process (tab, split, /// window, etc.). If this is unset then no limit will be set. /// -/// Note that this sets the "pids.max" configuration for the process number -/// controller, which is a hard limit. +/// Note that this sets the `TasksMax` setting on the transient `systemd` scope, +/// which is a hard limit. +/// +/// Changing this value and reloading the config will not affect existing +/// surfaces. +/// +/// See the `systemd.resource-control` manual page for more information: +/// https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html @"linux-cgroup-processes-limit": ?u64 = null, -/// If this is false, then any cgroup initialization (for linux-cgroup) -/// will be allowed to fail and the failure is ignored. This is useful if -/// you view cgroup isolation as a "nice to have" and not a critical resource -/// management feature, because Ghostty startup will not fail if cgroup APIs -/// fail. +/// If this is false, then creating a transient `systemd` scope (for +/// `linux-cgroup`) will be allowed to fail and the failure is ignored. This is +/// useful if you view cgroup isolation as a "nice to have" and not a critical +/// resource management feature, because surface creation will not fail if +/// `systemd` APIs fail. /// -/// If this is true, then any cgroup initialization failure will cause -/// Ghostty to exit or new surfaces to not be created. +/// If this is true, then any transient `systemd` scope creation failure will +/// cause surface creation to fail. /// -/// Note: This currently only affects cgroup initialization. Subprocesses -/// must always be able to move themselves into an isolated cgroup. +/// Changing this value and reloading the config will not affect existing +/// surfaces. @"linux-cgroup-hard-fail": bool = false, /// Enable or disable GTK's OpenGL debugging logs. The default is `true` for diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig index a55732ca3..9e68a50fd 100644 --- a/src/os/cgroup.zig +++ b/src/os/cgroup.zig @@ -1,254 +1,26 @@ const std = @import("std"); -const assert = @import("../quirks.zig").inlineAssert; -const linux = std.os.linux; -const posix = std.posix; -const Allocator = std.mem.Allocator; const log = std.log.scoped(.@"linux-cgroup"); /// Returns the path to the cgroup for the given pid. -pub fn current(alloc: Allocator, pid: std.os.linux.pid_t) !?[]const u8 { - var buf: [std.fs.max_path_bytes]u8 = undefined; +pub fn current(buf: []u8, pid: u32) ?[]const u8 { + var path_buf: [std.fs.max_path_bytes]u8 = undefined; // Read our cgroup by opening /proc//cgroup and reading the first // line. The first line will look something like this: // 0::/user.slice/user-1000.slice/session-1.scope // The cgroup path is the third field. - const path = try std.fmt.bufPrint(&buf, "/proc/{}/cgroup", .{pid}); - const file = try std.fs.cwd().openFile(path, .{}); + const path = std.fmt.bufPrint(&path_buf, "/proc/{}/cgroup", .{pid}) catch return null; + const file = std.fs.openFileAbsolute(path, .{}) catch return null; defer file.close(); - // Read it all into memory -- we don't expect this file to ever be that large. - const contents = try file.readToEndAlloc( - alloc, - 1 * 1024 * 1024, // 1MB - ); - defer alloc.free(contents); + var read_buf: [64]u8 = undefined; + var file_reader = file.reader(&read_buf); + const reader = &file_reader.interface; + const len = reader.readSliceShort(buf) catch return null; + const contents = buf[0..len]; // Find the last ':' const idx = std.mem.lastIndexOfScalar(u8, contents, ':') orelse return null; - const result = std.mem.trimRight(u8, contents[idx + 1 ..], " \r\n"); - return try alloc.dupe(u8, result); -} - -/// Create a new cgroup. This will not move any process into it unless move is -/// set. If move is set, the given pid will be moved into the created cgroup. -pub fn create( - cgroup: []const u8, - child: []const u8, - move: ?std.os.linux.pid_t, -) !void { - var buf: [std.fs.max_path_bytes]u8 = undefined; - const path = try std.fmt.bufPrint(&buf, "/sys/fs/cgroup{s}/{s}", .{ cgroup, child }); - try std.fs.cwd().makePath(path); - - // If we have a PID to move into the cgroup immediately, do it. - if (move) |pid| { - const pid_path = try std.fmt.bufPrint( - &buf, - "/sys/fs/cgroup{s}/{s}/cgroup.procs", - .{ cgroup, child }, - ); - const file = try std.fs.cwd().openFile(pid_path, .{ .mode = .write_only }); - defer file.close(); - - var file_buf: [64]u8 = undefined; - var writer = file.writer(&file_buf); - try writer.interface.print("{}", .{pid}); - try writer.interface.flush(); - } -} - -/// Remove a cgroup. This will only succeed if the cgroup is empty -/// (has no processes). The cgroup path should be relative to the -/// cgroup root (e.g. "/user.slice/surfaces/abc123.scope"). -pub fn remove(cgroup: []const u8) !void { - assert(cgroup.len > 0); - assert(cgroup[0] == '/'); - - var buf: [std.fs.max_path_bytes]u8 = undefined; - const path = try std.fmt.bufPrint(&buf, "/sys/fs/cgroup{s}", .{cgroup}); - std.fs.cwd().deleteDir(path) catch |err| switch (err) { - // If it doesn't exist, that's fine - maybe it was already cleaned up - error.FileNotFound => {}, - - // Any other error we failed to delete it so we want to notify - // the user. - else => return err, - }; -} - -/// Move the given PID into the given cgroup. -pub fn moveInto( - cgroup: []const u8, - pid: std.os.linux.pid_t, -) !void { - var buf: [std.fs.max_path_bytes]u8 = undefined; - const path = try std.fmt.bufPrint(&buf, "/sys/fs/cgroup{s}/cgroup.procs", .{cgroup}); - const file = try std.fs.cwd().openFile(path, .{ .mode = .write_only }); - defer file.close(); - try file.writer().print("{}", .{pid}); -} - -/// Use clone3 to have the kernel create a new process with the correct cgroup -/// rather than moving the process to the correct cgroup later. -pub fn cloneInto(cgroup: []const u8) !posix.pid_t { - var buf: [std.fs.max_path_bytes]u8 = undefined; - const path = try std.fmt.bufPrintZ(&buf, "/sys/fs/cgroup{s}", .{cgroup}); - - // Get a file descriptor that refers to the cgroup directory in the cgroup - // sysfs to pass to the kernel in clone3. - const fd: linux.fd_t = fd: { - const rc = linux.open( - path, - .{ - // Self-explanatory: we expect to open a directory, and - // we only need the path-level permissions. - .PATH = true, - .DIRECTORY = true, - - // We don't want to leak this fd to the child process - // when we clone below since we're using this fd for - // a cgroup clone. - .CLOEXEC = true, - }, - 0, - ); - - switch (posix.errno(rc)) { - .SUCCESS => break :fd @as(linux.fd_t, @intCast(rc)), - else => |errno| { - log.err("unable to open cgroup dir {s}: {}", .{ path, errno }); - return error.CloneError; - }, - } - }; - assert(fd >= 0); - defer _ = linux.close(fd); - - const args: extern struct { - flags: u64, - pidfd: u64, - child_tid: u64, - parent_tid: u64, - exit_signal: u64, - stack: u64, - stack_size: u64, - tls: u64, - set_tid: u64, - set_tid_size: u64, - cgroup: u64, - } = .{ - .flags = linux.CLONE.INTO_CGROUP, - .pidfd = 0, - .child_tid = 0, - .parent_tid = 0, - .exit_signal = linux.SIG.CHLD, - .stack = 0, - .stack_size = 0, - .tls = 0, - .set_tid = 0, - .set_tid_size = 0, - .cgroup = @intCast(fd), - }; - - const rc = linux.syscall2(linux.SYS.clone3, @intFromPtr(&args), @sizeOf(@TypeOf(args))); - // do not use posix.errno, when linking libc it will use the libc errno which will not be set when making the syscall directly - return switch (std.os.linux.E.init(rc)) { - .SUCCESS => @as(posix.pid_t, @intCast(rc)), - else => |errno| err: { - log.err("unable to clone: {}", .{errno}); - break :err error.CloneError; - }, - }; -} - -/// Returns all available cgroup controllers for the given cgroup. -/// The cgroup should have a '/'-prefix. -/// -/// The returned list of is the raw space-separated list of -/// controllers from the /sys/fs directory. This avoids some extra -/// work since creating an iterator over this is easy and much cheaper -/// than allocating a bunch of copies for an array. -pub fn controllers(alloc: Allocator, cgroup: []const u8) ![]const u8 { - assert(cgroup[0] == '/'); - var buf: [std.fs.max_path_bytes]u8 = undefined; - - // Read the available controllers. These will be space separated. - const path = try std.fmt.bufPrint( - &buf, - "/sys/fs/cgroup{s}/cgroup.controllers", - .{cgroup}, - ); - const file = try std.fs.cwd().openFile(path, .{}); - defer file.close(); - - // Read it all into memory -- we don't expect this file to ever - // be that large. - const contents = try file.readToEndAlloc( - alloc, - 1 * 1024 * 1024, // 1MB - ); - defer alloc.free(contents); - - // Return our raw list of controllers - const result = std.mem.trimRight(u8, contents, " \r\n"); - return try alloc.dupe(u8, result); -} - -/// Configure the set of controllers in the cgroup. The "v" should -/// be in a valid format for "cgroup.subtree_control" -pub fn configureControllers( - cgroup: []const u8, - v: []const u8, -) !void { - assert(cgroup[0] == '/'); - var buf: [std.fs.max_path_bytes]u8 = undefined; - - // Read the available controllers. These will be space separated. - const path = try std.fmt.bufPrint( - &buf, - "/sys/fs/cgroup{s}/cgroup.subtree_control", - .{cgroup}, - ); - const file = try std.fs.cwd().openFile(path, .{ .mode = .write_only }); - defer file.close(); - - // Write - var writer_buf: [4096]u8 = undefined; - var writer = file.writer(&writer_buf); - try writer.interface.writeAll(v); - try writer.interface.flush(); -} - -pub const Limit = union(enum) { - memory_high: usize, - pids_max: usize, -}; - -/// Configure a limit for the given cgroup. Use the various -/// fields in Limit to configure a specific type of limit. -pub fn configureLimit(cgroup: []const u8, limit: Limit) !void { - assert(cgroup[0] == '/'); - - const filename, const size = switch (limit) { - .memory_high => |v| .{ "memory.high", v }, - .pids_max => |v| .{ "pids.max", v }, - }; - - // Open our file - var buf: [std.fs.max_path_bytes]u8 = undefined; - const path = try std.fmt.bufPrint( - &buf, - "/sys/fs/cgroup{s}/{s}", - .{ cgroup, filename }, - ); - const file = try std.fs.cwd().openFile(path, .{ .mode = .write_only }); - defer file.close(); - - // Write our limit in bytes - var writer_buf: [4096]u8 = undefined; - var writer = file.writer(&writer_buf); - try writer.interface.print("{}", .{size}); - try writer.interface.flush(); + return std.mem.trimRight(u8, contents[idx + 1 ..], " \r\n"); } diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 4443f324b..af4df3fef 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -566,7 +566,9 @@ pub const Config = struct { working_directory: ?[]const u8 = null, resources_dir: ?[]const u8, term: []const u8, - linux_cgroup: Command.LinuxCgroup = Command.linux_cgroup_default, + + rt_pre_exec_info: Command.RtPreExecInfo, + rt_post_fork_info: Command.RtPostForkInfo, }; const Subprocess = struct { @@ -584,7 +586,9 @@ const Subprocess = struct { screen_size: renderer.ScreenSize, pty: ?Pty = null, process: ?Process = null, - linux_cgroup: Command.LinuxCgroup = Command.linux_cgroup_default, + + rt_pre_exec_info: Command.RtPreExecInfo, + rt_post_fork_info: Command.RtPostForkInfo, /// Union that represents the running process type. const Process = union(enum) { @@ -851,21 +855,14 @@ const Subprocess = struct { // https://github.com/ghostty-org/ghostty/discussions/7769 if (cwd) |pwd| try env.put("PWD", pwd); - // If we have a cgroup, then we copy that into our arena so the - // memory remains valid when we start. - const linux_cgroup: Command.LinuxCgroup = cgroup: { - const default = Command.linux_cgroup_default; - if (comptime builtin.os.tag != .linux) break :cgroup default; - const path = cfg.linux_cgroup orelse break :cgroup default; - break :cgroup try alloc.dupe(u8, path); - }; - return .{ .arena = arena, .env = env, .cwd = cwd, .args = args, - .linux_cgroup = linux_cgroup, + + .rt_pre_exec_info = cfg.rt_pre_exec_info, + .rt_post_fork_info = cfg.rt_post_fork_info, // Should be initialized with initTerminal call. .grid_size = .{}, @@ -1014,17 +1011,27 @@ const Subprocess = struct { .stdout = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave }, .stderr = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave }, .pseudo_console = if (builtin.os.tag == .windows) pty.pseudo_console else {}, - .pre_exec = if (builtin.os.tag == .windows) null else (struct { - fn callback(cmd: *Command) void { - const sp = cmd.getData(Subprocess) orelse unreachable; - sp.childPreExec() catch |err| log.err( - "error initializing child: {}", - .{err}, - ); - } - }).callback, + .os_pre_exec = switch (comptime builtin.os.tag) { + .windows => null, + else => f: { + const f = struct { + fn callback(cmd: *Command) ?u8 { + const sp = cmd.getData(Subprocess) orelse unreachable; + sp.childPreExec() catch |err| log.err( + "error initializing child: {}", + .{err}, + ); + return null; + } + }; + break :f f.callback; + }, + }, + .rt_pre_exec = if (comptime @hasDecl(apprt.runtime, "pre_exec")) apprt.runtime.pre_exec.preExec else null, + .rt_pre_exec_info = self.rt_pre_exec_info, + .rt_post_fork = if (comptime @hasDecl(apprt.runtime, "post_fork")) apprt.runtime.post_fork.postFork else null, + .rt_post_fork_info = self.rt_post_fork_info, .data = self, - .linux_cgroup = self.linux_cgroup, }; cmd.start(alloc) catch |err| { @@ -1046,9 +1053,6 @@ const Subprocess = struct { log.warn("error killing command during cleanup err={}", .{err}); }; log.info("started subcommand path={s} pid={?}", .{ self.args[0], cmd.pid }); - if (comptime builtin.os.tag == .linux) { - log.info("subcommand cgroup={s}", .{self.linux_cgroup orelse "-"}); - } self.process = .{ .fork_exec = cmd }; return switch (builtin.os.tag) {