mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-21 19:26:09 +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_wakeup = render_thread.wakeup,
|
||||||
.renderer_mailbox = render_thread.mailbox,
|
.renderer_mailbox = render_thread.mailbox,
|
||||||
.surface_mailbox = .{ .surface = self, .app = app_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();
|
errdefer io.deinit();
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ const CoreSurface = @import("../../Surface.zig");
|
|||||||
|
|
||||||
const build_options = @import("build_options");
|
const build_options = @import("build_options");
|
||||||
|
|
||||||
|
const cgroup = @import("cgroup.zig");
|
||||||
const Surface = @import("Surface.zig");
|
const Surface = @import("Surface.zig");
|
||||||
const Window = @import("Window.zig");
|
const Window = @import("Window.zig");
|
||||||
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
|
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
|
||||||
@ -43,6 +44,9 @@ config: Config,
|
|||||||
app: *c.GtkApplication,
|
app: *c.GtkApplication,
|
||||||
ctx: *c.GMainContext,
|
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.
|
/// The "none" cursor. We use one that is shared across the entire app.
|
||||||
cursor_none: ?*c.GdkCursor,
|
cursor_none: ?*c.GdkCursor,
|
||||||
|
|
||||||
@ -61,6 +65,11 @@ running: bool = true,
|
|||||||
/// Xkb state (X11 only). Will be null on Wayland.
|
/// Xkb state (X11 only). Will be null on Wayland.
|
||||||
x11_xkb: ?x11.Xkb = null,
|
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 {
|
pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||||
_ = opts;
|
_ = opts;
|
||||||
|
|
||||||
@ -263,6 +272,7 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
|||||||
.ctx = ctx,
|
.ctx = ctx,
|
||||||
.cursor_none = cursor_none,
|
.cursor_none = cursor_none,
|
||||||
.x11_xkb = x11_xkb,
|
.x11_xkb = x11_xkb,
|
||||||
|
.single_instance = single_instance,
|
||||||
// If we are NOT the primary instance, then we never want to run.
|
// 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
|
// This means that another instance of the GTK app is running and
|
||||||
// our "activate" call above will open a window.
|
// 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.cursor_none) |cursor| c.g_object_unref(cursor);
|
||||||
if (self.menu) |menu| c.g_object_unref(menu);
|
if (self.menu) |menu| c.g_object_unref(menu);
|
||||||
|
if (self.transient_cgroup_base) |path| self.core_app.alloc.free(path);
|
||||||
|
|
||||||
self.config.deinit();
|
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.
|
/// Run the event loop. This doesn't return until the app exits.
|
||||||
pub fn run(self: *App) !void {
|
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 (!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.initActions();
|
||||||
self.initMenu();
|
self.initMenu();
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ const font = @import("../../font/main.zig");
|
|||||||
const input = @import("../../input.zig");
|
const input = @import("../../input.zig");
|
||||||
const terminal = @import("../../terminal/main.zig");
|
const terminal = @import("../../terminal/main.zig");
|
||||||
const CoreSurface = @import("../../Surface.zig");
|
const CoreSurface = @import("../../Surface.zig");
|
||||||
|
const internal_os = @import("../../os/main.zig");
|
||||||
|
|
||||||
const App = @import("App.zig");
|
const App = @import("App.zig");
|
||||||
const Split = @import("Split.zig");
|
const Split = @import("Split.zig");
|
||||||
@ -255,6 +256,10 @@ im_commit_buffered: bool = false,
|
|||||||
im_buf: [128]u8 = undefined,
|
im_buf: [128]u8 = undefined,
|
||||||
im_len: u7 = 0,
|
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 {
|
pub fn create(alloc: Allocator, app: *App, opts: Options) !*Surface {
|
||||||
var surface = try alloc.create(Surface);
|
var surface = try alloc.create(Surface);
|
||||||
errdefer alloc.destroy(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;
|
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
|
// Build our result
|
||||||
self.* = .{
|
self.* = .{
|
||||||
.app = app,
|
.app = app,
|
||||||
@ -354,6 +389,7 @@ pub fn init(self: *Surface, app: *App, opts: Options) !void {
|
|||||||
.size = .{ .width = 800, .height = 600 },
|
.size = .{ .width = 800, .height = 600 },
|
||||||
.cursor_pos = .{ .x = 0, .y = 0 },
|
.cursor_pos = .{ .x = 0, .y = 0 },
|
||||||
.im_context = im_context,
|
.im_context = im_context,
|
||||||
|
.cgroup_path = cgroup_path,
|
||||||
};
|
};
|
||||||
errdefer self.* = undefined;
|
errdefer self.* = undefined;
|
||||||
|
|
||||||
@ -442,9 +478,10 @@ pub fn deinit(self: *Surface) void {
|
|||||||
self.core_surface.deinit();
|
self.core_surface.deinit();
|
||||||
self.core_surface = undefined;
|
self.core_surface = undefined;
|
||||||
|
|
||||||
|
if (self.cgroup_path) |path| self.app.core_app.alloc.free(path);
|
||||||
|
|
||||||
// Free all our GTK stuff
|
// Free all our GTK stuff
|
||||||
c.g_object_unref(self.im_context);
|
c.g_object_unref(self.im_context);
|
||||||
|
|
||||||
if (self.cursor) |cursor| c.g_object_unref(cursor);
|
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);
|
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.
|
/// Queue the inspector to render if we have one.
|
||||||
pub fn queueInspectorRender(self: *Surface) void {
|
pub fn queueInspectorRender(self: *Surface) void {
|
||||||
if (self.inspector) |v| v.queueRender();
|
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 {
|
fn comptimeGenerateFishCompletions() []const u8 {
|
||||||
comptime {
|
comptime {
|
||||||
@setEvalBranchQuota(13000);
|
@setEvalBranchQuota(15000);
|
||||||
var counter = std.io.countingWriter(std.io.null_writer);
|
var counter = std.io.countingWriter(std.io.null_writer);
|
||||||
try writeFishCompletions(&counter.writer());
|
try writeFishCompletions(&counter.writer());
|
||||||
|
|
||||||
|
@ -988,6 +988,56 @@ keybind: Keybinds = .{},
|
|||||||
/// This does not work with GLFW builds.
|
/// This does not work with GLFW builds.
|
||||||
@"macos-option-as-alt": OptionAsAlt = .false,
|
@"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:
|
/// 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
|
/// each new `ghostty` process launched will result in a new window if there
|
||||||
/// is already a running process.
|
/// is already a running process.
|
||||||
@ -3495,3 +3545,10 @@ pub const GraphemeWidthMethod = enum {
|
|||||||
legacy,
|
legacy,
|
||||||
unicode,
|
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("pipe.zig");
|
||||||
pub usingnamespace @import("resourcesdir.zig");
|
pub usingnamespace @import("resourcesdir.zig");
|
||||||
pub const TempDir = @import("TempDir.zig");
|
pub const TempDir = @import("TempDir.zig");
|
||||||
|
pub const cgroup = @import("cgroup.zig");
|
||||||
pub const passwd = @import("passwd.zig");
|
pub const passwd = @import("passwd.zig");
|
||||||
pub const xdg = @import("xdg.zig");
|
pub const xdg = @import("xdg.zig");
|
||||||
pub const windows = @import("windows.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.width_px = subprocess.screen_size.width;
|
||||||
term.height_px = subprocess.screen_size.height;
|
term.height_px = subprocess.screen_size.height;
|
||||||
|
|
||||||
return Exec{
|
return .{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.terminal = term,
|
.terminal = term,
|
||||||
.subprocess = subprocess,
|
.subprocess = subprocess,
|
||||||
@ -897,6 +897,7 @@ const Subprocess = struct {
|
|||||||
pty: ?Pty = null,
|
pty: ?Pty = null,
|
||||||
command: ?Command = null,
|
command: ?Command = null,
|
||||||
flatpak_command: ?FlatpakHostCommand = 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
|
/// Initialize the subprocess. This will NOT start it, this only sets
|
||||||
/// up the internal state necessary to start it later.
|
/// up the internal state necessary to start it later.
|
||||||
@ -1193,6 +1194,15 @@ const Subprocess = struct {
|
|||||||
else
|
else
|
||||||
null;
|
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
|
// Our screen size should be our padded size
|
||||||
const padded_size = opts.screen_size.subPadding(opts.padding);
|
const padded_size = opts.screen_size.subPadding(opts.padding);
|
||||||
|
|
||||||
@ -1203,6 +1213,7 @@ const Subprocess = struct {
|
|||||||
.args = args,
|
.args = args,
|
||||||
.grid_size = opts.grid_size,
|
.grid_size = opts.grid_size,
|
||||||
.screen_size = padded_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 {},
|
.pseudo_console = if (builtin.os.tag == .windows) pty.pseudo_console else {},
|
||||||
.pre_exec = if (builtin.os.tag == .windows) null else (struct {
|
.pre_exec = if (builtin.os.tag == .windows) null else (struct {
|
||||||
fn callback(cmd: *Command) void {
|
fn callback(cmd: *Command) void {
|
||||||
const p = cmd.getData(Pty) orelse unreachable;
|
const sp = cmd.getData(Subprocess) orelse unreachable;
|
||||||
p.childPreExec() catch |err|
|
sp.childPreExec() catch |err| log.err(
|
||||||
log.err("error initializing child: {}", .{err});
|
"error initializing child: {}",
|
||||||
|
.{err},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}).callback,
|
}).callback,
|
||||||
.data = &self.pty.?,
|
.data = self,
|
||||||
};
|
};
|
||||||
try cmd.start(alloc);
|
try cmd.start(alloc);
|
||||||
errdefer killCommand(&cmd) catch |err| {
|
errdefer killCommand(&cmd) catch |err| {
|
||||||
log.warn("error killing command during cleanup err={}", .{err});
|
log.warn("error killing command during cleanup err={}", .{err});
|
||||||
};
|
};
|
||||||
log.info("started subcommand path={s} pid={?}", .{ self.args[0], cmd.pid });
|
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;
|
self.command = cmd;
|
||||||
return switch (builtin.os.tag) {
|
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
|
/// Called to notify that we exited externally so we can unset our
|
||||||
/// running state.
|
/// running state.
|
||||||
pub fn externalExit(self: *Subprocess) void {
|
pub fn externalExit(self: *Subprocess) void {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
//! The options that are used to configure a terminal IO implementation.
|
//! The options that are used to configure a terminal IO implementation.
|
||||||
|
|
||||||
|
const builtin = @import("builtin");
|
||||||
const xev = @import("xev");
|
const xev = @import("xev");
|
||||||
const apprt = @import("../apprt.zig");
|
const apprt = @import("../apprt.zig");
|
||||||
const renderer = @import("../renderer.zig");
|
const renderer = @import("../renderer.zig");
|
||||||
@ -41,3 +42,10 @@ renderer_mailbox: *renderer.Thread.Mailbox,
|
|||||||
|
|
||||||
/// The mailbox for sending the surface messages.
|
/// The mailbox for sending the surface messages.
|
||||||
surface_mailbox: apprt.surface.Mailbox,
|
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