/// 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 gio = @import("gio"); const glib = @import("glib"); const gobject = @import("gobject"); const App = @import("App.zig"); const internal_os = @import("../../os/main.zig"); const log = std.log.scoped(.gtk_systemd_cgroup); pub const Options = struct { memory_high: ?u64 = null, pids_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(); // 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); // 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.time.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; } /// 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.ArrayList(u8).init(alloc); defer builder.deinit(); // Controllers are space-separated var it = std.mem.splitScalar(u8, raw, ' '); while (it.next()) |controller| { try builder.append('+'); try builder.appendSlice(controller); if (it.rest().len > 0) try builder.append(' '); } // Enable them all try internal_os.cgroup.configureControllers( cgroup, builder.items, ); } /// Create a transient systemd scope unit for the current process and /// move our process into it. 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. var name_buf: [256]u8 = undefined; const name = std.fmt.bufPrintZ( &name_buf, "app-ghostty-transient-{}.scope", .{pid}, ) catch unreachable; const builder_type = glib.VariantType.new("(ssa(sv)a(sa(sv)))"); defer glib.free(builder_type); // Initialize our builder to build up our parameters var builder: glib.VariantBuilder = undefined; builder.init(builder_type); builder.add("s", name.ptr); builder.add("s", "fail"); { // Properties const properties_type = glib.VariantType.new("a(sv)"); defer glib.free(properties_type); builder.open(properties_type); defer builder.close(); // https://www.freedesktop.org/software/systemd/man/latest/systemd-oomd.service.html const pressure_value = 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 const pids_value_type = glib.VariantType.new("u"); defer glib.free(pids_value_type); const pids_value = glib.Variant.newFixedArray(pids_value_type, &pid, 1, @sizeOf(u32)); builder.add("(sv)", "PIDs", pids_value); } { // Aux const aux_type = glib.VariantType.new("a(sa(sv))"); defer glib.free(aux_type); builder.open(aux_type); defer builder.close(); } var err: ?*glib.Error = null; defer if (err) |e| e.free(); const reply_type = glib.VariantType.new("(o)"); defer glib.free(reply_type); const value = builder.end(); const reply = dbus.callSync( "org.freedesktop.systemd1", "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager", "StartTransientUnit", value, reply_type, .{}, -1, null, &err, ) orelse { if (err) |e| log.err( "creating transient cgroup scope failed code={} err={s}", .{ e.f_code, if (e.f_message) |msg| msg else "(no message)", }, ); return error.DbusCallFailed; }; defer reply.unref(); }