diff --git a/src/cli/install/phase.zig b/src/cli/install/phase.zig index b4fb591..b3c2174 100644 --- a/src/cli/install/phase.zig +++ b/src/cli/install/phase.zig @@ -170,12 +170,19 @@ pub fn run(allocator: std.mem.Allocator, opts: InstallOptions) !void { udev.removeServiceSentinel(allocator, plan.opts.destdir); } - if (plan.do_enable_systemctl) { - services.runSystemctlPhase(&plan); - } else if (!plan.staging_mode) { + // Order is load-bearing: reload the udev ruleset, THEN mutate live driver + // state, THEN start the service. applyDriverState re-probes driverless + // interfaces, which generates bind uevents udevd evaluates against its + // loaded ruleset — so a stale "block usbhid" rule must be unloaded first, + // or it re-unbinds usbhid and the device loses its hidraw node (#355). + if (plan.do_enable_systemctl or !plan.staging_mode) { _ = std.posix.write(std.posix.STDOUT_FILENO, "\nReloading system daemons...\n") catch {}; runCmd(&.{ "udevadm", "control", "--reload-rules" }); runCmd(&.{ "udevadm", "trigger" }); + udev.applyDriverState(allocator, &plan, device_entries.items); + } + if (plan.do_enable_systemctl) { + services.runSystemctlUnits(&plan); } if (mapping_failed) { diff --git a/src/cli/install/services.zig b/src/cli/install/services.zig index 81fbc3d..ee4e647 100644 --- a/src/cli/install/services.zig +++ b/src/cli/install/services.zig @@ -480,11 +480,10 @@ pub fn installReconnectScript(allocator: std.mem.Allocator, plan: *const Install _ = std.posix.write(std.posix.STDOUT_FILENO, "\n") catch {}; } -pub fn runSystemctlPhase(plan: *const InstallPlan) void { - _ = std.posix.write(std.posix.STDOUT_FILENO, "\nReloading system daemons...\n") catch {}; - runCmd(&.{ "udevadm", "control", "--reload-rules" }); - runCmd(&.{ "udevadm", "trigger" }); - +// Manage the systemd units. The caller reloads the udev ruleset and applies +// driver state first, so the service starts against a device already in its +// final driver state. +pub fn runSystemctlUnits(plan: *const InstallPlan) void { if (plan.systemctl_plan.mode == .skip) { var groups: [3][]const []const u8 = undefined; var n: usize = 0; diff --git a/src/cli/install/udev.zig b/src/cli/install/udev.zig index 970c622..f5565a0 100644 --- a/src/cli/install/udev.zig +++ b/src/cli/install/udev.zig @@ -290,6 +290,16 @@ 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 {}; }; +} + +/// Mutate live kernel driver state to match the freshly written udev rules. +/// MUST be called AFTER `udevadm control --reload-rules`: re-probe generates +/// bind uevents that udevd evaluates against its currently loaded ruleset, so a +/// stale "block usbhid" rule still loaded from a previous install would re-unbind +/// usbhid and strip the device's hidraw node ("no device"). Self-gates to a +/// no-op outside root or in staging mode. +pub fn applyDriverState(allocator: std.mem.Allocator, plan: *const InstallPlan, entries: []const UdevEntry) void { + if (plan.staging_mode or !plan.is_root) return; // Re-probe interfaces left driverless by an earlier install whose block // list this install no longer covers, BEFORE evicting currently-blocked @@ -300,17 +310,14 @@ pub fn installUdevRules(allocator: std.mem.Allocator, plan: *const InstallPlan, // skips bound ones, so it never re-binds a driver the unbind step below is // about to evict. Not gated by shouldProactiveUnbind so it still recovers // when the block list is empty/reduced. - if (!plan.staging_mode and plan.is_root) { - probeAndReprobeDrivers(allocator, entries, ""); - } - - // Evict already-attached devices without waiting for reboot. udevadm - // trigger only sends add events; bind rules fire on the next plug cycle. - // Proactively writing to sysfs unbind covers devices already claimed by a - // blocking driver at install time — but only when this install actually - // starts a runnable service, otherwise an unbound device is left ownerless. - // Runs last so the eviction is authoritative over the reprobe above. - if (!plan.staging_mode and plan.is_root and shouldProactiveUnbind(plan)) { + probeAndReprobeDrivers(allocator, entries, ""); + + // Evict already-attached devices without waiting for reboot. Proactively + // writing to sysfs unbind covers devices already claimed by a blocking + // driver at install time — but only when this install actually starts a + // runnable service, otherwise an unbound device is left ownerless. Runs + // last so the eviction is authoritative over the reprobe above. + if (shouldProactiveUnbind(plan)) { probeAndUnbindDrivers(allocator, entries, ""); } }