diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 35201ea8a..4789d7ed1 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -23,6 +23,7 @@ const CoreSurface = @import("../../Surface.zig"); const build_options = @import("build_options"); +const cgroup = @import("cgroup.zig"); const Surface = @import("Surface.zig"); const Window = @import("Window.zig"); const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig"); @@ -373,9 +374,27 @@ pub fn wakeup(self: App) void { /// Run the event loop. This doesn't return until the app exits. pub fn run(self: *App) !void { + // Running will be false when we're not the primary instance and should + // exit (GTK single instance mode). If we're not running, we're done + // right away. if (!self.running) return; - // If we're not remote, then we also setup our actions and menus. + // If we are running, then we proceed to setup our app. + + // Setup our cgroup configurations for our tabs. + if (cgroup.init(self)) |cgroup_path| { + self.core_app.alloc.free(cgroup_path); + } else |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}, + ); + } + + // Setup our menu items self.initActions(); self.initMenu(); diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig new file mode 100644 index 000000000..861b1e9cb --- /dev/null +++ b/src/apprt/gtk/cgroup.zig @@ -0,0 +1,129 @@ +/// Contains all the logic for putting the Ghostty process and +/// each individual surface into its own cgroup. +const std = @import("std"); +const assert = std.debug.assert; +const Allocator = std.mem.Allocator; +const c = @import("c.zig"); +const App = @import("App.zig"); +const internal_os = @import("../../os/main.zig"); + +const log = std.log.scoped(.gtk_systemd_cgroup); + +/// 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. +pub fn init(app: *App) ![]const u8 { + const pid = std.os.linux.getpid(); + const alloc = app.core_app.alloc; + const connection = c.g_application_get_dbus_connection(@ptrCast(app.app)) orelse + return error.DbusConnectionRequired; + + // 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.linux.cgroupPath( + alloc, + pid, + ) orelse ""; + defer alloc.free(original); + + // 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(connection); + const transient = transient: while (true) { + const current = try internal_os.linux.cgroupPath( + alloc, + pid, + ) orelse ""; + if (!std.mem.eql(u8, original, current)) break :transient current; + std.time.sleep(25 * std.time.ns_per_ms); + }; + errdefer alloc.free(transient); + log.info("transient scope created cgroup={s}", .{transient}); + + return transient; +} + +/// Create a transient systemd scope unit for the current process. +/// +/// On success this will return the name of the transient scope +/// cgroup prefix, allocated with the given allocator. +fn createScope(connection: *c.GDBusConnection) !void { + // Our pid that we will move into the cgroup + const pid: c.guint32 = @intCast(std.os.linux.getpid()); + + // 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; + + // Initialize our builder to build up our parameters + var builder: c.GVariantBuilder = undefined; + c.g_variant_builder_init(&builder, c.G_VARIANT_TYPE("(ssa(sv)a(sa(sv)))")); + c.g_variant_builder_add(&builder, "s", name.ptr); + c.g_variant_builder_add(&builder, "s", "fail"); + { + // Properties + c.g_variant_builder_open(&builder, c.G_VARIANT_TYPE("a(sv)")); + defer c.g_variant_builder_close(&builder); + + // https://www.freedesktop.org/software/systemd/man/latest/systemd-oomd.service.html + c.g_variant_builder_add( + &builder, + "(sv)", + "ManagedOOMMemoryPressure", + c.g_variant_new_string("kill"), + ); + + // Delegate + c.g_variant_builder_add( + &builder, + "(sv)", + "Delegate", + c.g_variant_new_boolean(1), + ); + + // Pid to move into the unit + c.g_variant_builder_add( + &builder, + "(sv)", + "PIDs", + c.g_variant_new_fixed_array( + c.G_VARIANT_TYPE("u"), + &pid, + 1, + @sizeOf(c.guint32), + ), + ); + } + { + // Aux + c.g_variant_builder_open(&builder, c.G_VARIANT_TYPE("a(sa(sv))")); + defer c.g_variant_builder_close(&builder); + } + + var err: ?*c.GError = null; + defer if (err) |e| c.g_error_free(e); + _ = c.g_dbus_connection_call_sync( + connection, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", + "org.freedesktop.systemd1.Manager", + "StartTransientUnit", + c.g_variant_builder_end(&builder), + c.G_VARIANT_TYPE("(o)"), + c.G_DBUS_CALL_FLAGS_NONE, + -1, + null, + &err, + ) orelse { + if (err) |e| log.err( + "creating transient cgroup scope failed err={s}", + .{e.message}, + ); + return error.DbusCallFailed; + }; +}