Merge pull request 'Declare the operator's account in the openrazer group (hq ADR 0252, issue 247)' (#125) from feat/module-groups into main

This commit was merged in pull request #125.
This commit is contained in:
2026-10-08 10:09:23 +00:00
5 changed files with 61 additions and 24 deletions
+29 -16
View File
@@ -12,6 +12,7 @@ clients on the session bus.
| the kernel driver's source, built by DKMS for each installed kernel (`razerkbd`, `razermouse`, `razerkraken`, `razeraccessory`) | package `openrazer-driver-dkms` |
| the daemon (`org.razer` on the session bus) and its user unit | package `openrazer-daemon` |
| the client library every front end speaks to the daemon through | package `python-openrazer` |
| the operator's account in the group `openrazer` | a `user` resource naming the account and that group, nothing else of it |
All three from the official repositories (`extra`). On both workstations they are installed today as
dependencies of the AUR tray, and become the mesh's here.
@@ -29,24 +30,33 @@ Not this module's:
No module declares them yet. `openrazer_check` says when the driver is not built for the running
kernel.
- **`dkms` itself**, which the driver package depends on.
- **The account's membership of the `openrazer` group** (below).
- **The account's shell, home and other groups**: the login shell's module's, and the operator's.
## The group: a step for the operator, once
## The group: declared here, and a new login once
The driver's udev rules give each device's files to the group `openrazer`, which the driver package
creates (sysusers). The daemon refuses to start for an account outside that group: *User is not a
member of the openrazer group*.
**On both workstations the account is not in it today, so the daemon has failed at every start**
since openrazer moved from `plugdev` to its own group. The tray runs, and shows no devices. The
account is still in `plugdev`, which openrazer no longer uses. Another device's rules may (the laptop
has a Logitech receiver rule that does), so it stays.
**The module puts the operator's account in `openrazer`** (novox/hq ADR 0252, issue 247). It declares
the account as a `user` resource with that one group and nothing else of it. The `zsh` module declares
the same account to set its shell; the controller lets several modules add groups to one account, and
refuses only two that set its shell or home. The resource is written after the three packages, and the
controller keeps it there, so the group exists when the account is put in it.
The module cannot declare the membership. The host's `user` resource takes groups, additively, but the
`zsh` module already declares the operator's account as its `user` resource. A second module declaring
the same account is refused at composition, as two owners of one name. Until the mesh can add a group
to the account from a second module, this is the operator's step (below), and `openrazer_check` holds
it.
- **The account's other groups are never touched.** The node-engine appends (`usermod --append`).
- **A new login is needed.** The account's running service manager, and the daemon it starts, keep the
groups they started with. The node-engine says so in the apply's outcome, and on every look until the
running manager has the group: the module's resource `openrazer.account`, of kind `account`, is
unhealthy with *relogin needed*, and the controller raises it as openrazer's condition on that machine.
It clears on the first look after a new login.
- **Unassigned, the account leaves `openrazer`**, but only if the mesh put it there and no other module
still asks for it. An account that was in the group before is left in it.
**On both workstations the account is not in it today, so the daemon has failed at every start**
since openrazer moved from `plugdev` to its own group. The tray runs, and shows no devices. The account
is still in `plugdev`, which openrazer no longer uses. Another device's rules may (the laptop has a
Logitech receiver rule that does), so it stays.
## How it starts: D-Bus activation, and nothing else
@@ -78,18 +88,19 @@ root in the tests.
|---|---|---|
| packages | none: the three are installed, 3.12.4, as dependencies of the tray | the same |
| driver | none: built for the running kernel, `razermouse` loaded, a Basilisk V3 Pro bound | the same, a Basilisk V2 bound |
| the account | put in `openrazer`; its other groups as they are | the same |
| daemon | none: the unit stays disabled; it failed at login (not in the group) | the same |
## Migration (ADR 0182)
On each workstation, once:
On each workstation, once, after the push that sends this module's account resource:
1. `sudo gpasswd -a $USER openrazer`
1. The apply puts the account in `openrazer`, and openrazer's health says *relogin needed*.
2. Log out of every session, or reboot. The account's service manager takes its groups when it
starts, and the daemon runs under it.
3. `openrazer_check` answers `ok`, and the tray shows the devices.
3. The condition clears, `openrazer_check` answers `ok`, and the tray shows the devices.
`plugdev` stays. Nothing else is required.
`plugdev` stays. Nothing else is required, and nothing is done with `sudo` by hand.
## Leaves as found
@@ -100,7 +111,9 @@ On each workstation, once:
## Relies on
- **The account in `openrazer`**, by hand (above).
- **A node-engine that gives back what it put the account in, and says a new login is needed** (ADR
0252). An older one puts the account in the group just the same, says nothing of the login, and
never takes the group back.
- **A client to start the daemon.** At login that is the `polychromatic` module's tray helper.
Without a client nothing asks, and nothing needs it to run.
- **The kernel headers of every installed kernel**, for DKMS.
@@ -90,21 +90,36 @@ func TestItInstallsTheStackAndStartsNothing(t *testing.T) {
t.Fatalf("%+v", m)
}
var pkgs []string
for _, r := range m.Resources {
if r["type"] != "package" {
t.Errorf("only packages: %v", r)
var accounts []map[string]any
for i, r := range m.Resources {
switch r["type"] {
case "package":
if len(accounts) > 0 {
t.Errorf("a package after the account: the group is the driver's package's to make first: %v", r)
}
pkgs = append(pkgs, r["package"].(string))
case "user":
accounts = append(accounts, m.Resources[i])
default:
t.Errorf("only packages and the account's group: %v", r)
}
pkgs = append(pkgs, r["package"].(string))
}
if !reflect.DeepEqual(pkgs, packages) {
t.Fatalf("%v", pkgs)
}
// The operator's account in the driver's group, and nothing else of the account: its shell and home
// are the login shell's module's (novox/hq ADR 0252). Written after the packages, so the group the
// driver's package makes exists when the account is put in it.
want := map[string]any{"id": "account", "type": "user", "name": "${machine:account}", "groups": []any{group}}
if len(accounts) != 1 || !reflect.DeepEqual(accounts[0], want) {
t.Fatalf("the account: %v, want %v", accounts, want)
}
if m.Claims != nil || m.Seats != nil || m.Environment != nil || m.Shell != nil || m.Contributions != nil {
t.Fatal("it holds no seat, sets no environment and adds no start")
}
// The tray is polychromatic's, from outside the official repositories; the kernel headers DKMS
// builds against are the kernel's; the account's groups are its user resource's (zsh's).
for _, never := range []string{"polychromatic", "linux-headers", "\"dkms\"", "\"user\"", "groups"} {
// builds against are the kernel's.
for _, never := range []string{"polychromatic", "linux-headers", "\"dkms\"", "\"shell\"", "\"home\""} {
if strings.Contains(raw, never) {
t.Errorf("module.json names %s", never)
}
@@ -445,7 +445,8 @@ func (m *Machine) Check() (CheckAnswer, error) {
add("the group openrazer does not exist", "it comes with openrazer-driver-dkms (sysusers): reinstall it, or `sudo systemd-sysusers`")
case !g.Account:
add("the account is not in the openrazer group: the daemon refuses to start, and the devices' files are the group's",
"`sudo gpasswd -a $USER openrazer`, then log out of every session (or reboot)")
"the module declares the account in the group (novox/hq ADR 0252): send this machine its declaration, "+
"then log out of every session (or reboot); the apply's outcome says why if it did not")
case g.Manager != nil && !*g.Manager:
add("the account is in the openrazer group, but its running service manager started before it was",
"log out of every session (or reboot), so the service manager starts again with the group")
@@ -133,7 +133,7 @@ func TestCheckPassesTheWholeStackAndNamesWhatIsMissing(t *testing.T) {
all = append(all, x.What+" => "+x.Do)
}
got := strings.Join(all, "\n")
for _, want := range []string{"not built for the running kernel 7.3.0-arch1-1", "not in the openrazer group", "gpasswd -a $USER openrazer",
for _, want := range []string{"not built for the running kernel 7.3.0-arch1-1", "not in the openrazer group", "the module declares the account in the group",
"a second start: ~/.config/i3/config:1", "failed at its last start: razer | CRITICAL | User is not a member"} {
if !strings.Contains(got, want) {
t.Errorf("no finding %q in\n%s", want, got)
+8
View File
@@ -24,6 +24,14 @@
"id": "library",
"type": "package",
"package": "python-openrazer"
},
{
"id": "account",
"type": "user",
"name": "${machine:account}",
"groups": [
"openrazer"
]
}
],
"build": {