mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 03:06:15 +03:00
Merge pull request #1828 from ghostty-org/cgroup
Linux: Launch each terminal surface into its own cgroup
This commit is contained in:
@ -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();
|
||||
|
||||
|
@ -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");
|
||||
@ -43,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,
|
||||
|
||||
@ -61,6 +65,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;
|
||||
|
||||
@ -263,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.
|
||||
@ -280,6 +290,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();
|
||||
}
|
||||
@ -373,9 +384,42 @@ 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 surfaces.
|
||||
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},
|
||||
);
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
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();
|
||||
self.initMenu();
|
||||
|
||||
|
@ -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,
|
||||
"surfaces/{X}.service",
|
||||
.{@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();
|
||||
|
183
src/apprt/gtk/cgroup.zig
Normal file
183
src/apprt/gtk/cgroup.zig
Normal file
@ -0,0 +1,183 @@
|
||||
/// 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;
|
||||
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 (app.config.@"linux-cgroup-memory-limit") |limit| {
|
||||
try internal_os.cgroup.configureMemoryLimit(surfaces, .{
|
||||
.high = 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.
|
||||
///
|
||||
/// 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 code={} err={s}",
|
||||
.{ e.code, e.message },
|
||||
);
|
||||
return error.DbusCallFailed;
|
||||
};
|
||||
}
|
@ -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());
|
||||
|
||||
|
@ -988,6 +988,56 @@ 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.
|
||||
///
|
||||
/// 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.
|
||||
/// * `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",
|
||||
|
||||
/// 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
|
||||
/// 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.
|
||||
@ -3495,3 +3545,10 @@ pub const GraphemeWidthMethod = enum {
|
||||
legacy,
|
||||
unicode,
|
||||
};
|
||||
|
||||
/// See linux-cgroup
|
||||
pub const LinuxCgroup = enum {
|
||||
never,
|
||||
always,
|
||||
@"single-instance",
|
||||
};
|
||||
|
149
src/os/cgroup.zig
Normal file
149
src/os/cgroup.zig
Normal file
@ -0,0 +1,149 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const Allocator = std.mem.Allocator;
|
||||
|
||||
/// 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;
|
||||
|
||||
// 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, .{});
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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});
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// 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.
|
||||
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);
|
||||
}
|
||||
|
||||
/// 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);
|
||||
}
|
||||
|
||||
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});
|
||||
}
|
@ -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 cgroup = @import("cgroup.zig");
|
||||
pub const passwd = @import("passwd.zig");
|
||||
pub const xdg = @import("xdg.zig");
|
||||
pub const windows = @import("windows.zig");
|
||||
|
@ -179,7 +179,7 @@ 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{
|
||||
return .{
|
||||
.alloc = alloc,
|
||||
.terminal = term,
|
||||
.subprocess = subprocess,
|
||||
@ -897,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.
|
||||
@ -1193,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);
|
||||
|
||||
@ -1203,6 +1213,7 @@ const Subprocess = struct {
|
||||
.args = args,
|
||||
.grid_size = opts.grid_size,
|
||||
.screen_size = padded_size,
|
||||
.linux_cgroup = linux_cgroup,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1296,18 +1307,23 @@ 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| {
|
||||
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) {
|
||||
@ -1323,6 +1339,21 @@ 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| {
|
||||
try internal_os.cgroup.moveInto(cgroup, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Called to notify that we exited externally so we can unset our
|
||||
/// running state.
|
||||
pub fn externalExit(self: *Subprocess) void {
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user