mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-19 10:16:12 +03:00
apprt/gtk-ng: cgroup base setup
This commit is contained in:
213
src/apprt/gtk-ng/cgroup.zig
Normal file
213
src/apprt/gtk-ng/cgroup.zig
Normal file
@ -0,0 +1,213 @@
|
||||
/// 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();
|
||||
}
|
@ -8,6 +8,7 @@ const gobject = @import("gobject");
|
||||
|
||||
const build_config = @import("../../../build_config.zig");
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const cgroup = @import("../cgroup.zig");
|
||||
const CoreApp = @import("../../../App.zig");
|
||||
const configpkg = @import("../../../config.zig");
|
||||
const Config = configpkg.Config;
|
||||
@ -50,6 +51,11 @@ pub const GhosttyApplication = extern struct {
|
||||
/// The configuration for the application.
|
||||
config: *Config,
|
||||
|
||||
/// 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 false internally when the event loop
|
||||
/// should exit and the application should quit. This must
|
||||
/// only be set by the main loop thread.
|
||||
@ -131,6 +137,7 @@ pub const GhosttyApplication = extern struct {
|
||||
const priv = self.private();
|
||||
priv.config.deinit();
|
||||
alloc.destroy(priv.config);
|
||||
if (priv.transient_cgroup_base) |base| alloc.free(base);
|
||||
}
|
||||
|
||||
/// Run the application. This is a replacement for `gio.Application.run`
|
||||
@ -253,12 +260,20 @@ pub const GhosttyApplication = extern struct {
|
||||
// Setup our style manager (light/dark mode)
|
||||
self.startupStyleManager();
|
||||
|
||||
// Setup our cgroup for the application.
|
||||
self.startupCgroup() catch {
|
||||
log.warn("TODO", .{});
|
||||
};
|
||||
|
||||
gio.Application.virtual_methods.startup.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
);
|
||||
}
|
||||
|
||||
/// Setup the style manager on startup. The primary task here is to
|
||||
/// setup our initial light/dark mode based on the configuration and
|
||||
/// setup listeners for changes to the style manager.
|
||||
fn startupStyleManager(self: *GhosttyApplication) void {
|
||||
const priv = self.private();
|
||||
const config = priv.config;
|
||||
@ -288,6 +303,72 @@ pub const GhosttyApplication = 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: *GhosttyApplication) CgroupError!void {
|
||||
const priv = self.private();
|
||||
const config = priv.config;
|
||||
|
||||
// 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: *GhosttyApplication) callconv(.C) void {
|
||||
// This is called when the application is activated, but we
|
||||
// don't need to do anything here since we handle activation
|
||||
|
Reference in New Issue
Block a user