mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-23 15:55:19 +00:00
gtk: revamp cgroup/transient scope handling
This changes the way Ghostty assigns itself and subprocesses to cgroups and how resource controls are applied. * Ghostty itself no longer modifies it's own cgroup or moves itself to a transient scope. To modify the main Ghostty process' resource controls ensure that you're launching Ghostty with a systemd unit and use the standard systemd methods for overriding and applying changes to systemd units. * If configured (on by default), the process used to run your command will be moved to a transient systemd scope after it is forked from Ghostty but before the user's command is executed. Resource controls will be applied to the transient scope at this time. Changes to the `linux-cgroup*` configuration entries will not alter existing commands. If changes are made to the `linux-cgroup*` configuration entries commands will need to be relaunched. Resource limits can also be modified after launch outside of Ghostty using systemd tooling. The transient scope name can be shown by running `systemctl --user whoami` in a shell running inside Ghostty. Fixes #2084. Related to #6669
This commit is contained in:
@@ -18,6 +18,7 @@ const Command = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const configpkg = @import("config.zig");
|
||||
const global_state = &@import("global.zig").state;
|
||||
const internal_os = @import("os/main.zig");
|
||||
const windows = internal_os.windows;
|
||||
@@ -30,8 +31,20 @@ const testing = std.testing;
|
||||
const Allocator = std.mem.Allocator;
|
||||
const File = std.fs.File;
|
||||
const EnvMap = std.process.EnvMap;
|
||||
const apprt = @import("apprt.zig");
|
||||
|
||||
const PreExecFn = fn (*Command) void;
|
||||
/// Function prototype for a function executed /in the child process/ after the
|
||||
/// fork, but before exec'ing the command. If the function returns a u8, the
|
||||
/// child process will be exited with that error code.
|
||||
const PreExecFn = fn (*Command) ?u8;
|
||||
|
||||
/// Allowable set of errors that can be returned by a post fork function. Any
|
||||
/// errors will result in the failure to create the surface.
|
||||
pub const PostForkError = error{PostForkError};
|
||||
|
||||
/// Function prototype for a function executed /in the parent process/
|
||||
/// after the fork.
|
||||
const PostForkFn = fn (*Command) PostForkError!void;
|
||||
|
||||
/// Path to the command to run. This doesn't have to be an absolute path,
|
||||
/// because use exec functions that search the PATH, if necessary.
|
||||
@@ -63,9 +76,25 @@ stderr: ?File = null,
|
||||
/// If set, this will be executed /in the child process/ after fork but
|
||||
/// before exec. This is useful to setup some state in the child before the
|
||||
/// exec process takes over, such as signal handlers, setsid, setuid, etc.
|
||||
pre_exec: ?*const PreExecFn = null,
|
||||
os_pre_exec: ?*const PreExecFn,
|
||||
|
||||
linux_cgroup: LinuxCgroup = linux_cgroup_default,
|
||||
/// If set, this will be executed /in the child process/ after fork but
|
||||
/// before exec. This is useful to setup some state in the child before the
|
||||
/// exec process takes over, such as signal handlers, setsid, setuid, etc.
|
||||
rt_pre_exec: ?*const PreExecFn,
|
||||
|
||||
/// Configuration information needed by the apprt pre exec function. Note
|
||||
/// that this should be a trivially copyable struct and not require any
|
||||
/// allocation/deallocation.
|
||||
rt_pre_exec_info: RtPreExecInfo,
|
||||
|
||||
/// If set, this will be executed in the /in the parent process/ after the fork.
|
||||
rt_post_fork: ?*const PostForkFn,
|
||||
|
||||
/// Configuration information needed by the apprt post fork function. Note
|
||||
/// that this should be a trivially copyable struct and not require any
|
||||
/// allocation/deallocation.
|
||||
rt_post_fork_info: RtPostForkInfo,
|
||||
|
||||
/// If set, then the process will be created attached to this pseudo console.
|
||||
/// `stdin`, `stdout`, and `stderr` will be ignored if set.
|
||||
@@ -79,11 +108,6 @@ data: ?*anyopaque = null,
|
||||
/// Process ID is set after start is called.
|
||||
pid: ?posix.pid_t = null,
|
||||
|
||||
/// LinuxCGroup type depends on our target OS
|
||||
pub const LinuxCgroup = if (builtin.os.tag == .linux) ?[]const u8 else void;
|
||||
pub const linux_cgroup_default = if (LinuxCgroup == void)
|
||||
{} else null;
|
||||
|
||||
/// The various methods a process may exit.
|
||||
pub const Exit = if (builtin.os.tag == .windows) union(enum) {
|
||||
Exited: u32,
|
||||
@@ -112,6 +136,24 @@ pub const Exit = if (builtin.os.tag == .windows) union(enum) {
|
||||
}
|
||||
};
|
||||
|
||||
/// Configuration information needed by the apprt pre exec function. Note
|
||||
/// that this should be a trivially copyable struct and not require any
|
||||
/// allocation/deallocation.
|
||||
pub const RtPreExecInfo = if (@hasDecl(apprt.runtime, "pre_exec")) apprt.runtime.pre_exec.PreExecInfo else struct {
|
||||
pub inline fn init(_: *const configpkg.Config) @This() {
|
||||
return .{};
|
||||
}
|
||||
};
|
||||
|
||||
/// Configuration information needed by the apprt post fork function. Note
|
||||
/// that this should be a trivially copyable struct and not require any
|
||||
/// allocation/deallocation.
|
||||
pub const RtPostForkInfo = if (@hasDecl(apprt.runtime, "post_fork")) apprt.runtime.post_fork.PostForkInfo else struct {
|
||||
pub inline fn init(_: *const configpkg.Config) @This() {
|
||||
return .{};
|
||||
}
|
||||
};
|
||||
|
||||
/// Start the subprocess. This returns immediately once the child is started.
|
||||
///
|
||||
/// After this is successful, self.pid is available.
|
||||
@@ -143,19 +185,13 @@ fn startPosix(self: *Command, arena: Allocator) !void {
|
||||
else
|
||||
@compileError("missing env vars");
|
||||
|
||||
// Fork. If we have a cgroup specified on Linxu then we use clone
|
||||
const pid: posix.pid_t = switch (builtin.os.tag) {
|
||||
.linux => if (self.linux_cgroup) |cgroup|
|
||||
try internal_os.cgroup.cloneInto(cgroup)
|
||||
else
|
||||
try posix.fork(),
|
||||
|
||||
else => try posix.fork(),
|
||||
};
|
||||
// Fork.
|
||||
const pid = try posix.fork();
|
||||
|
||||
if (pid != 0) {
|
||||
// Parent, return immediately.
|
||||
self.pid = @intCast(pid);
|
||||
if (self.rt_post_fork) |f| try f(self);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -182,8 +218,9 @@ fn startPosix(self: *Command, arena: Allocator) !void {
|
||||
// any failures are ignored (its best effort).
|
||||
global_state.rlimits.restore();
|
||||
|
||||
// If the user requested a pre exec callback, call it now.
|
||||
if (self.pre_exec) |f| f(self);
|
||||
// If there are pre exec callbacks, call them now.
|
||||
if (self.os_pre_exec) |f| if (f(self)) |exitcode| posix.exit(exitcode);
|
||||
if (self.rt_pre_exec) |f| if (f(self)) |exitcode| posix.exit(exitcode);
|
||||
|
||||
// Finally, replace our process.
|
||||
// Note: we must use the "p"-variant of exec here because we
|
||||
|
||||
@@ -636,16 +636,8 @@ pub fn init(
|
||||
.working_directory = config.@"working-directory",
|
||||
.resources_dir = global_state.resources_dir.host(),
|
||||
.term = config.term,
|
||||
|
||||
// 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
|
||||
Command.linux_cgroup_default,
|
||||
.rt_pre_exec_info = .init(config),
|
||||
.rt_post_fork_info = .init(config),
|
||||
});
|
||||
errdefer io_exec.deinit();
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ pub const resourcesDir = @import("gtk/flatpak.zig").resourcesDir;
|
||||
// The exported API, custom for the apprt.
|
||||
pub const class = @import("gtk/class.zig");
|
||||
pub const WeakRef = @import("gtk/weak_ref.zig").WeakRef;
|
||||
pub const pre_exec = @import("gtk/pre_exec.zig");
|
||||
pub const post_fork = @import("gtk/post_fork.zig");
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/// Contains all the logic for putting the Ghostty process and
|
||||
/// each individual surface into its own cgroup.
|
||||
/// Contains all the logic for putting individual surfaces into
|
||||
/// transient systemd scopes.
|
||||
const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = @import("../../quirks.zig").inlineAssert;
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
@@ -12,125 +13,27 @@ const log = std.log.scoped(.gtk_systemd_cgroup);
|
||||
|
||||
pub const Options = struct {
|
||||
memory_high: ?u64 = null,
|
||||
pids_max: ?u64 = null,
|
||||
tasks_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();
|
||||
pub fn fmtScope(buf: []u8, pid: u32) [:0]const u8 {
|
||||
const fmt = "app-ghostty-surface-transient-{}.scope";
|
||||
|
||||
// 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);
|
||||
assert(buf.len >= fmt.len - 2 + std.math.log10_int(@as(usize, std.math.maxInt(@TypeOf(pid)))) + 1);
|
||||
|
||||
// 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.Thread.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;
|
||||
return std.fmt.bufPrintZ(buf, fmt, .{pid}) catch unreachable;
|
||||
}
|
||||
|
||||
/// 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.Io.Writer.Allocating = .init(alloc);
|
||||
defer builder.deinit();
|
||||
|
||||
// Controllers are space-separated
|
||||
var it = std.mem.splitScalar(u8, raw, ' ');
|
||||
while (it.next()) |controller| {
|
||||
try builder.writer.writeByte('+');
|
||||
try builder.writer.writeAll(controller);
|
||||
if (it.rest().len > 0) try builder.writer.writeByte(' ');
|
||||
}
|
||||
|
||||
// Enable them all
|
||||
try internal_os.cgroup.configureControllers(
|
||||
cgroup,
|
||||
builder.written(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a transient systemd scope unit for the current process and
|
||||
/// move our process into it.
|
||||
fn createScope(
|
||||
/// Create a transient systemd scope unit for the given process and
|
||||
/// move the process into it.
|
||||
pub 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.
|
||||
pid: u32,
|
||||
options: Options,
|
||||
) error{DbusCallFailed}!void {
|
||||
// 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 name = fmtScope(&name_buf, pid);
|
||||
|
||||
const builder_type = glib.VariantType.new("(ssa(sv)a(sa(sv)))");
|
||||
defer glib.free(builder_type);
|
||||
@@ -150,16 +53,21 @@ fn createScope(
|
||||
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");
|
||||
if (options.memory_high) |value| {
|
||||
builder.add("(sv)", "MemoryHigh", glib.Variant.newUint64(value));
|
||||
}
|
||||
|
||||
builder.add("(sv)", "ManagedOOMMemoryPressure", pressure_value);
|
||||
if (options.tasks_max) |value| {
|
||||
builder.add("(sv)", "TasksMax", glib.Variant.newUint64(value));
|
||||
}
|
||||
|
||||
// https://www.freedesktop.org/software/systemd/man/latest/systemd-oomd.service.html
|
||||
builder.add("(sv)", "ManagedOOMMemoryPressure", glib.Variant.newString("kill"));
|
||||
|
||||
// Delegate
|
||||
const delegate_value = glib.Variant.newBoolean(1);
|
||||
builder.add("(sv)", "Delegate", delegate_value);
|
||||
builder.add("(sv)", "Delegate", glib.Variant.newBoolean(@intFromBool(true)));
|
||||
|
||||
// Pid to move into the unit
|
||||
// PID to move into the unit
|
||||
const pids_value_type = glib.VariantType.new("u");
|
||||
defer glib.free(pids_value_type);
|
||||
|
||||
@@ -169,7 +77,7 @@ fn createScope(
|
||||
}
|
||||
|
||||
{
|
||||
// Aux
|
||||
// Aux - unused but must be present
|
||||
const aux_type = glib.VariantType.new("a(sa(sv))");
|
||||
defer glib.free(aux_type);
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ const build_config = @import("../../../build_config.zig");
|
||||
const state = &@import("../../../global.zig").state;
|
||||
const i18n = @import("../../../os/main.zig").i18n;
|
||||
const apprt = @import("../../../apprt.zig");
|
||||
const cgroup = @import("../cgroup.zig");
|
||||
const CoreApp = @import("../../../App.zig");
|
||||
const configpkg = @import("../../../config.zig");
|
||||
const input = @import("../../../input.zig");
|
||||
@@ -176,11 +175,6 @@ pub const Application = extern struct {
|
||||
/// The global shortcut logic.
|
||||
global_shortcuts: *GlobalShortcuts,
|
||||
|
||||
/// 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 true so long as we request a window exactly
|
||||
/// once. This prevents quitting the app before we've shown one
|
||||
/// window.
|
||||
@@ -438,7 +432,6 @@ pub const Application = extern struct {
|
||||
priv.config.unref();
|
||||
priv.winproto.deinit(alloc);
|
||||
priv.global_shortcuts.unref();
|
||||
if (priv.transient_cgroup_base) |base| alloc.free(base);
|
||||
if (priv.saved_language) |language| alloc.free(language);
|
||||
if (gdk.Display.getDefault()) |display| {
|
||||
gtk.StyleContext.removeProviderForDisplay(
|
||||
@@ -809,11 +802,6 @@ pub const Application = extern struct {
|
||||
return &self.private().winproto;
|
||||
}
|
||||
|
||||
/// Returns the cgroup base (if any).
|
||||
pub fn cgroupBase(self: *Self) ?[]const u8 {
|
||||
return self.private().transient_cgroup_base;
|
||||
}
|
||||
|
||||
/// This will get called when there are no more open surfaces.
|
||||
fn startQuitTimer(self: *Self) void {
|
||||
const priv = self.private();
|
||||
@@ -1312,22 +1300,6 @@ pub const Application = extern struct {
|
||||
// Setup our global shortcuts
|
||||
self.startupGlobalShortcuts();
|
||||
|
||||
// Setup our cgroup for the application.
|
||||
self.startupCgroup() catch |err| {
|
||||
log.warn("cgroup initialization failed err={}", .{err});
|
||||
|
||||
// Add it to our config diagnostics so it shows up in a GUI dialog.
|
||||
// Admittedly this has two issues: (1) we shuldn't be using the
|
||||
// config errors dialog for this long term and (2) using a mut
|
||||
// ref to the config wouldn't propagate changes to UI properly,
|
||||
// but we're in startup mode so its okay.
|
||||
const config = self.private().config.getMut();
|
||||
config.addDiagnosticFmt(
|
||||
"cgroup initialization failed: {}",
|
||||
.{err},
|
||||
) catch {};
|
||||
};
|
||||
|
||||
// If we have any config diagnostics from loading, then we
|
||||
// show the diagnostics dialog. We show this one as a general
|
||||
// modal (not to any specific window) because we don't even
|
||||
@@ -1461,72 +1433,6 @@ pub const Application = 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: *Self) CgroupError!void {
|
||||
const priv = self.private();
|
||||
const config = priv.config.get();
|
||||
|
||||
// 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: *Self) callconv(.c) void {
|
||||
log.debug("activate", .{});
|
||||
|
||||
|
||||
@@ -551,10 +551,6 @@ pub const Surface = extern struct {
|
||||
/// The configuration that this surface is using.
|
||||
config: ?*Config = null,
|
||||
|
||||
/// The cgroup created for this surface. This will be created
|
||||
/// if `Application.transient_cgroup_base` is set.
|
||||
cgroup_path: ?[]const u8 = null,
|
||||
|
||||
/// The default size for a window that embeds this surface.
|
||||
default_size: ?*Size = null,
|
||||
|
||||
@@ -1433,63 +1429,6 @@ pub const Surface = extern struct {
|
||||
};
|
||||
}
|
||||
|
||||
/// Initialize the cgroup for this surface if it hasn't been
|
||||
/// already. While this is `init`-prefixed, we prefer to call this
|
||||
/// in the realize function because we don't need to create a cgroup
|
||||
/// if we don't init a surface.
|
||||
fn initCgroup(self: *Self) void {
|
||||
const priv = self.private();
|
||||
|
||||
// If we already have a cgroup path then we don't do it again.
|
||||
if (priv.cgroup_path != null) return;
|
||||
|
||||
const app = Application.default();
|
||||
const alloc = app.allocator();
|
||||
const base = app.cgroupBase() orelse return;
|
||||
|
||||
// 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,
|
||||
"surfaces/{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.warn("failed to create surface cgroup err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// Success, save the cgroup path.
|
||||
priv.cgroup_path = std.fmt.allocPrint(
|
||||
alloc,
|
||||
"{s}/{s}",
|
||||
.{ base, name },
|
||||
) catch null;
|
||||
}
|
||||
|
||||
/// Deletes the cgroup if set.
|
||||
fn clearCgroup(self: *Self) void {
|
||||
const priv = self.private();
|
||||
const path = priv.cgroup_path orelse return;
|
||||
|
||||
internal_os.cgroup.remove(path) catch |err| {
|
||||
// We don't want this to be fatal in any way so we just log
|
||||
// and continue. A dangling empty cgroup is not a big deal
|
||||
// and this should be rare.
|
||||
log.warn(
|
||||
"failed to remove cgroup for surface path={s} err={}",
|
||||
.{ path, err },
|
||||
);
|
||||
};
|
||||
|
||||
Application.default().allocator().free(path);
|
||||
priv.cgroup_path = null;
|
||||
}
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Libghostty Callbacks
|
||||
|
||||
@@ -1525,10 +1464,6 @@ pub const Surface = extern struct {
|
||||
return true;
|
||||
}
|
||||
|
||||
pub fn cgroupPath(self: *Self) ?[]const u8 {
|
||||
return self.private().cgroup_path;
|
||||
}
|
||||
|
||||
pub fn getContentScale(self: *Self) apprt.ContentScale {
|
||||
const priv = self.private();
|
||||
const gl_area = priv.gl_area;
|
||||
@@ -1968,8 +1903,6 @@ pub const Surface = extern struct {
|
||||
for (priv.key_tables.items) |s| alloc.free(s);
|
||||
priv.key_tables.deinit(alloc);
|
||||
|
||||
self.clearCgroup();
|
||||
|
||||
gobject.Object.virtual_methods.finalize.call(
|
||||
Class.parent,
|
||||
self.as(Parent),
|
||||
@@ -3331,10 +3264,6 @@ pub const Surface = extern struct {
|
||||
const app = Application.default();
|
||||
const alloc = app.allocator();
|
||||
|
||||
// Initialize our cgroup if we can.
|
||||
self.initCgroup();
|
||||
errdefer self.clearCgroup();
|
||||
|
||||
// Make our pointer to store our surface
|
||||
const surface = try alloc.create(CoreSurface);
|
||||
errdefer alloc.destroy(surface);
|
||||
|
||||
121
src/apprt/gtk/post_fork.zig
Normal file
121
src/apprt/gtk/post_fork.zig
Normal file
@@ -0,0 +1,121 @@
|
||||
const std = @import("std");
|
||||
|
||||
const gio = @import("gio");
|
||||
const glib = @import("glib");
|
||||
|
||||
const log = std.log.scoped(.gtk_post_fork);
|
||||
|
||||
const configpkg = @import("../../config.zig");
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
const Command = @import("../../Command.zig");
|
||||
const cgroup = @import("./cgroup.zig");
|
||||
|
||||
const Application = @import("class/application.zig").Application;
|
||||
|
||||
pub const PostForkInfo = struct {
|
||||
gtk_single_instance: configpkg.Config.GtkSingleInstance,
|
||||
linux_cgroup: configpkg.Config.LinuxCgroup,
|
||||
linux_cgroup_hard_fail: bool,
|
||||
linux_cgroup_memory_limit: ?u64,
|
||||
linux_cgroup_processes_limit: ?u64,
|
||||
|
||||
pub fn init(cfg: *const configpkg.Config) PostForkInfo {
|
||||
return .{
|
||||
.gtk_single_instance = cfg.@"gtk-single-instance",
|
||||
.linux_cgroup = cfg.@"linux-cgroup",
|
||||
.linux_cgroup_hard_fail = cfg.@"linux-cgroup-hard-fail",
|
||||
.linux_cgroup_memory_limit = cfg.@"linux-cgroup-memory-limit",
|
||||
.linux_cgroup_processes_limit = cfg.@"linux-cgroup-processes-limit",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// If we are configured to do so, tell `systemd` to move the new child PID into
|
||||
/// a transient `systemd` scope with the configured resource limits.
|
||||
///
|
||||
/// If we are configured to hard fail, log an error message and return an error
|
||||
/// code if we don't detect the move in time.
|
||||
pub fn postFork(cmd: *Command) Command.PostForkError!void {
|
||||
switch (cmd.rt_post_fork_info.linux_cgroup) {
|
||||
.always => {},
|
||||
.never => return,
|
||||
.@"single-instance" => switch (cmd.rt_post_fork_info.gtk_single_instance) {
|
||||
.true => {},
|
||||
.false => return,
|
||||
.detect => {
|
||||
log.err("gtk-single-instance is set to detect which should be impossible!", .{});
|
||||
return error.PostForkError;
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const pid: u32 = @intCast(cmd.pid orelse {
|
||||
log.err("PID of child not known!", .{});
|
||||
return error.PostForkError;
|
||||
});
|
||||
|
||||
var expected_cgroup_buf: [256]u8 = undefined;
|
||||
const expected_cgroup = cgroup.fmtScope(&expected_cgroup_buf, pid);
|
||||
|
||||
log.debug("beginning transition to transient systemd scope {s}", .{expected_cgroup});
|
||||
|
||||
const app = Application.default();
|
||||
|
||||
const dbus = app.as(gio.Application).getDbusConnection() orelse {
|
||||
if (cmd.rt_post_fork_info.linux_cgroup_hard_fail) {
|
||||
log.err("dbus connection required for cgroup isolation, exiting", .{});
|
||||
return error.PostForkError;
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
cgroup.createScope(
|
||||
dbus,
|
||||
pid,
|
||||
.{
|
||||
.memory_high = cmd.rt_post_fork_info.linux_cgroup_memory_limit,
|
||||
.tasks_max = cmd.rt_post_fork_info.linux_cgroup_processes_limit,
|
||||
},
|
||||
) catch |err| {
|
||||
if (cmd.rt_post_fork_info.linux_cgroup_hard_fail) {
|
||||
log.err("unable to create transient systemd scope {s}: {t}", .{ expected_cgroup, err });
|
||||
return error.PostForkError;
|
||||
}
|
||||
log.warn("unable to create transient systemd scope {s}: {t}", .{ expected_cgroup, err });
|
||||
return;
|
||||
};
|
||||
|
||||
const start = std.time.Instant.now() catch unreachable;
|
||||
|
||||
loop: while (true) {
|
||||
const now = std.time.Instant.now() catch unreachable;
|
||||
|
||||
if (now.since(start) > 250 * std.time.ns_per_ms) {
|
||||
if (cmd.rt_pre_exec_info.linux_cgroup_hard_fail) {
|
||||
log.err("transition to new transient systemd scope {s} took too long", .{expected_cgroup});
|
||||
return error.PostForkError;
|
||||
}
|
||||
log.warn("transition to transient systemd scope {s} took too long", .{expected_cgroup});
|
||||
break :loop;
|
||||
}
|
||||
|
||||
not_found: {
|
||||
var current_cgroup_buf: [4096]u8 = undefined;
|
||||
|
||||
const current_cgroup_raw = internal_os.cgroup.current(
|
||||
¤t_cgroup_buf,
|
||||
@intCast(pid),
|
||||
) orelse break :not_found;
|
||||
|
||||
const index = std.mem.lastIndexOfScalar(u8, current_cgroup_raw, '/') orelse break :not_found;
|
||||
const current_cgroup = current_cgroup_raw[index + 1 ..];
|
||||
|
||||
if (std.mem.eql(u8, current_cgroup, expected_cgroup)) {
|
||||
log.debug("transition to transient systemd scope {s} complete", .{expected_cgroup});
|
||||
break :loop;
|
||||
}
|
||||
}
|
||||
|
||||
std.Thread.sleep(25 * std.time.ns_per_ms);
|
||||
}
|
||||
}
|
||||
81
src/apprt/gtk/pre_exec.zig
Normal file
81
src/apprt/gtk/pre_exec.zig
Normal file
@@ -0,0 +1,81 @@
|
||||
const std = @import("std");
|
||||
|
||||
const log = std.log.scoped(.gtk_pre_exec);
|
||||
|
||||
const configpkg = @import("../../config.zig");
|
||||
|
||||
const internal_os = @import("../../os/main.zig");
|
||||
const Command = @import("../../Command.zig");
|
||||
const cgroup = @import("./cgroup.zig");
|
||||
|
||||
pub const PreExecInfo = struct {
|
||||
gtk_single_instance: configpkg.Config.GtkSingleInstance,
|
||||
linux_cgroup: configpkg.Config.LinuxCgroup,
|
||||
linux_cgroup_hard_fail: bool,
|
||||
|
||||
pub fn init(cfg: *const configpkg.Config) PreExecInfo {
|
||||
return .{
|
||||
.gtk_single_instance = cfg.@"gtk-single-instance",
|
||||
.linux_cgroup = cfg.@"linux-cgroup",
|
||||
.linux_cgroup_hard_fail = cfg.@"linux-cgroup-hard-fail",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/// If we are expecting to be moved to a transient systemd scope, wait to see if
|
||||
/// that happens by checking for the correct name of the current cgroup. Wait at
|
||||
/// most 250ms so that we don't overly delay the soft-fail scenario.
|
||||
///
|
||||
/// If we are configured to hard fail, log an error message and return an error
|
||||
/// code if we don't detect the move in time.
|
||||
pub fn preExec(cmd: *Command) ?u8 {
|
||||
switch (cmd.rt_pre_exec_info.linux_cgroup) {
|
||||
.always => {},
|
||||
.never => return null,
|
||||
.@"single-instance" => switch (cmd.rt_pre_exec_info.gtk_single_instance) {
|
||||
.true => {},
|
||||
.false => return null,
|
||||
.detect => {
|
||||
log.err("gtk-single-instance is set to detect", .{});
|
||||
return 127;
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const pid: u32 = @intCast(std.os.linux.getpid());
|
||||
|
||||
var expected_cgroup_buf: [256]u8 = undefined;
|
||||
const expected_cgroup = cgroup.fmtScope(&expected_cgroup_buf, pid);
|
||||
|
||||
const start = std.time.Instant.now() catch unreachable;
|
||||
|
||||
while (true) {
|
||||
const now = std.time.Instant.now() catch unreachable;
|
||||
|
||||
if (now.since(start) > 250 * std.time.ns_per_ms) {
|
||||
if (cmd.rt_pre_exec_info.linux_cgroup_hard_fail) {
|
||||
log.err("transition to new transient systemd scope took too long", .{});
|
||||
return 127;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
not_found: {
|
||||
var current_cgroup_buf: [4096]u8 = undefined;
|
||||
|
||||
const current_cgroup_raw = internal_os.cgroup.current(
|
||||
¤t_cgroup_buf,
|
||||
@intCast(pid),
|
||||
) orelse break :not_found;
|
||||
|
||||
const index = std.mem.lastIndexOfScalar(u8, current_cgroup_raw, '/') orelse break :not_found;
|
||||
const current_cgroup = current_cgroup_raw[index + 1 ..];
|
||||
|
||||
if (std.mem.eql(u8, current_cgroup, expected_cgroup)) return null;
|
||||
}
|
||||
|
||||
std.Thread.sleep(25 * std.time.ns_per_ms);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -3392,13 +3392,12 @@ keybind: Keybinds = .{},
|
||||
/// Available since: 1.2.0
|
||||
@"macos-shortcuts": MacShortcuts = .ask,
|
||||
|
||||
/// Put every surface (tab, split, window) into a dedicated Linux cgroup.
|
||||
/// Put every surface (tab, split, window) into a transient `systemd` scope.
|
||||
///
|
||||
/// 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 allows per-surface resource management. 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
|
||||
@@ -3407,9 +3406,9 @@ 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).
|
||||
/// 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:
|
||||
///
|
||||
@@ -3425,30 +3424,34 @@ else
|
||||
/// 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
|
||||
/// Note that this sets the `MemoryHigh` setting on the transient `systemd`
|
||||
/// scope, which is a soft limit. You should configure something like
|
||||
/// `systemd-oom` to handle killing processes that have too much memory
|
||||
/// pressure.
|
||||
///
|
||||
/// See the `systemd.resource-control` manual page for more information:
|
||||
/// https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html
|
||||
@"linux-cgroup-memory-limit": ?u64 = null,
|
||||
|
||||
/// Number of processes limit for any individual terminal process (tab, split,
|
||||
/// window, etc.). If this is unset then no limit will be set.
|
||||
///
|
||||
/// Note that this sets the "pids.max" configuration for the process number
|
||||
/// controller, which is a hard limit.
|
||||
/// Note that this sets the `TasksMax` setting on the transient `systemd` scope,
|
||||
/// which is a hard limit.
|
||||
///
|
||||
/// See the `systemd.resource-control` manual page for more information:
|
||||
/// https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html
|
||||
@"linux-cgroup-processes-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
|
||||
/// management feature, because Ghostty startup will not fail if cgroup APIs
|
||||
/// fail.
|
||||
/// If this is false, then creating a transient `systemd` scope (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 surface creation will not fail if
|
||||
/// `systemd` APIs fail.
|
||||
///
|
||||
/// If this is true, then any cgroup initialization failure will cause
|
||||
/// Ghostty to exit or new surfaces to not be created.
|
||||
/// If this is true, then any transient `systemd` scope creation failure will
|
||||
/// cause surface creation to fail.
|
||||
///
|
||||
/// 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,
|
||||
|
||||
/// Enable or disable GTK's OpenGL debugging logs. The default is `true` for
|
||||
|
||||
@@ -1,254 +1,26 @@
|
||||
const std = @import("std");
|
||||
const assert = @import("../quirks.zig").inlineAssert;
|
||||
const linux = std.os.linux;
|
||||
const posix = std.posix;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
const log = std.log.scoped(.@"linux-cgroup");
|
||||
|
||||
/// Returns the path to the cgroup for the given pid.
|
||||
pub fn current(alloc: Allocator, pid: std.os.linux.pid_t) !?[]const u8 {
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
pub fn current(buf: []u8, pid: u32) ?[]const u8 {
|
||||
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
|
||||
// Read our cgroup by opening /proc/<pid>/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, .{});
|
||||
const path = std.fmt.bufPrint(&path_buf, "/proc/{}/cgroup", .{pid}) catch return null;
|
||||
const file = std.fs.openFileAbsolute(path, .{}) catch return null;
|
||||
defer file.close();
|
||||
|
||||
// Read it all into memory -- we don't expect this file to ever be that large.
|
||||
const contents = try file.readToEndAlloc(
|
||||
alloc,
|
||||
1 * 1024 * 1024, // 1MB
|
||||
);
|
||||
defer alloc.free(contents);
|
||||
var read_buf: [64]u8 = undefined;
|
||||
var file_reader = file.reader(&read_buf);
|
||||
const reader = &file_reader.interface;
|
||||
const len = reader.readSliceShort(buf) catch return null;
|
||||
const contents = buf[0..len];
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
/// 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();
|
||||
|
||||
var file_buf: [64]u8 = undefined;
|
||||
var writer = file.writer(&file_buf);
|
||||
try writer.interface.print("{}", .{pid});
|
||||
try writer.interface.flush();
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove a cgroup. This will only succeed if the cgroup is empty
|
||||
/// (has no processes). The cgroup path should be relative to the
|
||||
/// cgroup root (e.g. "/user.slice/surfaces/abc123.scope").
|
||||
pub fn remove(cgroup: []const u8) !void {
|
||||
assert(cgroup.len > 0);
|
||||
assert(cgroup[0] == '/');
|
||||
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const path = try std.fmt.bufPrint(&buf, "/sys/fs/cgroup{s}", .{cgroup});
|
||||
std.fs.cwd().deleteDir(path) catch |err| switch (err) {
|
||||
// If it doesn't exist, that's fine - maybe it was already cleaned up
|
||||
error.FileNotFound => {},
|
||||
|
||||
// Any other error we failed to delete it so we want to notify
|
||||
// the user.
|
||||
else => return err,
|
||||
};
|
||||
}
|
||||
|
||||
/// 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});
|
||||
}
|
||||
|
||||
/// Use clone3 to have the kernel create a new process with the correct cgroup
|
||||
/// rather than moving the process to the correct cgroup later.
|
||||
pub fn cloneInto(cgroup: []const u8) !posix.pid_t {
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const path = try std.fmt.bufPrintZ(&buf, "/sys/fs/cgroup{s}", .{cgroup});
|
||||
|
||||
// Get a file descriptor that refers to the cgroup directory in the cgroup
|
||||
// sysfs to pass to the kernel in clone3.
|
||||
const fd: linux.fd_t = fd: {
|
||||
const rc = linux.open(
|
||||
path,
|
||||
.{
|
||||
// Self-explanatory: we expect to open a directory, and
|
||||
// we only need the path-level permissions.
|
||||
.PATH = true,
|
||||
.DIRECTORY = true,
|
||||
|
||||
// We don't want to leak this fd to the child process
|
||||
// when we clone below since we're using this fd for
|
||||
// a cgroup clone.
|
||||
.CLOEXEC = true,
|
||||
},
|
||||
0,
|
||||
);
|
||||
|
||||
switch (posix.errno(rc)) {
|
||||
.SUCCESS => break :fd @as(linux.fd_t, @intCast(rc)),
|
||||
else => |errno| {
|
||||
log.err("unable to open cgroup dir {s}: {}", .{ path, errno });
|
||||
return error.CloneError;
|
||||
},
|
||||
}
|
||||
};
|
||||
assert(fd >= 0);
|
||||
defer _ = linux.close(fd);
|
||||
|
||||
const args: extern struct {
|
||||
flags: u64,
|
||||
pidfd: u64,
|
||||
child_tid: u64,
|
||||
parent_tid: u64,
|
||||
exit_signal: u64,
|
||||
stack: u64,
|
||||
stack_size: u64,
|
||||
tls: u64,
|
||||
set_tid: u64,
|
||||
set_tid_size: u64,
|
||||
cgroup: u64,
|
||||
} = .{
|
||||
.flags = linux.CLONE.INTO_CGROUP,
|
||||
.pidfd = 0,
|
||||
.child_tid = 0,
|
||||
.parent_tid = 0,
|
||||
.exit_signal = linux.SIG.CHLD,
|
||||
.stack = 0,
|
||||
.stack_size = 0,
|
||||
.tls = 0,
|
||||
.set_tid = 0,
|
||||
.set_tid_size = 0,
|
||||
.cgroup = @intCast(fd),
|
||||
};
|
||||
|
||||
const rc = linux.syscall2(linux.SYS.clone3, @intFromPtr(&args), @sizeOf(@TypeOf(args)));
|
||||
// do not use posix.errno, when linking libc it will use the libc errno which will not be set when making the syscall directly
|
||||
return switch (std.os.linux.E.init(rc)) {
|
||||
.SUCCESS => @as(posix.pid_t, @intCast(rc)),
|
||||
else => |errno| err: {
|
||||
log.err("unable to clone: {}", .{errno});
|
||||
break :err error.CloneError;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// 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 controllers(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.
|
||||
const contents = try file.readToEndAlloc(
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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
|
||||
var writer_buf: [4096]u8 = undefined;
|
||||
var writer = file.writer(&writer_buf);
|
||||
try writer.interface.writeAll(v);
|
||||
try writer.interface.flush();
|
||||
}
|
||||
|
||||
pub const Limit = union(enum) {
|
||||
memory_high: usize,
|
||||
pids_max: usize,
|
||||
};
|
||||
|
||||
/// Configure a limit for the given cgroup. Use the various
|
||||
/// fields in Limit to configure a specific type of limit.
|
||||
pub fn configureLimit(cgroup: []const u8, limit: Limit) !void {
|
||||
assert(cgroup[0] == '/');
|
||||
|
||||
const filename, const size = switch (limit) {
|
||||
.memory_high => |v| .{ "memory.high", v },
|
||||
.pids_max => |v| .{ "pids.max", 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
|
||||
var writer_buf: [4096]u8 = undefined;
|
||||
var writer = file.writer(&writer_buf);
|
||||
try writer.interface.print("{}", .{size});
|
||||
try writer.interface.flush();
|
||||
return std.mem.trimRight(u8, contents[idx + 1 ..], " \r\n");
|
||||
}
|
||||
|
||||
@@ -566,7 +566,9 @@ pub const Config = struct {
|
||||
working_directory: ?[]const u8 = null,
|
||||
resources_dir: ?[]const u8,
|
||||
term: []const u8,
|
||||
linux_cgroup: Command.LinuxCgroup = Command.linux_cgroup_default,
|
||||
|
||||
rt_pre_exec_info: Command.RtPreExecInfo,
|
||||
rt_post_fork_info: Command.RtPostForkInfo,
|
||||
};
|
||||
|
||||
const Subprocess = struct {
|
||||
@@ -584,7 +586,9 @@ const Subprocess = struct {
|
||||
screen_size: renderer.ScreenSize,
|
||||
pty: ?Pty = null,
|
||||
process: ?Process = null,
|
||||
linux_cgroup: Command.LinuxCgroup = Command.linux_cgroup_default,
|
||||
|
||||
rt_pre_exec_info: Command.RtPreExecInfo,
|
||||
rt_post_fork_info: Command.RtPostForkInfo,
|
||||
|
||||
/// Union that represents the running process type.
|
||||
const Process = union(enum) {
|
||||
@@ -851,21 +855,14 @@ const Subprocess = struct {
|
||||
// https://github.com/ghostty-org/ghostty/discussions/7769
|
||||
if (cwd) |pwd| try env.put("PWD", pwd);
|
||||
|
||||
// If we have a cgroup, then we copy that into our arena so the
|
||||
// memory remains valid when we start.
|
||||
const linux_cgroup: Command.LinuxCgroup = cgroup: {
|
||||
const default = Command.linux_cgroup_default;
|
||||
if (comptime builtin.os.tag != .linux) break :cgroup default;
|
||||
const path = cfg.linux_cgroup orelse break :cgroup default;
|
||||
break :cgroup try alloc.dupe(u8, path);
|
||||
};
|
||||
|
||||
return .{
|
||||
.arena = arena,
|
||||
.env = env,
|
||||
.cwd = cwd,
|
||||
.args = args,
|
||||
.linux_cgroup = linux_cgroup,
|
||||
|
||||
.rt_pre_exec_info = cfg.rt_pre_exec_info,
|
||||
.rt_post_fork_info = cfg.rt_post_fork_info,
|
||||
|
||||
// Should be initialized with initTerminal call.
|
||||
.grid_size = .{},
|
||||
@@ -1014,17 +1011,27 @@ const Subprocess = struct {
|
||||
.stdout = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave },
|
||||
.stderr = if (builtin.os.tag == .windows) null else .{ .handle = pty.slave },
|
||||
.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 sp = cmd.getData(Subprocess) orelse unreachable;
|
||||
sp.childPreExec() catch |err| log.err(
|
||||
"error initializing child: {}",
|
||||
.{err},
|
||||
);
|
||||
}
|
||||
}).callback,
|
||||
.os_pre_exec = switch (comptime builtin.os.tag) {
|
||||
.windows => null,
|
||||
else => f: {
|
||||
const f = struct {
|
||||
fn callback(cmd: *Command) ?u8 {
|
||||
const sp = cmd.getData(Subprocess) orelse unreachable;
|
||||
sp.childPreExec() catch |err| log.err(
|
||||
"error initializing child: {}",
|
||||
.{err},
|
||||
);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
break :f f.callback;
|
||||
},
|
||||
},
|
||||
.rt_pre_exec = if (comptime @hasDecl(apprt.runtime, "pre_exec")) apprt.runtime.pre_exec.preExec else null,
|
||||
.rt_pre_exec_info = self.rt_pre_exec_info,
|
||||
.rt_post_fork = if (comptime @hasDecl(apprt.runtime, "post_fork")) apprt.runtime.post_fork.postFork else null,
|
||||
.rt_post_fork_info = self.rt_post_fork_info,
|
||||
.data = self,
|
||||
.linux_cgroup = self.linux_cgroup,
|
||||
};
|
||||
|
||||
cmd.start(alloc) catch |err| {
|
||||
@@ -1046,9 +1053,6 @@ 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.process = .{ .fork_exec = cmd };
|
||||
return switch (builtin.os.tag) {
|
||||
|
||||
Reference in New Issue
Block a user