gtk: add process scanner

This commit is contained in:
Jeffrey C. Ollie
2025-02-17 20:46:05 -06:00
parent da32534e8a
commit 07971edfe5
5 changed files with 404 additions and 0 deletions

View File

@ -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;
}

View File

@ -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.

View 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;
}

View File

@ -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
View 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;
}
};