Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/cli/install/phase.zig
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ pub fn uninstall(allocator: std.mem.Allocator, opts: InstallOptions) !void {
"/lib/systemd/user/padctl.service",
"/lib/udev/rules.d/60-padctl.rules",
"/lib/udev/rules.d/61-padctl-driver-block.rules",
"/lib/udev/rules.d/62-padctl-reprobe.rules",
"/lib/udev/rules.d/90-padctl.rules",
"/lib/udev/rules.d/99-padctl.rules",
"/lib/modules-load.d/padctl.conf",
Expand All @@ -374,6 +375,7 @@ pub fn uninstall(allocator: std.mem.Allocator, opts: InstallOptions) !void {
const etc_rules = [_][]const u8{
"/etc/udev/rules.d/60-padctl.rules",
"/etc/udev/rules.d/61-padctl-driver-block.rules",
"/etc/udev/rules.d/62-padctl-reprobe.rules",
"/etc/udev/rules.d/90-padctl.rules",
"/etc/udev/rules.d/99-padctl.rules",
"/etc/modules-load.d/padctl.conf",
Expand Down
79 changes: 79 additions & 0 deletions src/cli/install/tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const parseHexOrDec = _udev.parseHexOrDec;
const generateUdevRules = _udev.generateUdevRules;
const generateDriverBlockRules = _udev.generateDriverBlockRules;
const generateDriverBlockRulesFromEntries = _udev.generateDriverBlockRulesFromEntries;
const generateReprobeRulesFromEntries = _udev.generateReprobeRulesFromEntries;
const sentinelPath = udev_mod.sentinelPath;
const runtime_sentinel_path = udev_mod.runtime_sentinel_path;
const writeServiceSentinel = udev_mod.writeServiceSentinel;
Expand Down Expand Up @@ -1940,6 +1941,84 @@ test "install: #137 generates ACTION==remove rebind line" {
try testing.expect(std.mem.indexOf(u8, content, "/bind") != null);
}

// FALSIFIABILITY:
// (A) FAILS if ATTRS{idVendor} value is wrong (e.g. "37d8" instead of "37d7") —
// the hard-coded expected string "37d7" will not be found in the output.
// (B) FAILS if the drivers_probe write target is removed or renamed — the
// substring "drivers_probe" will not appear in the output.
// (C) FAILS if the live driverless guard (`test -e`) is removed — the
// "test -e /sys%p/driver" substring will not match.
// (D) FAILS if ATTRS{idProduct} does not encode to "2401".
// (E) FAILS if ACTION is wrong (e.g. "add" only, missing "|bind").
// (F) FAILS if DEVTYPE is wrong (e.g. "usb_device").
test "install: #355 generateReprobeRulesFromEntries generates correct rule for Vader 5" {
const testing = std.testing;
const allocator = testing.allocator;

var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const tmp_path = try tmp.dir.realpathAlloc(allocator, ".");
defer allocator.free(tmp_path);

const rules_path = try std.fmt.allocPrint(allocator, "{s}/62.rules", .{tmp_path});
defer allocator.free(rules_path);

const entries = [_]UdevEntry{.{
.name = "Flydigi Vader 5",
.vid = 0x37d7,
.pid = 0x2401,
}};
try generateReprobeRulesFromEntries(allocator, &entries, rules_path);

const content = try readRulesFile(allocator, rules_path);
defer allocator.free(content);

// (A) vid match
try testing.expect(std.mem.indexOf(u8, content, "ATTRS{idVendor}==\"37d7\"") != null);
// (D) pid match
try testing.expect(std.mem.indexOf(u8, content, "ATTRS{idProduct}==\"2401\"") != null);
// (E) action includes |bind
try testing.expect(std.mem.indexOf(u8, content, "ACTION==\"add|bind\"") != null);
// (F) interface devtype
try testing.expect(std.mem.indexOf(u8, content, "ENV{DEVTYPE}==\"usb_interface\"") != null);
// (B) drivers_probe write target
try testing.expect(std.mem.indexOf(u8, content, "drivers_probe") != null);
// (C) live driverless guard
try testing.expect(std.mem.indexOf(u8, content, "test -e /sys%p/driver") != null);
}

// FALSIFIABILITY: FAILS if the loop-safety condition is removed — a bound
// interface (driver symlink present) must NOT be re-probed. We verify the
// guard is literally present; removing it would cause spurious re-probes on
// already-bound interfaces (idem-potency breach).
test "install: #355 reprobe rule has live driverless guard on every entry line" {
const testing = std.testing;
const allocator = testing.allocator;

var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const tmp_path = try tmp.dir.realpathAlloc(allocator, ".");
defer allocator.free(tmp_path);

const rules_path = try std.fmt.allocPrint(allocator, "{s}/62.rules", .{tmp_path});
defer allocator.free(rules_path);

const entries = [_]UdevEntry{
.{ .name = "Device A", .vid = 0x37d7, .pid = 0x2401 },
.{ .name = "Device B", .vid = 0x0f0d, .pid = 0x00c1 },
};
try generateReprobeRulesFromEntries(allocator, &entries, rules_path);

const content = try readRulesFile(allocator, rules_path);
defer allocator.free(content);

var it = std.mem.splitScalar(u8, content, '\n');
while (it.next()) |line| {
if (std.mem.indexOf(u8, line, "drivers_probe") == null) continue;
try testing.expect(std.mem.indexOf(u8, line, "test -e /sys%p/driver") != null);
}
}

test "install: staged driver-block udev rule uses runtime sentinel path" {
const testing = std.testing;
const allocator = testing.allocator;
Expand Down
40 changes: 40 additions & 0 deletions src/cli/install/udev.zig
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,14 @@ pub fn installUdevRules(allocator: std.mem.Allocator, plan: *const InstallPlan,
const msg = std.fmt.bufPrint(&errbuf, "warning: driver block rules not generated: {}\n", .{err}) catch "warning: driver block rules error\n";
_ = std.posix.write(std.posix.STDERR_FILENO, msg) catch {};
};

const reprobe_rules_path = try std.fmt.allocPrint(allocator, "{s}/62-padctl-reprobe.rules", .{plan.udev_dir});
defer allocator.free(reprobe_rules_path);
generateReprobeRulesFromEntries(allocator, entries, reprobe_rules_path) catch |err| {
var errbuf: [256]u8 = undefined;
const msg = std.fmt.bufPrint(&errbuf, "warning: reprobe rules not generated: {}\n", .{err}) catch "warning: reprobe rules error\n";
_ = std.posix.write(std.posix.STDERR_FILENO, msg) catch {};
};
}

/// Mutate live kernel driver state to match the freshly written udev rules.
Expand Down Expand Up @@ -332,6 +340,7 @@ pub fn cleanupLegacyUdevFiles(allocator: std.mem.Allocator, plan: *const Install
const basenames = [_][]const u8{
"60-padctl.rules",
"61-padctl-driver-block.rules",
"62-padctl-reprobe.rules",
"90-padctl.rules",
"99-padctl.rules",
};
Expand Down Expand Up @@ -558,6 +567,36 @@ pub fn generateDriverBlockRulesFromEntries(allocator: std.mem.Allocator, entries
try f.writeAll(buf.items);
}

/// Generate 62-padctl-reprobe.rules: one rule per entry that re-probes a
/// driverless USB interface at coldplug/hotplug so a device already attached at
/// boot gets a hidraw node without a physical replug.
///
/// The RUN+= shell snippet live-checks `test -e /sys%p/driver` (%p/%k are
/// substituted by udevd, matching the sibling driver-block rule) at execution
/// time rather than guarding on ENV{DRIVER}=="" (stale env). An
/// already-bound interface passes the test and the write is skipped; a driverless
/// interface fails and the interface name is written to drivers_probe. The
/// drivers_probe write is itself idempotent.
pub fn generateReprobeRulesFromEntries(allocator: std.mem.Allocator, entries: []const UdevEntry, rules_path: []const u8) !void {
var buf = std.ArrayList(u8){};
defer buf.deinit(allocator);
try buf.appendSlice(allocator, "# Auto-generated by padctl install — boot reprobe rules\n");

for (entries) |e| {
const line = try std.fmt.allocPrint(
allocator,
"ACTION==\"add|bind\", SUBSYSTEM==\"usb\", ENV{{DEVTYPE}}==\"usb_interface\", ATTRS{{idVendor}}==\"{x:0>4}\", ATTRS{{idProduct}}==\"{x:0>4}\", RUN+=\"/bin/sh -c 'test -e /sys%p/driver || echo %k > /sys/bus/usb/drivers_probe'\"\n# {s}\n",
.{ e.vid, e.pid, e.name },
);
defer allocator.free(line);
try buf.appendSlice(allocator, line);
}

var f = try std.fs.createFileAbsolute(rules_path, .{ .truncate = true });
defer f.close();
try f.writeAll(buf.items);
}

/// Collects entries then generates driver block rules.
fn generateDriverBlockRules(allocator: std.mem.Allocator, dirs: []const []const u8, rules_path: []const u8) !void {
var entries = try collectDeviceEntries(allocator, dirs);
Expand Down Expand Up @@ -989,6 +1028,7 @@ pub const _internals_for_tests = struct {
pub const generateUdevRules = @import("udev.zig").generateUdevRules;
pub const generateDriverBlockRules = @import("udev.zig").generateDriverBlockRules;
pub const generateDriverBlockRulesFromEntries = @import("udev.zig").generateDriverBlockRulesFromEntries;
pub const generateReprobeRulesFromEntries = @import("udev.zig").generateReprobeRulesFromEntries;
};

// setupTestUdev writes a udev rule that grants world-read access to UHID virtual
Expand Down
Loading