diff --git a/src/Surface.zig b/src/Surface.zig index 98c344927..7c0204f5f 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -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; +} diff --git a/src/apprt/gtk/App.zig b/src/apprt/gtk/App.zig index 227c36ec4..649c71c8c 100644 --- a/src/apprt/gtk/App.zig +++ b/src/apprt/gtk/App.zig @@ -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. diff --git a/src/apprt/gtk/ProcessScanner.zig b/src/apprt/gtk/ProcessScanner.zig new file mode 100644 index 000000000..071d3145b --- /dev/null +++ b/src/apprt/gtk/ProcessScanner.zig @@ -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; +} diff --git a/src/apprt/gtk/Surface.zig b/src/apprt/gtk/Surface.zig index 9a8c4513d..eb5f6344d 100644 --- a/src/apprt/gtk/Surface.zig +++ b/src/apprt/gtk/Surface.zig @@ -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(); +} diff --git a/src/os/linuxproc.zig b/src/os/linuxproc.zig new file mode 100644 index 000000000..955b23b96 --- /dev/null +++ b/src/os/linuxproc.zig @@ -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; + } +};