Finish restructuring, remove old code

This commit is contained in:
Armin Friedl 2024-07-06 17:56:18 +02:00
parent 72ba68744e
commit 056da26cc0
8 changed files with 82 additions and 446 deletions

View file

@ -15,25 +15,13 @@ pub fn build(b: *std.Build) void {
// set a preferred release mode, allowing the user to decide how to optimize.
const optimize = b.standardOptimizeOption(.{});
const relib = b.addStaticLibrary(.{
.name = "regex_slim",
.optimize = .Debug,
.target = target,
});
relib.addIncludePath(.{ .path = "lib" });
relib.addCSourceFile(.{ .file = b.path("lib/regex_slim.c") });
relib.linkLibC();
const exe = b.addExecutable(.{
.name = "unclog",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
exe.addIncludePath(.{ .path = "lib" });
exe.linkLibC();
exe.linkLibrary(relib);
// This declares intent for the executable to be installed into the
// standard location when the user invokes the "install" step (the default
@ -65,17 +53,15 @@ pub fn build(b: *std.Build) void {
// Creates a step for unit testing. This only builds the test executable
// but does not run it.
const procnet_unit_tests = b.addTest(.{
.root_source_file = b.path("src/procnet.zig"),
const sockets_unit_tests = b.addTest(.{
.root_source_file = b.path("src/sockets.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
procnet_unit_tests.addIncludePath(.{ .path = "lib" });
procnet_unit_tests.linkLibrary(relib);
sockets_unit_tests.addIncludePath(.{ .path = "lib" });
const run_procnet_unit_tests = b.addRunArtifact(procnet_unit_tests);
run_procnet_unit_tests.has_side_effects = true; // needed so tests aren't cached
const run_sockets_unit_tests = b.addRunArtifact(sockets_unit_tests);
run_sockets_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.
@ -83,10 +69,8 @@ pub fn build(b: *std.Build) void {
.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
@ -95,7 +79,6 @@ pub fn build(b: *std.Build) void {
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.link_libc = true,
});
exe_unit_tests.addIncludePath(.{ .path = "lib" });
@ -105,7 +88,7 @@ pub fn build(b: *std.Build) void {
// the `zig build --help` menu, providing a way for the user to request
// running the unit tests.
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_procnet_unit_tests.step);
test_step.dependOn(&run_sockets_unit_tests.step);
test_step.dependOn(&run_process_unit_tests.step);
test_step.dependOn(&run_exe_unit_tests.step);
}

View file

@ -1,9 +0,0 @@
#include "regex_slim.h"
regex_t* alloc_regex_t(void) {
return (regex_t*)malloc(sizeof(regex_t));
}
void free_regex_t(regex_t* ptr) {
free(ptr);
}

View file

@ -1,8 +0,0 @@
// Workaround for using regex in Zig.
// https://www.openmymind.net/Regular-Expressions-in-Zig/
// https://stackoverflow.com/questions/73086494/how-to-allocate-a-struct-of-incomplete-type-in-zig
#include <regex.h>
#include <stdlib.h>
regex_t* alloc_regex_t(void);
void free_regex_t(regex_t* ptr);

View file

@ -1,14 +1,8 @@
const std = @import("std");
const proc = @import("procnet.zig");
const pid = @import("procpid.zig");
const clog = @import("proc.zig");
const proces = @import("process.zig");
const c = @cImport({
@cInclude("arpa/inet.h");
@cInclude("signal.h");
});
const sockets = @import("sockets.zig");
const process = @import("process.zig");
pub const std_options = .{ .log_level = .err };
pub const std_options = .{ .log_level = .info };
pub fn main() !void {
var argsit = std.process.args();
@ -20,99 +14,19 @@ pub fn main() !void {
const alloc = gpa.allocator();
var clogged = false;
const clog_sockets = try sockets.parse(alloc, null);
defer alloc.free(clog_sockets);
var procs = std.ArrayList(pid.Process).init(alloc);
defer procs.deinit();
const tcp = try proc.read_proc_net(alloc, .V4, "/proc/net/tcp");
defer alloc.free(tcp);
for (tcp) |pn| {
if (pn.V4.port == port) {
clogged = true;
try print_proc_net(&pn, "TCP/IPv4");
try append_processes(alloc, pn.V4.inode, &procs);
}
}
const tcp6 = try proc.read_proc_net(alloc, .V6, "/proc/net/tcp6");
defer alloc.free(tcp6);
for (tcp6) |pn| {
if (pn.V6.port == port) {
clogged = true;
try print_proc_net(&pn, "TCP/IPv6");
try append_processes(alloc, pn.V6.inode, &procs);
}
}
const udp = try proc.read_proc_net(alloc, .V4, "/proc/net/udp");
defer alloc.free(udp);
for (udp) |pn| {
if (pn.V4.port == port) {
clogged = true;
try print_proc_net(&pn, "UDP/IPv4");
try append_processes(alloc, pn.V4.inode, &procs);
}
}
const udp6 = try proc.read_proc_net(alloc, .V6, "/proc/net/udp6");
defer alloc.free(udp6);
for (udp6) |pn| {
if (pn.V6.port == port) {
clogged = true;
try print_proc_net(&pn, "UDP/IPv6");
try append_processes(alloc, pn.V6.inode, &procs);
}
}
if (!clogged) {
try std.io.getStdOut().writer().print("Port {d} looks unclogged already\n", .{port});
} else {
_ = try std.io.getStdOut().writer().write("Kill? ");
var buf: [10]u8 = undefined;
if (try std.io.getStdIn().reader().readUntilDelimiterOrEof(buf[0..], '\n')) |input| {
const killproc = try std.fmt.parseInt(usize, input, 10);
const process = procs.items[killproc - 1];
_ = c.kill(@intCast(process.proc_pid.pid), c.SIGTERM);
}
}
}
fn append_processes(alloc: std.mem.Allocator, inode: u32, buf: *std.ArrayList(pid.Process)) !void {
const pids = try pid.find_proc(alloc, inode);
defer alloc.free(pids);
try std.io.getStdOut().writer().print("\t{s: <5}{s: <10}{s: <20}{s}\n", .{ "#", "PID", "CMD", "ARGS" });
for (pids) |p| {
const process = try pid.resolve_process(alloc, p);
defer process.deinit();
try buf.append(process);
const cmdline = try std.mem.join(alloc, " ", process.cmdline[1..]);
defer alloc.free(cmdline);
try std.io.getStdOut().writer().print("\t{d: <5}{d: <10}{s: <20}{s}\n", .{ buf.items.len, process.proc_pid.pid, process.comm[0 .. process.comm.len - 1], cmdline });
}
_ = try std.io.getStdOut().writer().write("\n");
}
fn print_proc_net(entry: *const proc.ProcNet, addr_type: []const u8) !void {
var stdio = std.io.getStdOut();
switch (entry.*) {
.V4 => |v4| {
const src_addr_pp = c.inet_ntoa(.{ .s_addr = v4.addr }); // allocates static global buffer, don't free
try stdio.writer().print("Port {d} clogged on {s} Address {s} with socket inode {d}\n", .{ v4.port, addr_type, src_addr_pp, v4.inode });
},
.V6 => |v6| {
try stdio.writer().print("Port {d} clogged on {s} Address ", .{ v6.port, addr_type });
for (@as(*const [16]u8, @ptrCast(&v6.addr)), 0..) |h, idx| {
if (idx % 2 == 0 and idx != 0) {
_ = try stdio.writer().write(":");
}
try stdio.writer().print("{X:0<2}", .{h});
for (clog_sockets) |clog_socket| {
if (clog_socket.port == port) {
const p = try process.find_by_inode(alloc, clog_socket.inode, null);
defer p.deinit();
switch (clog_socket.protocol_data) {
.tcp_v4 => |v4| std.log.info("Found process {any} clogging address {any}", .{ p, v4.addr }),
.udp_v4 => |v4| std.log.info("Found process {any} clogging address {any}", .{ p, v4.addr }),
.tcp_v6 => |v6| std.log.info("Found process {any} clogging address {any}", .{ p, v6.addr }),
.udp_v6 => |v6| std.log.info("Found process {any} clogging address {any}", .{ p, v6.addr }),
}
try stdio.writer().print(" with socket inode {d}\n", .{v6.inode});
},
}
}
}

View file

@ -2,6 +2,10 @@
const std = @import("std");
/// A simple array of clogging processes. Mainly a memory-owning wrapper for
/// handling deallocation correctly. Call `deinit` to deallocate the memory.
///
/// Processes can be accessed via `.items`
pub const Clogs = struct {
items: []ClogProcess,
alloc: std.mem.Allocator,
@ -10,7 +14,7 @@ pub const Clogs = struct {
try writer.print("{any}", .{value.items});
}
fn deinit(self: @This()) void {
pub fn deinit(self: @This()) void {
for (self.items) |item| {
item.deinit();
}
@ -18,6 +22,7 @@ pub const Clogs = struct {
}
};
/// Process metadata of a clogging process
pub const ClogProcess = struct {
// central data
pid: std.posix.pid_t,
@ -49,24 +54,29 @@ pub const ClogProcess = struct {
}
};
pub fn find_by_inode(alloc: std.mem.Allocator, inode: std.posix.ino_t, proc_path: ?[]u8) !Clogs {
/// Find clogging processes that hold a file handle on an inode
pub fn find_by_inode(alloc: std.mem.Allocator, inode: std.posix.ino_t, proc_path: ?[]const 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
defer clogs.deinit(); // noop we re-own memory at the end
const proc_dir = try std.fs.openDirAbsolute(base, .{ .iterate = true });
var proc_dir = try std.fs.openDirAbsolute(base, .{ .iterate = true });
defer proc_dir.close();
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) {
var 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) {
defer process_dir.close();
var fd_dir = process_dir.openDir("fd", .{ .iterate = true }) catch |err| switch (err) {
error.AccessDenied => continue,
else => return err,
};
defer fd_dir.close();
var fd_it = FdIterator{ .it = AccessSafeIterator{ .it = fd_dir.iterate() } };
fdit: while (try fd_it.next()) |fd_entry| {
@ -87,39 +97,15 @@ pub fn find_by_inode(alloc: std.mem.Allocator, inode: std.posix.ino_t, proc_path
}
}
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")));
}
const pfd: ProcessFileData = try parse_process_files(alloc, &process_dir) orelse continue;
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),
.comm = pfd.comm,
.cmdline = pfd.cmdline,
.exe = pfd.exe,
.alloc = alloc,
});
}
@ -129,6 +115,46 @@ pub fn find_by_inode(alloc: std.mem.Allocator, inode: std.posix.ino_t, proc_path
return Clogs{ .items = try clogs.toOwnedSlice(), .alloc = alloc };
}
const ProcessFileData = struct {
comm: []u8,
cmdline: [][]u8,
exe: []u8,
};
fn parse_process_files(alloc: std.mem.Allocator, process_dir: *const std.fs.Dir) !?ProcessFileData {
var comm_file = process_dir.openFile("comm", .{}) catch |err| switch (err) {
error.AccessDenied => return null,
else => return err,
};
var cmdline_file = process_dir.openFile("cmdline", .{}) catch |err| switch (err) {
error.AccessDenied => return null,
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 => return null,
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")));
}
return .{
.comm = try alloc.dupe(u8, std.mem.trim(u8, comm, "\n")),
.cmdline = try cmdline.toOwnedSlice(),
.exe = try alloc.dupe(u8, exe_path),
};
}
const FdIterator = struct {
it: AccessSafeIterator,
@ -178,7 +204,7 @@ const AccessSafeIterator = struct {
test "simple" {
std.testing.log_level = .info;
const clogs = try find_by_inode(std.testing.allocator, 3568, null);
const clogs = try find_by_inode(std.testing.allocator, 721, null);
defer clogs.deinit();
std.log.info("clogs: {any}", .{clogs});
}

View file

@ -1,166 +0,0 @@
const std = @import("std");
const log = std.log;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const c = @cImport({
@cInclude("arpa/inet.h");
});
pub const ProcNet = union(enum) {
V4: struct { addr: u32, port: u16, inode: u32 },
V6: struct { addr: u128, port: u16, inode: u32 },
pub fn format(value: @This(), comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
switch (value) {
.V4 => |v4| {
const src_addr_pp = c.inet_ntoa(.{ .s_addr = v4.addr }); // allocates static global buffer, don't free
try writer.print("{s: <15}{d: <10}{d}", .{ src_addr_pp, v4.port, v4.inode });
},
.V6 => |v6| {
for (@as(*const [16]u8, @ptrCast(&v6.addr)), 0..) |h, idx| {
if (idx % 2 == 0 and idx != 0) {
_ = try writer.write(":");
}
try writer.print("{X:0<2}", .{h});
}
try writer.print("\t{d: <10}{d}", .{ v6.port, v6.inode });
},
}
}
};
/// Read a protocol file in /proc/net
///
/// The memory of the resulting []ProcNet is caller owned and must be freed by
/// the caller
pub fn read_proc_net(alloc: Allocator, comptime addr_len: enum { V4, V6 }, path: []const u8) ![]ProcNet {
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
const proc_net_parsed: [][3][]u8 = try parse_proc_net(arena.allocator(), path);
var buf = try std.ArrayList(ProcNet).initCapacity(alloc, proc_net_parsed.len);
defer buf.deinit();
const addr_len_t = switch (addr_len) {
.V4 => u32,
.V6 => u128,
};
for (proc_net_parsed) |line| {
const src_addr = try std.fmt.parseUnsigned(addr_len_t, line[0], 16);
const src_port = try std.fmt.parseUnsigned(u16, line[1], 16);
const inode = try std.fmt.parseUnsigned(u32, line[2], 10);
try buf.append(switch (addr_len) {
.V4 => ProcNet{ .V4 = .{ .addr = src_addr, .port = src_port, .inode = inode } },
.V6 => ProcNet{ .V6 = .{ .addr = src_addr, .port = src_port, .inode = inode } },
});
}
return buf.toOwnedSlice();
}
fn parse_proc_net(alloc: Allocator, path: []const u8) ![][3][]u8 {
var proc_net_file = try std.fs.openFileAbsolute(path, .{});
defer proc_net_file.close();
var buf_reader = std.io.bufferedReader(proc_net_file.reader());
var reader = buf_reader.reader();
try reader.skipUntilDelimiterOrEof('\n'); // skip header
// allocates caller-owned memory
var res = std.ArrayList([3][]u8).init(alloc);
defer res.deinit();
// used for internal allocations, owned
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
while (try reader.readUntilDelimiterOrEofAlloc(arena.allocator(), '\n', 256)) |line| {
var tokens = std.ArrayList([]const u8).init(arena.allocator()); // buffer for a line in the proc file split by tokenizer
defer tokens.deinit();
var tokenizer = std.mem.tokenize(u8, line, " \t\n");
while (tokenizer.next()) |elem| {
try tokens.append(elem);
}
var src_it = std.mem.splitScalar(u8, tokens.items[1], ':');
try res.append(.{ try alloc.dupe(u8, src_it.next().?), try alloc.dupe(u8, src_it.next().?), try alloc.dupe(u8, tokens.items[9]) });
}
return res.toOwnedSlice();
}
test "basic 3" {
std.testing.log_level = .info;
const alloc = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
const res = try parse_proc_net(arena.allocator(), "/proc/net/tcp");
for (res) |line| {
for (line) |field| {
std.debug.print("{s}\t", .{field});
}
std.debug.print("\n", .{});
}
}
test "basic 4" {
std.testing.log_level = .info;
const alloc = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
const res = try read_proc_net(arena.allocator(), .V4, "/proc/net/tcp");
for (res) |pn| {
std.debug.print("{any}\n", .{pn});
}
}
test "basic 5" {
std.testing.log_level = .info;
const alloc = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
const res = try read_proc_net(arena.allocator(), .V6, "/proc/net/tcp6");
for (res) |pn| {
std.debug.print("{any}\n", .{pn});
}
}
test "basic 6" {
std.testing.log_level = .info;
const alloc = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
const res = try read_proc_net(arena.allocator(), .V4, "/proc/net/udp");
for (res) |pn| {
std.debug.print("{any}\n", .{pn});
}
}
test "basic 7" {
std.testing.log_level = .info;
const alloc = std.testing.allocator;
var arena = std.heap.ArenaAllocator.init(alloc);
defer arena.deinit();
const res = try read_proc_net(arena.allocator(), .V6, "/proc/net/udp6");
for (res) |pn| {
std.debug.print("{any}\n", .{pn});
}
}

View file

@ -1,104 +0,0 @@
const std = @import("std");
const log = std.log;
const testing = std.testing;
const Allocator = std.mem.Allocator;
const c = @cImport({
@cInclude("regex.h");
@cInclude("regex_slim.h");
});
pub const ProcPid = struct {
pid: u32,
inode: u32,
fd: u32,
};
pub const Process = struct {
proc_pid: ProcPid,
comm: []u8,
cmdline: [][]u8,
alloc: std.heap.ArenaAllocator,
pub fn deinit(self: @This()) void {
self.alloc.deinit();
}
};
pub fn find_proc(alloc: Allocator, inode: u32) ![]ProcPid {
var proc_dir = try std.fs.openDirAbsolute("/proc", .{ .iterate = true });
defer proc_dir.close();
const regex_t = c.alloc_regex_t() orelse {
log.err("Could not allocate regex_t memory", .{});
return error.RegexAllocFailed;
};
defer c.free_regex_t(regex_t);
if (c.regcomp(regex_t, "^[0-9]\\{1,\\}/fd/[0-9]\\{1,\\}$", c.REG_NOSUB) != 0) {
return error.REGCOMP;
}
var walker = try proc_dir.walk(alloc);
defer walker.deinit();
var buf = std.ArrayList(ProcPid).init(alloc);
defer buf.deinit();
while (true) {
const entry = walker.next() catch |err| switch (err) {
error.AccessDenied => continue,
else => return err,
} orelse {
log.info("No more entry. Exiting.", .{});
break;
};
if (c.regexec(regex_t, entry.path, 0, null, 0) == 0) {
const stat = proc_dir.statFile(entry.path) catch |err| switch (err) {
error.AccessDenied => continue,
else => return err,
};
if (stat.kind == .unix_domain_socket) {
if (stat.inode == inode) {
log.info("Found procpid path {s}", .{entry.path});
// <pid>/fd/<fd>
var compit = try std.fs.path.componentIterator(entry.path);
const pid = try std.fmt.parseInt(u32, compit.next().?.name, 10); // parse <pid>
_ = compit.next().?.name; // skip /fd/
const fd = try std.fmt.parseInt(u32, compit.next().?.name, 10); // parse <fd>
try buf.append(ProcPid{ .pid = pid, .inode = inode, .fd = fd });
}
}
}
}
return buf.toOwnedSlice();
}
pub fn resolve_process(alloc: Allocator, proc_pid: ProcPid) !Process {
var fmt_buf = [_]u8{0} ** 20;
const path = try std.fmt.bufPrint(&fmt_buf, "/proc/{d}", .{proc_pid.pid});
const proc_dir = try std.fs.openDirAbsolute(path, .{});
var arena_alloc = std.heap.ArenaAllocator.init(alloc);
const comm_file = try proc_dir.openFile("comm", .{});
const comm = try comm_file.reader().readAllAlloc(arena_alloc.allocator(), 4096);
const cmdline_file = try proc_dir.openFile("cmdline", .{});
const cmdline_raw = try cmdline_file.reader().readAllAlloc(alloc, 4096);
defer alloc.free(cmdline_raw);
var cmdline_it = std.mem.splitScalar(u8, cmdline_raw, 0x0);
var cmdline_buf = std.ArrayList([]u8).init(arena_alloc.allocator());
defer cmdline_buf.deinit();
while (cmdline_it.next()) |cmdline_elem| {
try cmdline_buf.append(try arena_alloc.allocator().dupe(u8, cmdline_elem));
}
return Process{ .proc_pid = proc_pid, .comm = comm, .cmdline = try cmdline_buf.toOwnedSlice(), .alloc = arena_alloc };
}