Regex free process reader

This commit is contained in:
Armin Friedl 2024-07-05 20:38:10 +02:00
parent 633713180d
commit 72ba68744e
10 changed files with 244 additions and 22 deletions

View file

@ -77,6 +77,20 @@ pub fn build(b: *std.Build) void {
const run_procnet_unit_tests = b.addRunArtifact(procnet_unit_tests); const run_procnet_unit_tests = b.addRunArtifact(procnet_unit_tests);
run_procnet_unit_tests.has_side_effects = true; // needed so tests aren't cached run_procnet_unit_tests.has_side_effects = true; // needed so tests aren't cached
// Creates a step for unit testing. This only builds the test executable
// but does not run it.
const process_unit_tests = b.addTest(.{
.root_source_file = b.path("src/process.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
process_unit_tests.addIncludePath(.{ .path = "lib" });
process_unit_tests.linkLibrary(relib);
const run_process_unit_tests = b.addRunArtifact(process_unit_tests);
run_process_unit_tests.has_side_effects = true; // needed so tests aren't cached
const exe_unit_tests = b.addTest(.{ const exe_unit_tests = b.addTest(.{
.root_source_file = b.path("src/main.zig"), .root_source_file = b.path("src/main.zig"),
.target = target, .target = target,
@ -92,5 +106,6 @@ pub fn build(b: *std.Build) void {
// running the unit tests. // running the unit tests.
const test_step = b.step("test", "Run unit tests"); const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_procnet_unit_tests.step); test_step.dependOn(&run_procnet_unit_tests.step);
test_step.dependOn(&run_process_unit_tests.step);
test_step.dependOn(&run_exe_unit_tests.step); test_step.dependOn(&run_exe_unit_tests.step);
} }

View file

@ -1 +0,0 @@
const std = @import("std");

View file

@ -2,6 +2,7 @@ const std = @import("std");
const proc = @import("procnet.zig"); const proc = @import("procnet.zig");
const pid = @import("procpid.zig"); const pid = @import("procpid.zig");
const clog = @import("proc.zig"); const clog = @import("proc.zig");
const proces = @import("process.zig");
const c = @cImport({ const c = @cImport({
@cInclude("arpa/inet.h"); @cInclude("arpa/inet.h");
@cInclude("signal.h"); @cInclude("signal.h");

View file

@ -5,7 +5,7 @@
const std = @import("std"); const std = @import("std");
// Socket that currently clogs a port with protocol information /// Socket that currently clogs a port with protocol information
pub const ClogSocket = struct { pub const ClogSocket = struct {
port: u16, port: u16,
inode: std.posix.ino_t, inode: std.posix.ino_t,
@ -13,7 +13,7 @@ pub const ClogSocket = struct {
protocol_data: ProtocolData, protocol_data: ProtocolData,
}; };
// Known protocols /// Known protocols
pub const Protocol = enum { pub const Protocol = enum {
udp_v4, udp_v4,
udp_v6, udp_v6,
@ -21,7 +21,7 @@ pub const Protocol = enum {
tcp_v6, tcp_v6,
}; };
// Protocol specific data /// Protocol specific data
pub const ProtocolData = union(Protocol) { pub const ProtocolData = union(Protocol) {
udp_v4: struct { addr: u32 }, udp_v4: struct { addr: u32 },
udp_v6: struct { addr: u128 }, udp_v6: struct { addr: u128 },
@ -29,15 +29,17 @@ pub const ProtocolData = union(Protocol) {
tcp_v6: struct { addr: u128 }, tcp_v6: struct { addr: u128 },
}; };
// Parses `/proc/net` information into a flat list of ClogSockets. /// Parses `/proc/net` information into a flat list of ClogSockets.
// ///
// Memory is owned by caller. []ClogSocket must be freed by caller. /// Memory is owned by caller. []ClogSocket must be freed by caller.
///
/// If proc_path is `null` will default to `/proc/net`
pub fn parse(alloc: std.mem.Allocator, proc_path: ?[]u8) ![]ClogSocket { pub fn parse(alloc: std.mem.Allocator, proc_path: ?[]u8) ![]ClogSocket {
const base = proc_path orelse "/proc/net";
var buf = std.ArrayList(ClogSocket).init(alloc); var buf = std.ArrayList(ClogSocket).init(alloc);
defer buf.deinit(); // noop we re-own memory to parent at the end defer buf.deinit(); // noop we re-own memory to parent at the end
const base = proc_path orelse "/proc/net";
var arena = std.heap.ArenaAllocator.init(alloc); var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit(); defer arena.deinit();
const child_alloc = arena.allocator(); const child_alloc = arena.allocator();
@ -133,6 +135,12 @@ fn parse_internal(path: []const u8, protocol: Protocol, buf: *std.ArrayList(Clog
test "parse" { test "parse" {
var alloc = std.testing.allocator; var alloc = std.testing.allocator;
const clogs = try parse(alloc, null);
alloc.free(clogs); std.testing.log_level = .info;
const file_path = try std.fs.cwd().realpathAlloc(alloc, "./test/proc1");
defer alloc.free(file_path);
const clogs = try parse(alloc, file_path);
defer alloc.free(clogs);
} }

184
src/process.zig Normal file
View file

@ -0,0 +1,184 @@
//! Find and get information about a processes
const std = @import("std");
pub const Clogs = struct {
items: []ClogProcess,
alloc: std.mem.Allocator,
pub fn format(value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
try writer.print("{any}", .{value.items});
}
fn deinit(self: @This()) void {
for (self.items) |item| {
item.deinit();
}
self.alloc.free(self.items);
}
};
pub const ClogProcess = struct {
// central data
pid: std.posix.pid_t,
inode: std.posix.ino_t,
fd: std.posix.fd_t,
comm: []u8,
exe: []u8,
cmdline: [][]u8,
// for housekeeping
alloc: std.mem.Allocator,
pub fn format(value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
try writer.writeAll("\nClogProcess{\n");
try writer.print("\t.pid={d}, .inode={d}, .fd={d},\n", .{ value.pid, value.inode, value.fd });
try writer.print("\t.comm={s}\n", .{value.comm});
try writer.print("\t.exe={s}\n", .{value.exe});
try writer.print("\t.cmdline={s}\n", .{value.cmdline});
try writer.writeAll("}");
}
fn deinit(self: @This()) void {
self.alloc.free(self.comm);
self.alloc.free(self.exe);
for (self.cmdline) |cl| {
self.alloc.free(cl);
}
self.alloc.free(self.cmdline);
}
};
pub fn find_by_inode(alloc: std.mem.Allocator, inode: std.posix.ino_t, proc_path: ?[]u8) !Clogs {
const base = proc_path orelse "/proc";
var clogs = std.ArrayList(ClogProcess).init(alloc);
defer clogs.deinit(); // noop we re-own memory to parent at the end
const proc_dir = try std.fs.openDirAbsolute(base, .{ .iterate = true });
var process_it = PidIterator{ .it = AccessSafeIterator{ .it = proc_dir.iterate() } };
while (try process_it.next()) |process_entry| {
const process_dir = proc_dir.openDir(process_entry.name, .{ .iterate = true }) catch |err| switch (err) {
error.AccessDenied => continue,
else => return err,
};
const fd_dir = process_dir.openDir("fd", .{ .iterate = true }) catch |err| switch (err) {
error.AccessDenied => continue,
else => return err,
};
var fd_it = FdIterator{ .it = AccessSafeIterator{ .it = fd_dir.iterate() } };
fdit: while (try fd_it.next()) |fd_entry| {
_ = std.fmt.parseInt(std.posix.pid_t, fd_entry.name, 10) catch continue;
const fd_stat = fd_dir.statFile(fd_entry.name) catch |err| switch (err) {
error.AccessDenied => continue,
else => return err,
};
if (fd_stat.inode == inode) {
std.log.debug("Found inode {d} in {s}/{s}", .{ fd_stat.inode, process_entry.name, fd_entry.name });
const pid = try std.fmt.parseInt(std.posix.pid_t, process_entry.name, 10);
for (clogs.items) |clog| {
if (clog.pid == pid) {
std.log.debug("pid {d} already in clogs", .{pid});
continue :fdit;
}
}
var comm_file = process_dir.openFile("comm", .{}) catch |err| switch (err) {
error.AccessDenied => continue,
else => return err,
};
var cmdline_file = process_dir.openFile("cmdline", .{}) catch |err| switch (err) {
error.AccessDenied => continue,
else => return err,
};
var exe_path_buf: [std.fs.max_path_bytes]u8 = undefined;
const exe_path = process_dir.readLink("exe", &exe_path_buf) catch |err| switch (err) {
error.AccessDenied => continue,
else => return err,
};
const comm = try comm_file.readToEndAlloc(alloc, 4096);
defer alloc.free(comm);
var cmdline = std.ArrayList([]u8).init(alloc);
const cmdline_raw = try cmdline_file.readToEndAlloc(alloc, 4096);
defer alloc.free(cmdline_raw);
var cmdline_it = std.mem.splitScalar(u8, cmdline_raw, 0x0);
while (cmdline_it.next()) |cl| {
try cmdline.append(try alloc.dupe(u8, std.mem.trim(u8, cl, "\n")));
}
try clogs.append(ClogProcess{
.pid = pid,
.inode = fd_stat.inode,
.fd = try std.fmt.parseInt(std.posix.pid_t, fd_entry.name, 10),
.comm = try alloc.dupe(u8, std.mem.trim(u8, comm, "\n")),
.cmdline = try cmdline.toOwnedSlice(),
.exe = try alloc.dupe(u8, exe_path),
.alloc = alloc,
});
}
}
}
return Clogs{ .items = try clogs.toOwnedSlice(), .alloc = alloc };
}
const FdIterator = struct {
it: AccessSafeIterator,
pub fn next(self: *@This()) !?std.fs.Dir.Entry {
return try self.it.next();
}
};
/// Iterator over directories that look like a process directory
const PidIterator = struct {
it: AccessSafeIterator,
pub fn next(self: *@This()) !?std.fs.Dir.Entry {
while (try self.it.next()) |entry| {
if (entry.kind == .directory) {
_ = std.fmt.parseInt(std.posix.pid_t, entry.name, 10) catch continue;
return entry;
}
}
return null;
}
};
/// Iterator which ignores `AccessDenied` errors from child iterator
const AccessSafeIterator = struct {
it: std.fs.Dir.Iterator,
pub fn next(self: *@This()) !?std.fs.Dir.Entry {
while (true) {
const elem = self.it.next() catch |err| switch (err) {
error.AccessDenied => continue,
else => return err,
};
if (elem) |e| {
self.it.dir.access(e.name, .{}) catch continue;
}
return elem;
}
return null;
}
};
test "simple" {
std.testing.log_level = .info;
const clogs = try find_by_inode(std.testing.allocator, 3568, null);
defer clogs.deinit();
std.log.info("clogs: {any}", .{clogs});
}

View file

@ -1,11 +0,0 @@
const std = @import("std");
pub const Protocol {
proto: enum {
TCP_V4,
TCP_V6,
UDP_V4,
UDP_V6
},
port_map: std.AutoHashMap(u16, Process);
};

8
test/proc1/tcp Normal file
View file

@ -0,0 +1,8 @@
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 0100007F:0277 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 7987 1 00000000510949ee 99 0 0 10 0
1: 0100007F:0521 00000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 229198 1 000000000cc7de62 99 0 0 10 0
2: 017AA8C0:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 12004 1 000000000e13fb3e 99 0 0 10 5
3: 9600A8C0:91BE 1A71528C:01BB 01 00000000:00000000 02:00005772 00000000 1000 0 312207 2 0000000039cb8c9e 61 3 31 10 -1
4: 9600A8C0:8CF0 5DF36B22:01BB 01 00000000:00000000 02:00001FA2 00000000 1000 0 264134 2 00000000a051e7c1 22 3 31 10 -1
5: 9600A8C0:B0A6 AC4C825E:01BB 01 00000000:00000000 02:00097528 00000000 1000 0 304874 2 000000009579cfcc 22 3 30 10 -1
6: 9600A8C0:D042 AC4C825E:01BB 01 00000000:00000000 02:0005F59F 00000000 1000 0 279726 2 00000000b9879d52 22 3 24 10 -1

7
test/proc1/tcp6 Normal file
View file

@ -0,0 +1,7 @@
sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode
0: 00000000000000000000000001000000:0277 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 0 0 7986 1 00000000a819e168 99 0 0 10 0
1: 0000000000000000FFFF00000100007F:CE5D 00000000000000000000000000000000:0000 0A 00000000:00000000 00:00000000 00000000 1000 0 44943 1 000000009005f71a 99 0 0 10 0
2: 8883022A00E0C11A96B7CA3E649815D5:9B08 00470626000000000000000034B11368:01BB 06 00000000:00000000 03:00001606 00000000 0 0 0 3 00000000f49b65c7
3: 8883022A00E0C11A96B7CA3E649815D5:9A5A 7C060120F02001140000000001000000:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 291092 1 0000000038d2a6ed 23 3 30 10 -1
4: 8883022A00E0C11A96B7CA3E649815D5:E52E 5014002A0C080D40000000000E200000:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 320332 1 00000000ec54adc7 23 3 31 10 -1
5: 8883022A00E0C11A96B7CA3E649815D5:A0BA C011002A000066000000000033000000:01BB 01 00000000:00000000 00:00000000 00000000 1000 0 306175 1 00000000e41ac297 23 3 28 10 -1

6
test/proc1/udp Normal file
View file

@ -0,0 +1,6 @@
sl local_address rem_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops
17624: 017AA8C0:0035 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 12003 2 000000000be6b861 0
17638: 00000000:0043 00000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 12000 2 00000000183b2711 0
17639: 9600A8C0:0044 0100A8C0:0043 01 00000000:00000000 00:00000000 00000000 0 0 276252 2 000000000a09b1ba 0
22924: 00000000:14E9 00000000:0000 07 00000000:00000000 00:00000000 00000000 974 0 3352 2 000000002c689e03 0
31811: 00000000:B7A0 00000000:0000 07 00000000:00000000 00:00000000 00000000 974 0 3354 2 0000000071428bb3 0

5
test/proc1/udp6 Normal file
View file

@ -0,0 +1,5 @@
sl local_address remote_address st tx_queue rx_queue tr tm->when retrnsmt uid timeout inode ref pointer drops
18117: 000080FE000000000716539EF9245F4B:0222 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000 0 0 274919 2 00000000503fce27 0
20307: 00000000000000000000000000000000:8AB0 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000 974 0 3355 2 00000000e8a66559 0
22924: 00000000000000000000000000000000:14E9 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000 974 0 3353 2 00000000f71f77f1 0
30065: 00000000000000000000000000000000:B0CE 00000000000000000000000000000000:0000 07 00000000:00000000 00:00000000 00000000 1000 0 316229 2 000000008792aef6 640