mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-08-02 14:57:31 +03:00
gtk: add process scanner
This commit is contained in:
@ -131,6 +131,9 @@ child_exited: bool = false,
|
||||
/// to let us know.
|
||||
focused: bool = true,
|
||||
|
||||
ssh: bool = false,
|
||||
elevated: bool = false,
|
||||
|
||||
/// The effect of an input event. This can be used by callers to take
|
||||
/// the appropriate action after an input event. For example, key
|
||||
/// input can be forwarded to the OS for further processing if it
|
||||
@ -4711,3 +4714,9 @@ fn presentSurface(self: *Surface) !void {
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
/// Return the PID of the "root" process being served by this surface.
|
||||
pub fn getPid(self: *Surface) ?std.posix.pid_t {
|
||||
const command = self.io.backend.exec.subprocess.command orelse return null;
|
||||
return command.pid;
|
||||
}
|
||||
|
@ -31,11 +31,14 @@ const Window = @import("Window.zig");
|
||||
const ConfigErrorsWindow = @import("ConfigErrorsWindow.zig");
|
||||
const ClipboardConfirmationWindow = @import("ClipboardConfirmationWindow.zig");
|
||||
const Split = @import("Split.zig");
|
||||
const ProcessScanner = @import("ProcessScanner.zig");
|
||||
const c = @import("c.zig").c;
|
||||
const version = @import("version.zig");
|
||||
const inspector = @import("inspector.zig");
|
||||
const key = @import("key.zig");
|
||||
const winproto = @import("winproto.zig");
|
||||
const linuxproc = @import("../../os/linuxproc.zig");
|
||||
|
||||
const testing = std.testing;
|
||||
|
||||
const log = std.log.scoped(.gtk);
|
||||
@ -95,6 +98,8 @@ quit_timer: union(enum) {
|
||||
expired: void,
|
||||
} = .{ .off = {} },
|
||||
|
||||
process_scanner: ProcessScanner,
|
||||
|
||||
pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
_ = opts;
|
||||
|
||||
@ -414,12 +419,14 @@ pub fn init(core_app: *CoreApp, opts: Options) !App {
|
||||
// our "activate" call above will open a window.
|
||||
.running = c.g_application_get_is_remote(gapp) == 0,
|
||||
.css_provider = css_provider,
|
||||
.process_scanner = undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// Terminate the application. The application will not be restarted after
|
||||
// this so all global state can be cleaned up.
|
||||
pub fn terminate(self: *App) void {
|
||||
self.process_scanner.stop();
|
||||
c.g_settings_sync();
|
||||
while (c.g_main_context_iteration(self.ctx, 0) != 0) {}
|
||||
c.g_main_context_release(self.ctx);
|
||||
@ -1198,6 +1205,8 @@ pub fn run(self: *App) !void {
|
||||
// right away.
|
||||
if (!self.running) return;
|
||||
|
||||
self.process_scanner.init(self);
|
||||
|
||||
// If we are running, then we proceed to setup our app.
|
||||
|
||||
// Setup our cgroup configurations for our surfaces.
|
||||
|
231
src/apprt/gtk/ProcessScanner.zig
Normal file
231
src/apprt/gtk/ProcessScanner.zig
Normal file
@ -0,0 +1,231 @@
|
||||
const ProcessScanner = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const linux = std.os.linux;
|
||||
|
||||
const xev = @import("xev");
|
||||
|
||||
const CoreApp = @import("../../App.zig");
|
||||
const App = @import("App.zig");
|
||||
const Surface = @import("Surface.zig");
|
||||
|
||||
const linuxproc = @import("../../os/linuxproc.zig");
|
||||
|
||||
const log = std.log.scoped(.process_scanner);
|
||||
|
||||
alloc: std.mem.Allocator,
|
||||
app: *App,
|
||||
thread: ?std.Thread,
|
||||
|
||||
stop_: ?*xev.Async = null,
|
||||
|
||||
pub fn init(self: *ProcessScanner, app: *App) void {
|
||||
self.* = .{
|
||||
.alloc = app.core_app.alloc,
|
||||
.app = app,
|
||||
.thread = null,
|
||||
};
|
||||
|
||||
self.thread = std.Thread.spawn(.{}, run, .{self}) catch |err| {
|
||||
log.warn("unable to spawn process scanner thread: {}", .{err});
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
pub fn stop(self: *ProcessScanner) void {
|
||||
if (self.stop_) |s| s.notify() catch |err| {
|
||||
log.warn("unable to stop process scanner: {}", .{err});
|
||||
};
|
||||
if (self.thread) |thread| thread.join();
|
||||
}
|
||||
|
||||
fn run(self: *ProcessScanner) void {
|
||||
var loop = xev.Loop.init(.{}) catch |err| {
|
||||
log.warn("unable to initialize process scan event loop: {}", .{err});
|
||||
return;
|
||||
};
|
||||
defer loop.deinit();
|
||||
|
||||
var stop_a = xev.Async.init() catch |err| {
|
||||
log.warn("unable to initialize process scan stop async: {}", .{err});
|
||||
return;
|
||||
};
|
||||
defer stop_a.deinit();
|
||||
|
||||
var stop_c: xev.Completion = undefined;
|
||||
|
||||
stop_a.wait(
|
||||
&loop,
|
||||
&stop_c,
|
||||
ProcessScanner,
|
||||
self,
|
||||
stopCallback,
|
||||
);
|
||||
self.stop_ = &stop_a;
|
||||
|
||||
var timer = xev.Timer.init() catch |err| {
|
||||
log.warn("unable to init process scan timer: {}", .{err});
|
||||
return;
|
||||
};
|
||||
defer timer.deinit();
|
||||
|
||||
var timer_c: xev.Completion = undefined;
|
||||
|
||||
timer.run(
|
||||
&loop,
|
||||
&timer_c,
|
||||
500,
|
||||
ProcessScanner,
|
||||
self,
|
||||
timerCallback,
|
||||
);
|
||||
|
||||
loop.run(.until_done) catch |err| {
|
||||
log.warn("error while running process scan loop: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn stopCallback(_: ?*ProcessScanner, loop: *xev.Loop, _: *xev.Completion, result: xev.Async.WaitError!void) xev.CallbackAction {
|
||||
_ = result catch unreachable;
|
||||
loop.stop();
|
||||
return .disarm;
|
||||
}
|
||||
|
||||
const SurfaceInfo = struct {
|
||||
surface: *Surface,
|
||||
ssh: bool = false,
|
||||
elevated: bool = false,
|
||||
};
|
||||
|
||||
const SurfaceInfoContext = struct {
|
||||
pub fn hash(_: SurfaceInfoContext, pid: std.os.linux.pid_t) u32 {
|
||||
return std.hash.XxHash32.hash(0, std.mem.asBytes(&pid));
|
||||
}
|
||||
|
||||
pub fn eql(_: SurfaceInfoContext, a: std.os.linux.pid_t, b: std.os.linux.pid_t, b_index: usize) bool {
|
||||
_ = b_index;
|
||||
return a == b;
|
||||
}
|
||||
};
|
||||
|
||||
const SurfacePidMap = std.ArrayHashMap(
|
||||
linux.pid_t,
|
||||
SurfaceInfo,
|
||||
SurfaceInfoContext,
|
||||
false,
|
||||
);
|
||||
|
||||
const ProcessPidMap = std.ArrayHashMap(
|
||||
linux.pid_t,
|
||||
linuxproc.LinuxProcessInfo,
|
||||
linuxproc.LinuxProcessInfoContext,
|
||||
false,
|
||||
);
|
||||
|
||||
fn timerCallback(
|
||||
self_: ?*ProcessScanner,
|
||||
_: *xev.Loop,
|
||||
_: *xev.Completion,
|
||||
result: xev.Timer.RunError!void,
|
||||
) xev.CallbackAction {
|
||||
const start = std.time.Instant.now() catch unreachable;
|
||||
_ = result catch unreachable;
|
||||
const self = self_ orelse unreachable;
|
||||
|
||||
var arena = std.heap.ArenaAllocator.init(self.alloc);
|
||||
defer arena.deinit();
|
||||
const alloc = arena.allocator();
|
||||
|
||||
// build a map from PID to surface of all our surfaces
|
||||
var surfaces = SurfacePidMap.init(alloc);
|
||||
{
|
||||
for (self.app.core_app.surfaces.items) |surface| {
|
||||
const pid = surface.getPid() orelse continue;
|
||||
surfaces.put(pid, .{ .surface = surface }) catch |err| {
|
||||
log.warn("unable to add surface to map: {}", .{err});
|
||||
return .rearm;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// build a map of all processes on the system
|
||||
var pids = ProcessPidMap.init(alloc);
|
||||
{
|
||||
const s = std.time.Instant.now() catch unreachable;
|
||||
var it = linuxproc.Iterator.init() catch |err| {
|
||||
log.err("unable to initialize linux proc iterator: {}", .{err});
|
||||
return .rearm;
|
||||
};
|
||||
defer it.deinit();
|
||||
|
||||
while (it.next() catch |err| {
|
||||
log.err("error getting next pid: {}", .{err});
|
||||
return .rearm;
|
||||
}) |pid| {
|
||||
pids.put(pid.pid, pid) catch |err| {
|
||||
log.err("unable to put pid {} into map: {}", .{ pid.pid, err });
|
||||
return .rearm;
|
||||
};
|
||||
}
|
||||
|
||||
const e = std.time.Instant.now() catch unreachable;
|
||||
const d = e.since(s);
|
||||
const d_ms = @as(f64, @floatFromInt(d)) / @as(f64, @floatFromInt(std.time.ns_per_ms));
|
||||
if (d_ms > 10.0) log.info("reading /proc took: {d:1.3}ms", .{d_ms});
|
||||
}
|
||||
|
||||
// iterate over all pids
|
||||
{
|
||||
var it = pids.iterator();
|
||||
while (it.next()) |kv| {
|
||||
// this is the process currently being considered
|
||||
const pi = kv.value_ptr;
|
||||
|
||||
// don't bother if it's not "special"
|
||||
if (!pi.ssh and !pi.elevated) continue;
|
||||
|
||||
// climb the tree of processes to try and find one that is
|
||||
// the "root" of a surface process tree
|
||||
var ppid = pi.pid;
|
||||
var entry_: ?SurfacePidMap.Entry = null;
|
||||
while (entry_ == null) {
|
||||
entry_ = surfaces.getEntry(ppid);
|
||||
|
||||
if (entry_) |entry| {
|
||||
// we got one!
|
||||
const si = entry.value_ptr;
|
||||
si.ssh = si.ssh or pi.ssh;
|
||||
si.elevated = si.elevated or pi.elevated;
|
||||
break;
|
||||
}
|
||||
const i = pids.get(ppid) orelse break;
|
||||
if (i.pid == 1) break;
|
||||
ppid = i.ppid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
var it = surfaces.iterator();
|
||||
while (it.next()) |kv| {
|
||||
const pid = kv.key_ptr.*;
|
||||
const si = kv.value_ptr;
|
||||
const surface = si.surface;
|
||||
if (surface.core_surface.ssh != si.ssh) {
|
||||
log.info("surface: pid: {} ssh: {} -> {}", .{ pid, surface.core_surface.ssh, si.ssh });
|
||||
surface.core_surface.ssh = si.ssh;
|
||||
}
|
||||
if (surface.core_surface.elevated != si.elevated) {
|
||||
log.info("surface: pid: {} elevated: {} -> {}", .{ pid, surface.core_surface.elevated, si.elevated });
|
||||
surface.core_surface.elevated = si.elevated;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const end = std.time.Instant.now() catch unreachable;
|
||||
const diff = end.since(start);
|
||||
const diff_ms = @as(f64, @floatFromInt(diff)) / @as(f64, @floatFromInt(std.time.ns_per_ms));
|
||||
if (diff_ms > 10.0) log.info("process scan took: {d:1.3}ms", .{diff_ms});
|
||||
|
||||
return .rearm;
|
||||
}
|
@ -2298,3 +2298,8 @@ fn g_value_holds(value_: ?*c.GValue, g_type: c.GType) bool {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Return the PID of the "root" process being served by this surface.
|
||||
pub fn getPid(self: *Surface) ?std.posix.pid_t {
|
||||
return self.core_surface.getPid();
|
||||
}
|
||||
|
150
src/os/linuxproc.zig
Normal file
150
src/os/linuxproc.zig
Normal file
@ -0,0 +1,150 @@
|
||||
const ProcessScanner = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const linux = std.os.linux;
|
||||
const posix = std.posix;
|
||||
|
||||
const log = std.log.scoped(.linuxproc);
|
||||
|
||||
/// Information about a Linux process that we are interested in
|
||||
pub const LinuxProcessInfo = struct {
|
||||
/// This PID of the process.
|
||||
pid: linux.pid_t,
|
||||
/// The parent PID of the process.
|
||||
ppid: linux.pid_t,
|
||||
/// If the process is named 'ssh'.
|
||||
ssh: bool,
|
||||
/// If the process has an effective UID of 0.
|
||||
elevated: bool,
|
||||
};
|
||||
|
||||
pub const LinuxProcessInfoContext = struct {
|
||||
pub fn hash(_: LinuxProcessInfoContext, pid: linux.pid_t) u32 {
|
||||
return std.hash.XxHash32.hash(0, std.mem.asBytes(&pid));
|
||||
}
|
||||
|
||||
pub fn eql(_: LinuxProcessInfoContext, a: linux.pid_t, b: linux.pid_t, b_index: usize) bool {
|
||||
_ = b_index;
|
||||
return a == b;
|
||||
}
|
||||
};
|
||||
|
||||
pub const Error = std.mem.Allocator.Error || std.fs.File.OpenError || std.fs.File.ReadError;
|
||||
|
||||
pub fn getProcessInfo(pid: linux.pid_t) Error!?LinuxProcessInfo {
|
||||
var buf: [4096]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buf);
|
||||
const alloc = fba.allocator();
|
||||
|
||||
const pathname = try std.fs.path.join(
|
||||
alloc,
|
||||
&.{
|
||||
"/proc",
|
||||
try std.fmt.allocPrint(alloc, "{}", .{pid}),
|
||||
"status",
|
||||
},
|
||||
);
|
||||
|
||||
const status_file = std.fs.openFileAbsolute(pathname, .{ .mode = .read_only }) catch |err| switch (err) {
|
||||
error.FileNotFound => return null,
|
||||
else => |e| return e,
|
||||
};
|
||||
defer status_file.close();
|
||||
|
||||
const data = status_file.readToEndAlloc(alloc, 2048) catch |err| switch (err) {
|
||||
error.FileTooBig => {
|
||||
log.warn("{s} too big", .{pathname});
|
||||
return null;
|
||||
},
|
||||
else => |e| return e,
|
||||
};
|
||||
|
||||
var ppid_: ?std.os.linux.pid_t = null;
|
||||
var uid_effective_: ?std.os.linux.uid_t = null;
|
||||
var ssh: bool = false;
|
||||
|
||||
// for the format of /proc/$pid/status see:
|
||||
// https://man7.org/linux/man-pages/man5/proc_pid_status.5.html
|
||||
|
||||
var lines = std.mem.splitScalar(u8, data, '\n');
|
||||
|
||||
while (lines.next()) |line| {
|
||||
var kv = std.mem.splitSequence(u8, line, ":\t");
|
||||
const key = kv.first();
|
||||
const value = kv.rest();
|
||||
const field = std.meta.stringToEnum(
|
||||
// The keys in this enum must match the name of a field in /proc/$pid/status
|
||||
enum {
|
||||
Name,
|
||||
PPid,
|
||||
Uid,
|
||||
},
|
||||
key,
|
||||
) orelse continue;
|
||||
switch (field) {
|
||||
.Name => {
|
||||
if (std.mem.eql(u8, value, "ssh")) ssh = true;
|
||||
},
|
||||
.PPid => {
|
||||
ppid_ = std.fmt.parseUnsigned(
|
||||
std.os.linux.pid_t,
|
||||
value,
|
||||
10,
|
||||
) catch continue;
|
||||
},
|
||||
.Uid => {
|
||||
var u_it = std.mem.splitScalar(u8, value, '\t');
|
||||
|
||||
_ = u_it.next() orelse continue;
|
||||
|
||||
uid_effective_ = std.fmt.parseUnsigned(
|
||||
std.os.linux.uid_t,
|
||||
u_it.next() orelse continue,
|
||||
10,
|
||||
) catch continue;
|
||||
|
||||
// this is the last thing we need so don't parse the rest of the file
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return .{
|
||||
.pid = pid,
|
||||
.ppid = ppid_ orelse return null,
|
||||
.ssh = ssh,
|
||||
.elevated = (uid_effective_ orelse return null) == 0,
|
||||
};
|
||||
}
|
||||
|
||||
pub const Iterator = struct {
|
||||
dir: std.fs.Dir,
|
||||
iterator: std.fs.Dir.Iterator,
|
||||
|
||||
pub fn init() !Iterator {
|
||||
var dir = try std.fs.openDirAbsolute("/proc", .{ .iterate = true });
|
||||
const iterator = dir.iterate();
|
||||
return .{
|
||||
.dir = dir,
|
||||
.iterator = iterator,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Iterator) void {
|
||||
self.dir.close();
|
||||
}
|
||||
|
||||
pub fn next(self: *Iterator) !?LinuxProcessInfo {
|
||||
while (try self.iterator.next()) |file| {
|
||||
switch (file.kind) {
|
||||
.directory => {
|
||||
const pid = std.fmt.parseUnsigned(linux.pid_t, file.name, 10) catch continue;
|
||||
const info = try getProcessInfo(pid) orelse continue;
|
||||
return info;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
Reference in New Issue
Block a user