From 0a5f3fa0a4eebde09c0a7b37841163d812dff5d0 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 4 Jun 2024 15:15:11 -0700 Subject: [PATCH 01/16] os: add linux API for getting cgroup by pid --- src/os/linux.zig | 28 ++++++++++++++++++++++++++++ src/os/main.zig | 1 + 2 files changed, 29 insertions(+) create mode 100644 src/os/linux.zig diff --git a/src/os/linux.zig b/src/os/linux.zig new file mode 100644 index 000000000..e399883dc --- /dev/null +++ b/src/os/linux.zig @@ -0,0 +1,28 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +/// Returns the path to the cgroup for the given pid. +pub fn cgroupPath(alloc: Allocator, pid: std.os.linux.pid_t) !?[]const u8 { + var 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, .{}); + defer file.close(); + + // Read it all into memory -- we don't expect this file to ever be that large. + var buf_reader = std.io.bufferedReader(file.reader()); + const contents = try buf_reader.reader().readAllAlloc( + alloc, + 1 * 1024 * 1024, // 1MB + ); + defer alloc.free(contents); + + // 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); +} diff --git a/src/os/main.zig b/src/os/main.zig index 1782601e0..e3c3ef595 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -13,6 +13,7 @@ pub usingnamespace @import("open.zig"); pub usingnamespace @import("pipe.zig"); pub usingnamespace @import("resourcesdir.zig"); pub const TempDir = @import("TempDir.zig"); +pub const linux = @import("linux.zig"); pub const passwd = @import("passwd.zig"); pub const xdg = @import("xdg.zig"); pub const windows = @import("windows.zig"); From 409c958b7ee5828d408d07ab3f7acd10dbd0e5b1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 4 Jun 2024 18:59:44 -0700 Subject: [PATCH 02/16] apprt/gtk: cgroup initialization --- src/apprt/gtk/App.zig | 21 ++++++- src/apprt/gtk/cgroup.zig | 129 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 src/apprt/gtk/cgroup.zig 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; + }; +} From c0b061edd915a009fa898cba5618c7603a4a81d7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 4 Jun 2024 19:23:18 -0700 Subject: [PATCH 03/16] os: API for listing cgroup controllers --- src/apprt/gtk/cgroup.zig | 27 +++++++++++++++++++++++++++ src/os/linux.zig | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig index 861b1e9cb..15673159f 100644 --- a/src/apprt/gtk/cgroup.zig +++ b/src/apprt/gtk/cgroup.zig @@ -41,9 +41,36 @@ pub fn init(app: *App) ![]const u8 { errdefer alloc.free(transient); log.info("transient scope created cgroup={s}", .{transient}); + // 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); + return transient; } +/// Enable all the cgroup controllers for the given cgroup. +fn enableControllers(alloc: Allocator, cgroup: []const u8) !void { + const raw = try internal_os.linux.cgroupControllers(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(' '); + } + + // TODO + log.warn("enabling controllers={s}", .{builder.items}); +} + /// Create a transient systemd scope unit for the current process. /// /// On success this will return the name of the transient scope diff --git a/src/os/linux.zig b/src/os/linux.zig index e399883dc..c9d4d0ba8 100644 --- a/src/os/linux.zig +++ b/src/os/linux.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const assert = std.debug.assert; const Allocator = std.mem.Allocator; /// Returns the path to the cgroup for the given pid. @@ -26,3 +27,37 @@ pub fn cgroupPath(alloc: Allocator, pid: std.os.linux.pid_t) !?[]const u8 { const result = std.mem.trimRight(u8, contents[idx + 1 ..], " \r\n"); return try alloc.dupe(u8, result); } + +/// 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 cgroupControllers(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. + var buf_reader = std.io.bufferedReader(file.reader()); + const contents = try buf_reader.reader().readAllAlloc( + 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); +} From b5c4d2f60df15ccac36343c342f6f08e9b2683ad Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 4 Jun 2024 19:28:12 -0700 Subject: [PATCH 04/16] os: rename linux => cgroup --- src/apprt/gtk/cgroup.zig | 6 +++--- src/os/{linux.zig => cgroup.zig} | 4 ++-- src/os/main.zig | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) rename src/os/{linux.zig => cgroup.zig} (93%) diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig index 15673159f..d808d1484 100644 --- a/src/apprt/gtk/cgroup.zig +++ b/src/apprt/gtk/cgroup.zig @@ -20,7 +20,7 @@ pub fn init(app: *App) ![]const u8 { // 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( + const original = try internal_os.cgroup.current( alloc, pid, ) orelse ""; @@ -31,7 +31,7 @@ pub fn init(app: *App) ![]const u8 { // 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( + const current = try internal_os.cgroup.current( alloc, pid, ) orelse ""; @@ -52,7 +52,7 @@ pub fn init(app: *App) ![]const u8 { /// Enable all the cgroup controllers for the given cgroup. fn enableControllers(alloc: Allocator, cgroup: []const u8) !void { - const raw = try internal_os.linux.cgroupControllers(alloc, cgroup); + const raw = try internal_os.cgroup.controllers(alloc, cgroup); defer alloc.free(raw); // Build our string builder for enabling all controllers diff --git a/src/os/linux.zig b/src/os/cgroup.zig similarity index 93% rename from src/os/linux.zig rename to src/os/cgroup.zig index c9d4d0ba8..0a7cc541f 100644 --- a/src/os/linux.zig +++ b/src/os/cgroup.zig @@ -3,7 +3,7 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; /// Returns the path to the cgroup for the given pid. -pub fn cgroupPath(alloc: Allocator, pid: std.os.linux.pid_t) !?[]const u8 { +pub fn current(alloc: Allocator, pid: std.os.linux.pid_t) !?[]const u8 { var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; // Read our cgroup by opening /proc//cgroup and reading the first @@ -35,7 +35,7 @@ pub fn cgroupPath(alloc: Allocator, pid: std.os.linux.pid_t) !?[]const u8 { /// 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 cgroupControllers(alloc: Allocator, cgroup: []const u8) ![]const u8 { +pub fn controllers(alloc: Allocator, cgroup: []const u8) ![]const u8 { assert(cgroup[0] == '/'); var buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; diff --git a/src/os/main.zig b/src/os/main.zig index e3c3ef595..cee592027 100644 --- a/src/os/main.zig +++ b/src/os/main.zig @@ -13,7 +13,7 @@ pub usingnamespace @import("open.zig"); pub usingnamespace @import("pipe.zig"); pub usingnamespace @import("resourcesdir.zig"); pub const TempDir = @import("TempDir.zig"); -pub const linux = @import("linux.zig"); +pub const cgroup = @import("cgroup.zig"); pub const passwd = @import("passwd.zig"); pub const xdg = @import("xdg.zig"); pub const windows = @import("windows.zig"); From bbe525c964adb9fa9f18755c67cd3c8035874320 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 4 Jun 2024 19:36:48 -0700 Subject: [PATCH 05/16] os: API to configure cgroup controllers --- src/apprt/gtk/cgroup.zig | 7 +++++-- src/os/cgroup.zig | 22 ++++++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig index d808d1484..ecee3f149 100644 --- a/src/apprt/gtk/cgroup.zig +++ b/src/apprt/gtk/cgroup.zig @@ -67,8 +67,11 @@ fn enableControllers(alloc: Allocator, cgroup: []const u8) !void { if (it.rest().len > 0) try builder.append(' '); } - // TODO - log.warn("enabling controllers={s}", .{builder.items}); + // Enable them all + try internal_os.cgroup.configureControllers( + cgroup, + builder.items, + ); } /// Create a transient systemd scope unit for the current process. diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig index 0a7cc541f..2f4f5d884 100644 --- a/src/os/cgroup.zig +++ b/src/os/cgroup.zig @@ -61,3 +61,25 @@ pub fn controllers(alloc: Allocator, cgroup: []const u8) ![]const u8 { 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 + try file.writer().writeAll(v); +} From d351e801580e2271980ed6f33f5975f4b14d722c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 4 Jun 2024 20:22:17 -0700 Subject: [PATCH 06/16] os: cgroup create/move --- src/apprt/gtk/cgroup.zig | 5 +++++ src/os/cgroup.zig | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig index ecee3f149..1b621d792 100644 --- a/src/apprt/gtk/cgroup.zig +++ b/src/apprt/gtk/cgroup.zig @@ -41,6 +41,11 @@ pub fn init(app: *App) ![]const u8 { 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.scope", pid); + // 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. diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig index 2f4f5d884..baad0c070 100644 --- a/src/os/cgroup.zig +++ b/src/os/cgroup.zig @@ -28,6 +28,30 @@ pub fn current(alloc: Allocator, pid: std.os.linux.pid_t) !?[]const u8 { 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(); + try file.writer().print("{}", .{pid}); + } +} + /// Returns all available cgroup controllers for the given cgroup. /// The cgroup should have a '/'-prefix. /// From 01bfce098149bb40c9fe92bee3eee16cff50c4fb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 4 Jun 2024 21:02:25 -0700 Subject: [PATCH 07/16] os: cgroup can set memory limits --- src/apprt/gtk/cgroup.zig | 9 +++++++++ src/os/cgroup.zig | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig index 1b621d792..47edd05bc 100644 --- a/src/apprt/gtk/cgroup.zig +++ b/src/apprt/gtk/cgroup.zig @@ -52,6 +52,15 @@ pub fn init(app: *App) ![]const u8 { // I don't know a scenario where this fails yet. try enableControllers(alloc, transient); + // 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. + // try internal_os.cgroup.configureMemoryLimit(transient, .{ + // // 1GB + // .high = 1 * 1024 * 1024 * 1024, + // }); + return transient; } diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig index baad0c070..8e666e89d 100644 --- a/src/os/cgroup.zig +++ b/src/os/cgroup.zig @@ -107,3 +107,31 @@ pub fn configureControllers( // Write try file.writer().writeAll(v); } + +pub const MemoryLimit = union(enum) { + /// memory.high + high: usize, +}; + +/// Configure the memory limit for the given cgroup. Use the various +/// fields in MemoryLimit to configure a specific type of limit. +pub fn configureMemoryLimit(cgroup: []const u8, limit: MemoryLimit) !void { + assert(cgroup[0] == '/'); + + const filename, const size = switch (limit) { + .high => |v| .{ "memory.high", 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 + try file.writer().print("{}", .{size}); +} From 1285b4f243ac4894b44a10eee734c3373b32c1d5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 4 Jun 2024 21:10:42 -0700 Subject: [PATCH 08/16] apprt/gtk: store transient cgroup --- src/apprt/gtk/App.zig | 10 ++++++++-- src/apprt/gtk/cgroup.zig | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 4789d7ed1..52ab5af8a 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -62,6 +62,11 @@ running: bool = true, /// Xkb state (X11 only). Will be null on Wayland. x11_xkb: ?x11.Xkb = null, +/// 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, + pub fn init(core_app: *CoreApp, opts: Options) !App { _ = opts; @@ -281,6 +286,7 @@ pub fn terminate(self: *App) void { if (self.cursor_none) |cursor| c.g_object_unref(cursor); if (self.menu) |menu| c.g_object_unref(menu); + if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path); self.config.deinit(); } @@ -381,9 +387,9 @@ pub fn run(self: *App) !void { // If we are running, then we proceed to setup our app. - // Setup our cgroup configurations for our tabs. + // Setup our cgroup configurations for our surfaces. if (cgroup.init(self)) |cgroup_path| { - self.core_app.alloc.free(cgroup_path); + self.transient_cgroup_base = 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. diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig index 47edd05bc..e081e51a1 100644 --- a/src/apprt/gtk/cgroup.zig +++ b/src/apprt/gtk/cgroup.zig @@ -36,6 +36,7 @@ pub fn init(app: *App) ![]const u8 { 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); From dc51b8269c23db663b1150b11e63646ac719b074 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 4 Jun 2024 21:37:34 -0700 Subject: [PATCH 09/16] plumb the linux cgroup through to termio --- src/Surface.zig | 10 +++++++++ src/apprt/gtk/Surface.zig | 44 ++++++++++++++++++++++++++++++++++++++- src/termio/Exec.zig | 9 +++++++- src/termio/Options.zig | 8 +++++++ 4 files changed, 69 insertions(+), 2 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 39c9b460e..2673b9722 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -404,6 +404,16 @@ pub fn init( .renderer_wakeup = render_thread.wakeup, .renderer_mailbox = render_thread.mailbox, .surface_mailbox = .{ .surface = self, .app = app_mailbox }, + + // 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 + termio.Options.linux_cgroup_default, }); errdefer io.deinit(); diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 794e2eaf4..f3543ed0a 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -11,6 +11,7 @@ const font = @import("../../font/main.zig"); const input = @import("../../input.zig"); const terminal = @import("../../terminal/main.zig"); const CoreSurface = @import("../../Surface.zig"); +const internal_os = @import("../../os/main.zig"); const App = @import("App.zig"); const Split = @import("Split.zig"); @@ -255,6 +256,10 @@ im_commit_buffered: bool = false, im_buf: [128]u8 = undefined, im_len: u7 = 0, +/// The surface-specific cgroup path. See App.transient_cgroup_path for +/// details on what this is. +cgroup_path: ?[]const u8 = null, + pub fn create(alloc: Allocator, app: *App, opts: Options) !*Surface { var surface = try alloc.create(Surface); errdefer alloc.destroy(surface); @@ -342,6 +347,36 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { break :font_size parent.font_size; }; + // If the parent has a transient cgroup, then we're creating cgroups + // for each surface if we can. We need to create a child cgroup. + const cgroup_path: ?[]const u8 = cgroup: { + const base = app.transient_cgroup_base orelse break :cgroup null; + + // 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, + "surface({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.err("failed to create surface cgroup err={}", .{err}); + break :cgroup null; + }; + + // Success, save the cgroup path. + break :cgroup std.fmt.allocPrint( + app.core_app.alloc, + "{s}/{s}", + .{ base, name }, + ) catch null; + }; + errdefer if (cgroup_path) |path| app.core_app.alloc.free(path); + // Build our result self.* = .{ .app = app, @@ -354,6 +389,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { .size = .{ .width = 800, .height = 600 }, .cursor_pos = .{ .x = 0, .y = 0 }, .im_context = im_context, + .cgroup_path = cgroup_path, }; errdefer self.* = undefined; @@ -442,9 +478,10 @@ pub fn deinit(self: *Surface) void { self.core_surface.deinit(); self.core_surface = undefined; + if (self.cgroup_path) |path| self.app.core_app.alloc.free(path); + // Free all our GTK stuff c.g_object_unref(self.im_context); - if (self.cursor) |cursor| c.g_object_unref(cursor); } @@ -463,6 +500,11 @@ fn render(self: *Surface) !void { try self.core_surface.renderer.drawFrame(self); } +/// Called by core surface to get the cgroup. +pub fn cgroup(self: *const Surface) ?[]const u8 { + return self.cgroup_path; +} + /// Queue the inspector to render if we have one. pub fn queueInspectorRender(self: *Surface) void { if (self.inspector) |v| v.queueRender(); diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 1287600eb..72041cb22 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -179,7 +179,14 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { term.width_px = subprocess.screen_size.width; term.height_px = subprocess.screen_size.height; - return Exec{ + // TODO: make work + if (comptime builtin.os.tag == .linux) { + if (opts.linux_cgroup) |cgroup| { + log.warn("DESIRED cgroup={s}", .{cgroup}); + } + } + + return .{ .alloc = alloc, .terminal = term, .subprocess = subprocess, diff --git a/src/termio/Options.zig b/src/termio/Options.zig index 1fd9d034a..079828f38 100644 --- a/src/termio/Options.zig +++ b/src/termio/Options.zig @@ -1,5 +1,6 @@ //! The options that are used to configure a terminal IO implementation. +const builtin = @import("builtin"); const xev = @import("xev"); const apprt = @import("../apprt.zig"); const renderer = @import("../renderer.zig"); @@ -41,3 +42,10 @@ renderer_mailbox: *renderer.Thread.Mailbox, /// The mailbox for sending the surface messages. surface_mailbox: apprt.surface.Mailbox, + +/// The cgroup to apply to the started termio process, if able by +/// the termio implementation. This only applies to Linux. +linux_cgroup: LinuxCgroup = linux_cgroup_default, + +pub const LinuxCgroup = if (builtin.os.tag == .linux) ?[]const u8 else void; +pub const linux_cgroup_default = if (LinuxCgroup == void) {} else null; From a63c8d09130af40cc8c30df3cb656f945ca902cc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 5 Jun 2024 09:25:06 -0700 Subject: [PATCH 10/16] termio: plumb a lot more to get ready to move into cgroup --- src/apprt/gtk/cgroup.zig | 4 ++-- src/termio/Exec.zig | 44 ++++++++++++++++++++++++++++++---------- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig index e081e51a1..86cd48b3d 100644 --- a/src/apprt/gtk/cgroup.zig +++ b/src/apprt/gtk/cgroup.zig @@ -166,8 +166,8 @@ fn createScope(connection: *c.GDBusConnection) !void { &err, ) orelse { if (err) |e| log.err( - "creating transient cgroup scope failed err={s}", - .{e.message}, + "creating transient cgroup scope failed code={} err={s}", + .{ e.code, e.message }, ); return error.DbusCallFailed; }; diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 72041cb22..0809a4b9b 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -179,13 +179,6 @@ pub fn init(alloc: Allocator, opts: termio.Options) !Exec { term.width_px = subprocess.screen_size.width; term.height_px = subprocess.screen_size.height; - // TODO: make work - if (comptime builtin.os.tag == .linux) { - if (opts.linux_cgroup) |cgroup| { - log.warn("DESIRED cgroup={s}", .{cgroup}); - } - } - return .{ .alloc = alloc, .terminal = term, @@ -904,6 +897,7 @@ const Subprocess = struct { pty: ?Pty = null, command: ?Command = null, flatpak_command: ?FlatpakHostCommand = null, + linux_cgroup: termio.Options.LinuxCgroup = termio.Options.linux_cgroup_default, /// Initialize the subprocess. This will NOT start it, this only sets /// up the internal state necessary to start it later. @@ -1200,6 +1194,15 @@ const Subprocess = struct { else null; + // If we have a cgroup, then we copy that into our arena so the + // memory remains valid when we start. + const linux_cgroup: termio.Options.LinuxCgroup = cgroup: { + const default = termio.Options.linux_cgroup_default; + if (comptime builtin.os.tag != .linux) break :cgroup default; + const path = opts.linux_cgroup orelse break :cgroup default; + break :cgroup try alloc.dupe(u8, path); + }; + // Our screen size should be our padded size const padded_size = opts.screen_size.subPadding(opts.padding); @@ -1210,6 +1213,7 @@ const Subprocess = struct { .args = args, .grid_size = opts.grid_size, .screen_size = padded_size, + .linux_cgroup = linux_cgroup, }; } @@ -1303,12 +1307,14 @@ const Subprocess = struct { .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 p = cmd.getData(Pty) orelse unreachable; - p.childPreExec() catch |err| - log.err("error initializing child: {}", .{err}); + const sp = cmd.getData(Subprocess) orelse unreachable; + sp.childPreExec() catch |err| log.err( + "error initializing child: {}", + .{err}, + ); } }).callback, - .data = &self.pty.?, + .data = self, }; try cmd.start(alloc); errdefer killCommand(&cmd) catch |err| { @@ -1330,6 +1336,22 @@ const Subprocess = struct { }; } + /// This should be called after fork but before exec in the child process. + /// To repeat: this function RUNS IN THE FORKED CHILD PROCESS before + /// exec is called; it does NOT run in the main Ghostty process. + fn childPreExec(self: *Subprocess) !void { + // Setup our pty + try self.pty.?.childPreExec(); + + // If we have a cgroup set, then we want to move into that cgroup. + if (comptime builtin.os.tag == .linux) { + if (self.linux_cgroup) |cgroup| { + // TODO: do it + _ = cgroup; + } + } + } + /// Called to notify that we exited externally so we can unset our /// running state. pub fn externalExit(self: *Subprocess) void { From 7d9da342597811e463c5407b94177a0d0d4061dd Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 5 Jun 2024 09:30:21 -0700 Subject: [PATCH 11/16] termio/exec: move subprocess into cgroup --- src/os/cgroup.zig | 12 ++++++++++++ src/termio/Exec.zig | 3 +-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/os/cgroup.zig b/src/os/cgroup.zig index 8e666e89d..aee772954 100644 --- a/src/os/cgroup.zig +++ b/src/os/cgroup.zig @@ -52,6 +52,18 @@ pub fn create( } } +/// 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}); +} + /// Returns all available cgroup controllers for the given cgroup. /// The cgroup should have a '/'-prefix. /// diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 0809a4b9b..1978659dd 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1346,8 +1346,7 @@ const Subprocess = struct { // If we have a cgroup set, then we want to move into that cgroup. if (comptime builtin.os.tag == .linux) { if (self.linux_cgroup) |cgroup| { - // TODO: do it - _ = cgroup; + try internal_os.cgroup.moveInto(cgroup, 0); } } } From ad5d44af10197de587f123179dcf62d6a387e05d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 5 Jun 2024 09:43:57 -0700 Subject: [PATCH 12/16] config: control cgroup isolation --- src/apprt/gtk/App.zig | 34 +++++++++++++++++++++++----------- src/build/fish_completions.zig | 2 +- src/config/Config.zig | 31 +++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 52ab5af8a..6cda51475 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -44,6 +44,9 @@ config: Config, app: *c.GtkApplication, ctx: *c.GMainContext, +/// True if the app was launched with single instance mode. +single_instance: bool, + /// The "none" cursor. We use one that is shared across the entire app. cursor_none: ?*c.GdkCursor, @@ -269,6 +272,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App { .ctx = ctx, .cursor_none = cursor_none, .x11_xkb = x11_xkb, + .single_instance = single_instance, // If we are NOT the primary instance, then we never want to run. // This means that another instance of the GTK app is running and // our "activate" call above will open a window. @@ -388,17 +392,25 @@ pub fn run(self: *App) !void { // If we are running, then we proceed to setup our app. // Setup our cgroup configurations for our surfaces. - if (cgroup.init(self)) |cgroup_path| { - self.transient_cgroup_base = 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}, - ); - } + if (switch (self.config.@"linux-cgroup") { + .never => false, + .always => true, + .@"single-instance" => self.single_instance, + }) cgroup: { + const path = cgroup.init(self) 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}, + ); + break :cgroup; + }; + + log.info("cgroup isolation enabled base={s}", .{path}); + self.transient_cgroup_base = path; + } else log.debug("cgroup isoation disabled config={}", .{self.config.@"linux-cgroup"}); // Setup our menu items self.initActions(); diff --git a/src/build/fish_completions.zig b/src/build/fish_completions.zig index b72360e7d..3a006a7b2 100644 --- a/src/build/fish_completions.zig +++ b/src/build/fish_completions.zig @@ -12,7 +12,7 @@ pub const fish_completions = comptimeGenerateFishCompletions(); fn comptimeGenerateFishCompletions() []const u8 { comptime { - @setEvalBranchQuota(13000); + @setEvalBranchQuota(15000); var counter = std.io.countingWriter(std.io.null_writer); try writeFishCompletions(&counter.writer()); diff --git a/src/config/Config.zig b/src/config/Config.zig index c17d1e42d..5b7370e37 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -988,6 +988,30 @@ keybind: Keybinds = .{}, /// This does not work with GLFW builds. @"macos-option-as-alt": OptionAsAlt = .false, +/// Put every surface (tab, split, window) into a dedicated Linux cgroup. +/// +/// 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 will cause startup times to be slower (a hundred milliseconds or so), +/// so the default value is "single-instance." In single-instance mode, only +/// one instance of Ghostty is running (see gtk-single-instance) so the startup +/// time is a one-time cost. Additionally, single instance Ghostty is much +/// more likely to have many windows, tabs, etc. so cgroup isolation is a +/// big benefit. +/// +/// Valid values are: +/// +/// * `never` - Never use cgroups. +/// * `always` - Always use cgroups. +/// * `single-instance` - Enable cgroups only for Ghostty instances launched +/// as single-instance applications (see gtk-single-instance). +/// +@"linux-cgroup": LinuxCgroup = .@"single-instance", + /// If true, the Ghostty GTK application will run in single-instance mode: /// each new `ghostty` process launched will result in a new window if there /// is already a running process. @@ -3495,3 +3519,10 @@ pub const GraphemeWidthMethod = enum { legacy, unicode, }; + +/// See linux-cgroup +pub const LinuxCgroup = enum { + never, + always, + @"single-instance", +}; From 3b41b89c23435796b1d261fae2ee6e58dab795fc Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 5 Jun 2024 09:47:48 -0700 Subject: [PATCH 13/16] apprt/gtk: config to hard fail on cgroup init --- src/apprt/gtk/App.zig | 7 +++++++ src/config/Config.zig | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 6cda51475..7575c547e 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -405,6 +405,13 @@ pub fn run(self: *App) !void { "failed to initialize cgroups, terminals will not be isolated err={}", .{err}, ); + + // If we have hard fail enabled then we exit now. + if (self.config.@"linux-cgroup-hard-fail") { + log.err("linux-cgroup-hard-fail enabled, exiting", .{}); + return error.CgroupInitFailed; + } + break :cgroup; }; diff --git a/src/config/Config.zig b/src/config/Config.zig index 5b7370e37..b6e2d7342 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1012,6 +1012,19 @@ keybind: Keybinds = .{}, /// @"linux-cgroup": LinuxCgroup = .@"single-instance", +/// 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 true, then any cgroup initialization failure will cause +/// Ghostty to exit or new surfaces to not be created. +/// +/// Note: this currently only affects cgroup initialization. Subprocesses +/// must always be able to move themselves into an isolated cgroup. +@"linux-cgroup-hard-fail": bool = false, + /// If true, the Ghostty GTK application will run in single-instance mode: /// each new `ghostty` process launched will result in a new window if there /// is already a running process. From 99d567274b2ea725b5d1de2d2aaf77dca0522246 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 5 Jun 2024 10:27:53 -0700 Subject: [PATCH 14/16] config: add cgroup memory limit config --- src/apprt/gtk/cgroup.zig | 9 +++++---- src/config/Config.zig | 9 +++++++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig index 86cd48b3d..fdb04de6e 100644 --- a/src/apprt/gtk/cgroup.zig +++ b/src/apprt/gtk/cgroup.zig @@ -57,10 +57,11 @@ pub fn init(app: *App) ![]const u8 { // 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. - // try internal_os.cgroup.configureMemoryLimit(transient, .{ - // // 1GB - // .high = 1 * 1024 * 1024 * 1024, - // }); + if (app.config.@"linux-cgroup-memory-limit") |limit| { + try internal_os.cgroup.configureMemoryLimit(transient, .{ + .high = limit, + }); + } return transient; } diff --git a/src/config/Config.zig b/src/config/Config.zig index b6e2d7342..2cc7d52ef 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1012,6 +1012,15 @@ keybind: Keybinds = .{}, /// @"linux-cgroup": LinuxCgroup = .@"single-instance", +/// 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 +/// pressure. +@"linux-cgroup-memory-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 From cc6d8bfbfd1b4a673040fb1c3691b55a346be557 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 5 Jun 2024 10:29:01 -0700 Subject: [PATCH 15/16] config: clarify some config --- src/config/Config.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/config/Config.zig b/src/config/Config.zig index 2cc7d52ef..fdaa1d0c4 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -1003,6 +1003,10 @@ 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). +/// /// Valid values are: /// /// * `never` - Never use cgroups. From dda6a22ea968af5414b4fc2e854dbd50bb3183ca Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 5 Jun 2024 10:42:43 -0700 Subject: [PATCH 16/16] apprt/gtk: cgroup hierarchy only affects surfaces --- src/apprt/gtk/Surface.zig | 2 +- src/apprt/gtk/cgroup.zig | 12 ++++++++++-- src/termio/Exec.zig | 3 +++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index f3543ed0a..d696d8b38 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -358,7 +358,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void { var buf: [256]u8 = undefined; const name = std.fmt.bufPrint( &buf, - "surface({X}).scope", + "surfaces/{X}.service", .{@intFromPtr(self)}, ) catch unreachable; diff --git a/src/apprt/gtk/cgroup.zig b/src/apprt/gtk/cgroup.zig index fdb04de6e..b936d83a1 100644 --- a/src/apprt/gtk/cgroup.zig +++ b/src/apprt/gtk/cgroup.zig @@ -45,20 +45,28 @@ pub fn init(app: *App) ![]const u8 { // 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.scope", pid); + 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 (app.config.@"linux-cgroup-memory-limit") |limit| { - try internal_os.cgroup.configureMemoryLimit(transient, .{ + try internal_os.cgroup.configureMemoryLimit(surfaces, .{ .high = limit, }); } diff --git a/src/termio/Exec.zig b/src/termio/Exec.zig index 1978659dd..d6342deb3 100644 --- a/src/termio/Exec.zig +++ b/src/termio/Exec.zig @@ -1321,6 +1321,9 @@ 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.command = cmd; return switch (builtin.os.tag) {