ghostty/src/apprt/gtk/cgroup.zig
2024-06-04 21:02:25 -07:00

174 lines
5.8 KiB
Zig

/// 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.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(connection);
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;
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.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.
// 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;
}
/// 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.
///
/// 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;
};
}