udevmon 0.8.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. udevmon-0.8.1/CHANGELOG.md +430 -0
  2. udevmon-0.8.1/CLAUDE.md +32 -0
  3. udevmon-0.8.1/DEVELOPMENT.md +156 -0
  4. udevmon-0.8.1/LICENSE +674 -0
  5. udevmon-0.8.1/MANIFEST.in +9 -0
  6. udevmon-0.8.1/PKG-INFO +1575 -0
  7. udevmon-0.8.1/README.md +1538 -0
  8. udevmon-0.8.1/SPECIFICATIONS.md +1176 -0
  9. udevmon-0.8.1/contrib/rclone-config/LICENSE +674 -0
  10. udevmon-0.8.1/contrib/rclone-config/README.md +147 -0
  11. udevmon-0.8.1/contrib/rclone-config/pyproject.toml +25 -0
  12. udevmon-0.8.1/contrib/rclone-config/rclone_config.py +402 -0
  13. udevmon-0.8.1/contrib/rclone-config/udevmon.toml.example +24 -0
  14. udevmon-0.8.1/docs/_ext/cli_help.py +45 -0
  15. udevmon-0.8.1/docs/_ext/module_reference.py +49 -0
  16. udevmon-0.8.1/docs/_static/layout.css +59 -0
  17. udevmon-0.8.1/docs/api.rst +29 -0
  18. udevmon-0.8.1/docs/cli.rst +8 -0
  19. udevmon-0.8.1/docs/conf.py +31 -0
  20. udevmon-0.8.1/docs/contributing.rst +51 -0
  21. udevmon-0.8.1/docs/getting-started.rst +34 -0
  22. udevmon-0.8.1/docs/index.rst +23 -0
  23. udevmon-0.8.1/docs/integration.rst +85 -0
  24. udevmon-0.8.1/docs/internals.rst +60 -0
  25. udevmon-0.8.1/docs/modules.rst +8 -0
  26. udevmon-0.8.1/docs/requirements.txt +3 -0
  27. udevmon-0.8.1/man/udevmon.1 +958 -0
  28. udevmon-0.8.1/man/udevmon.toml.5 +1217 -0
  29. udevmon-0.8.1/pyproject.toml +48 -0
  30. udevmon-0.8.1/setup.cfg +4 -0
  31. udevmon-0.8.1/setup.py +34 -0
  32. udevmon-0.8.1/src/udevmon/__init__.py +18 -0
  33. udevmon-0.8.1/src/udevmon/__main__.py +7 -0
  34. udevmon-0.8.1/src/udevmon/_scm.py +108 -0
  35. udevmon-0.8.1/src/udevmon/actions.py +306 -0
  36. udevmon-0.8.1/src/udevmon/cli.py +1044 -0
  37. udevmon-0.8.1/src/udevmon/completion.py +327 -0
  38. udevmon-0.8.1/src/udevmon/config.py +787 -0
  39. udevmon-0.8.1/src/udevmon/configedit.py +893 -0
  40. udevmon-0.8.1/src/udevmon/control.py +179 -0
  41. udevmon-0.8.1/src/udevmon/daemon.py +260 -0
  42. udevmon-0.8.1/src/udevmon/engine.py +238 -0
  43. udevmon-0.8.1/src/udevmon/events.py +113 -0
  44. udevmon-0.8.1/src/udevmon/explain.py +112 -0
  45. udevmon-0.8.1/src/udevmon/matching.py +201 -0
  46. udevmon-0.8.1/src/udevmon/notify.py +66 -0
  47. udevmon-0.8.1/src/udevmon/pause.py +88 -0
  48. udevmon-0.8.1/src/udevmon/runtime.py +30 -0
  49. udevmon-0.8.1/src/udevmon/scan.py +373 -0
  50. udevmon-0.8.1/src/udevmon/service.py +91 -0
  51. udevmon-0.8.1/src/udevmon/session.py +205 -0
  52. udevmon-0.8.1/src/udevmon/sources/__init__.py +3 -0
  53. udevmon-0.8.1/src/udevmon/sources/udev.py +179 -0
  54. udevmon-0.8.1/src/udevmon/sources/udisks.py +302 -0
  55. udevmon-0.8.1/src/udevmon/state.py +51 -0
  56. udevmon-0.8.1/src/udevmon.egg-info/PKG-INFO +1575 -0
  57. udevmon-0.8.1/src/udevmon.egg-info/SOURCES.txt +107 -0
  58. udevmon-0.8.1/src/udevmon.egg-info/dependency_links.txt +1 -0
  59. udevmon-0.8.1/src/udevmon.egg-info/entry_points.txt +2 -0
  60. udevmon-0.8.1/src/udevmon.egg-info/requires.txt +9 -0
  61. udevmon-0.8.1/src/udevmon.egg-info/top_level.txt +1 -0
  62. udevmon-0.8.1/tests/__init__.py +38 -0
  63. udevmon-0.8.1/tests/test_all_props.py +86 -0
  64. udevmon-0.8.1/tests/test_cli.py +58 -0
  65. udevmon-0.8.1/tests/test_completion.py +444 -0
  66. udevmon-0.8.1/tests/test_config.py +154 -0
  67. udevmon-0.8.1/tests/test_configedit_layout.py +192 -0
  68. udevmon-0.8.1/tests/test_configedit_tomlkit.py +241 -0
  69. udevmon-0.8.1/tests/test_contrib_rclone_config.py +417 -0
  70. udevmon-0.8.1/tests/test_docs.py +37 -0
  71. udevmon-0.8.1/tests/test_documentation.py +159 -0
  72. udevmon-0.8.1/tests/test_dropins.py +316 -0
  73. udevmon-0.8.1/tests/test_edit_more.py +266 -0
  74. udevmon-0.8.1/tests/test_engine_actions.py +139 -0
  75. udevmon-0.8.1/tests/test_exclude.py +269 -0
  76. udevmon-0.8.1/tests/test_explain.py +213 -0
  77. udevmon-0.8.1/tests/test_from_mountpoint.py +131 -0
  78. udevmon-0.8.1/tests/test_instance.py +369 -0
  79. udevmon-0.8.1/tests/test_isolation.py +25 -0
  80. udevmon-0.8.1/tests/test_license.py +55 -0
  81. udevmon-0.8.1/tests/test_lock.py +340 -0
  82. udevmon-0.8.1/tests/test_manpages.py +98 -0
  83. udevmon-0.8.1/tests/test_matching.py +46 -0
  84. udevmon-0.8.1/tests/test_no_serial.py +197 -0
  85. udevmon-0.8.1/tests/test_notify.py +123 -0
  86. udevmon-0.8.1/tests/test_order.py +159 -0
  87. udevmon-0.8.1/tests/test_pause.py +254 -0
  88. udevmon-0.8.1/tests/test_present.py +67 -0
  89. udevmon-0.8.1/tests/test_raw_placeholders.py +76 -0
  90. udevmon-0.8.1/tests/test_reload.py +221 -0
  91. udevmon-0.8.1/tests/test_removed.py +137 -0
  92. udevmon-0.8.1/tests/test_rename.py +413 -0
  93. udevmon-0.8.1/tests/test_retarget.py +221 -0
  94. udevmon-0.8.1/tests/test_retries.py +290 -0
  95. udevmon-0.8.1/tests/test_runner.py +275 -0
  96. udevmon-0.8.1/tests/test_scan_wizard.py +337 -0
  97. udevmon-0.8.1/tests/test_scm.py +161 -0
  98. udevmon-0.8.1/tests/test_script_dir.py +189 -0
  99. udevmon-0.8.1/tests/test_service.py +126 -0
  100. udevmon-0.8.1/tests/test_session.py +191 -0
  101. udevmon-0.8.1/tests/test_sources.py +105 -0
  102. udevmon-0.8.1/tests/test_status.py +412 -0
  103. udevmon-0.8.1/tests/test_tag_all.py +155 -0
  104. udevmon-0.8.1/tests/test_tags.py +463 -0
  105. udevmon-0.8.1/tests/test_trigger.py +256 -0
  106. udevmon-0.8.1/tests/test_vars.py +337 -0
  107. udevmon-0.8.1/tests/test_wildcards.py +144 -0
  108. udevmon-0.8.1/tools/docs.py +52 -0
  109. udevmon-0.8.1/tools/usb-test.py +452 -0
@@ -0,0 +1,430 @@
1
+ # Changelog
2
+
3
+ All notable changes to udevmon are recorded here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/), and the version numbers follow
5
+ [Semantic Versioning](https://semver.org/) (while below 1.0, a new minor
6
+ version may change behaviour).
7
+
8
+ How to keep it: add a line under **Unreleased** in the same commit as any
9
+ change a user would notice (new or changed commands, options, config keys,
10
+ dependencies, behaviour, fixes). When a release is tagged, rename that heading
11
+ to the version and date, and start a new empty **Unreleased** above it.
12
+
13
+ ## [Unreleased]
14
+
15
+ ## [0.8.1] - 2026-11-11
16
+
17
+ The first public release, on GitHub and PyPI. udevmon behaves as in 0.8.0:
18
+ this release adds the license, the packaging and the HTML manual.
19
+
20
+ ### Added
21
+
22
+ - **An HTML reference manual** (Sphinx, optional `docs` extra):
23
+ `python tools/docs.py build` or `serve`. It has the integration contracts,
24
+ which interfaces are provisional and which internal, the command reference
25
+ generated from the parser, and the module reference from the docstrings.
26
+ The core modules gained docstrings for it. The sdist now includes the docs,
27
+ man pages, tests and tools.
28
+ - **Licensed under GPL-3.0-or-later**: `LICENSE`, an SPDX header in every
29
+ Python file (a test checks them), and the license in the package metadata.
30
+ The same for `contrib/rclone-config`.
31
+ - **Ready for PyPI, GitHub and Read the Docs**: project metadata (readme,
32
+ authors, URLs, classifiers), install instructions with `pipx`, CI and a
33
+ release workflow that publishes a `vX.Y.Z` tag to PyPI, and a Read the
34
+ Docs configuration. The README says which interfaces other programs can
35
+ use (the CLI, the config format, what actions receive, `present --json`)
36
+ and that the Python modules are internal.
37
+
38
+ ### Fixed
39
+
40
+ - An editable install (`pip install -e .`) failed while writing
41
+ `_version.py`; it now skips that file and asks git, like a source checkout.
42
+ - `tests/test_manpages.py` failed to import on Python 3.10 (`tomllib`).
43
+
44
+ ### Changed
45
+
46
+ - The examples and tests use neutral drive labels, serials and host names.
47
+
48
+ ## [0.8.0] - 2026-11-10
49
+
50
+ ### Changed
51
+
52
+ - **`scan` now confirms before saving, and lets you go back.** Before anything is
53
+ written it shows what would be saved (name, match or rule with its strength, and
54
+ tags for a device) and asks you to confirm; `r`, `m` and `t` redo the name, the
55
+ match/rule or the tags without losing the rest, and `c` (also `n`, `q`) cancels,
56
+ leaving the thing in the list to pick again. A weak or medium match now needs an
57
+ explicit `y` at that screen instead of a separate "Save anyway?" prompt. A name
58
+ already in use is reported there, so you can rename and try again instead of
59
+ losing what you typed.
60
+ - **`scan` can now declare a category instead of a device.** For each thing you
61
+ pick to name, it first asks whether to register it as a **device** (one
62
+ particular, trusted thing) or a **category** (a declared tag with a rule,
63
+ matching every thing like it); a thing with no serial number, such as a YubiKey,
64
+ defaults to category, since no device match can single out one such unit anyway.
65
+ A category has no tags of its own and no strength gate, and the "add an action"
66
+ hint printed afterwards includes `--allow-unnamed`, since a category is unnamed
67
+ by default until it also matches a device you named. `iN` (ignore) is unchanged:
68
+ it is device-only and skips the kind choice.
69
+
70
+ ## [0.7.0] - 2026-11-05
71
+
72
+ ### Added
73
+
74
+ - **Drop-in files: `config.d/*.toml`.** Read after `config.toml`, in file-name
75
+ order, with the same tables except `[daemon]`. Their devices, tags and
76
+ actions join the main file's, so a program can install its own in a file it
77
+ owns and replaces or deletes whole. With the same `order` (below), actions
78
+ run in the order the files are read, main file first. A name
79
+ defined in two files is an error naming both. `check` lists the files read;
80
+ `list`, `show` and `explain` mark entries from a drop-in. The `add` commands
81
+ take `--file NAME` to write to `config.d/NAME.toml`; the other editing
82
+ commands change an entry where it is defined, and a rename fixes references
83
+ in every file. With only `config.toml` nothing changes.
84
+ - **`udevmon trigger DEVICE [EVENT]`** asks the running daemon to run a present
85
+ device's actions now, as for a real `startup` (default), `shutdown`,
86
+ `connected`, `mounted` or `unmounted`: for a drive named while plugged in, or
87
+ to test an action without unplugging. It follows pause, `disabled` and
88
+ `ignored`, and is logged like a real event. `--wait` waits for the actions
89
+ and shows their exit status. The daemon now listens on
90
+ `$XDG_RUNTIME_DIR/udevmon.sock` for this (only you can connect).
91
+ - **`udevmon devices add NAME --from-mountpoint PATH`** names the plugged-in
92
+ drive mounted at `PATH` without typing its identity: the match is its
93
+ filesystem's `fs_uuid` and its drive's `drive_serial`. It refuses (and writes
94
+ nothing) when nothing is mounted there, the drive has no usable serial number,
95
+ or a device already matches that filesystem.
96
+ - **`notify_on_failure = true` on an action** (`actions add|set
97
+ --notify-on-failure`): a desktop notification when it exits non-zero, times
98
+ out or cannot start, after its last attempt with `retries`. Sent on the
99
+ desktop's D-Bus; without a session bus nothing is shown. Off by default.
100
+ - **`order` on an action** (a whole number, default 50): the actions of one
101
+ event run by `order`, lower first, then in config order. This nests one
102
+ tool's actions around another's across files, which one file order cannot
103
+ (credentials up first on plugging, down last on unplugging).
104
+ `actions add|set --order N` sets it; `actions list` shows the actions in the
105
+ order they run, and `explain` shows a non-default order. Without `order`
106
+ nothing changes.
107
+ - **`udevmon present --json`**: what is plugged in, for scripts, with every
108
+ property, the devices and tags each thing is, and the suggested match.
109
+
110
+ ### Changed
111
+
112
+ - **A missing optional `script_dir` folder is a note, not a warning.** When
113
+ another folder of the list exists, one that does not (an example's
114
+ `~/.local/udev/scripts` on a machine without it) is a note in `udevmon check`
115
+ and an ordinary line in the log at start and reload. A path that exists but is
116
+ not a folder, or a list where no folder exists, is still a warning.
117
+ - **`contrib/rclone-config`: the example uses `order`** (10 for `up`, 90 for
118
+ `down`) and goes in `config.d/rclone-config.toml`, so it wraps the actions
119
+ that use the credentials whatever file they are in.
120
+
121
+ ## [0.6.0] - 2026-11-03
122
+
123
+ ### Added
124
+
125
+ - **`contrib/rclone-config`: rclone credentials carried on a drive.** A
126
+ separate helper package (not part of udevmon) whose `rclone-config`
127
+ command udevmon runs as an action: while a drive tagged `rclone` is present, its
128
+ `config/rclone/rclone.conf` is published to
129
+ `$XDG_RUNTIME_DIR/rclone/rclone.conf` for any rclone user; it is switched to
130
+ another tagged drive's config, or deleted, as soon as that drive is gone or
131
+ udevmon stops. See its README for installing and the two actions.
132
+
133
+ ### Fixed
134
+
135
+ - **Graphical programs started by actions work under the systemd service.**
136
+ The service got its environment once, when it started; if that was before the
137
+ desktop gave systemd its `DISPLAY` (or on a desktop that never does), every
138
+ action ran without a display. Now the session's `DISPLAY`, `WAYLAND_DISPLAY`,
139
+ `XAUTHORITY`, `DBUS_SESSION_BUS_ADDRESS` and `XDG_*` desktop variables are
140
+ looked up for each action: from systemd's environment for your user as it is
141
+ now, or else from one of your own running programs that has a display
142
+ (preferring those of your desktop login over, say, an `ssh -X` shell). At
143
+ the very beginning of a login, before the desktop has a display, an action
144
+ waits for it, up to 30 s. The log says which display is used, and warns when
145
+ there is none. Run from a terminal with a display, nothing changes.
146
+ - **Debounce no longer drops events of a different thing, or a real second
147
+ event.** It was keyed on the device name, so two things matching one device
148
+ (two YubiKeys without a serial in the match) within a second counted as one:
149
+ at startup only one got its `startup` action. And a `mounted`, `unmounted`,
150
+ `mounted` within a second lost the second `mounted`. Now a repeat is the same
151
+ kind of event for the same thing with nothing else in between, and a dropped
152
+ repeat is logged at the normal level.
153
+ - **Works with tomlkit 0.15.** Removing the last action left `actions = []` at
154
+ the top of the file (tomlkit 0.15 writes an emptied `[[actions]]` list that
155
+ way), which also broke the file's first line. The key is now removed with its
156
+ last entry.
157
+
158
+ ### Dependencies
159
+
160
+ - tomlkit **0.11.7** or newer (was 0.11.4), **except 0.15.0**: 0.11.4 to 0.11.6
161
+ fail two of the tests on variables, and 0.15.0 writes invalid TOML when a
162
+ variable in an inline table is changed (the edit is refused and rolled back;
163
+ fixed in tomlkit 0.15.1). Tested with 0.11.7, 0.12, 0.13, 0.14, 0.15.1 and
164
+ 0.15.2.
165
+
166
+ ### Documentation
167
+
168
+ - README: installing into a virtual environment without the system packages
169
+ (PyGObject's build needs, and `PyGObject<3.51` on Ubuntu 22.04), and the
170
+ `UDISKS_AUTO=0` udev rule as the clean way to keep a partition from being
171
+ automounted, rather than an action that unmounts it.
172
+
173
+ ## [0.5.0] - 2026-09-21
174
+
175
+ ### Added
176
+
177
+ - **`instance` and a state folder, to match an unplug with its plug.** Every plug of a
178
+ USB device and every mount of a filesystem has one `instance` value (`{instance}`,
179
+ `$UDEVMON_INSTANCE`), identical in all its events and new for the next plug or mount,
180
+ and a private folder (`{state_dir}`, `$UDEVMON_STATE_DIR`,
181
+ `$XDG_RUNTIME_DIR/udevmon/state/<instance>`) that exists from the first action to the
182
+ end of its last event, so a connect script can leave what it found out (the serial that
183
+ `ykman` reported) for the disconnect script. Wiped when the daemon starts and stops.
184
+ `instance` shows in `--all-props` and cannot be used in tag rules. See "Matching an
185
+ unplug with its plug" in the README.
186
+ - **`retries` and `retry_delay`** on an action (`actions add --retries N --retry-delay S`,
187
+ `actions set` the same): a command that exits with a non-zero status is run again after
188
+ the delay, up to `retries` more times, for things like `udisksctl unmount` right after a
189
+ desktop mounted the drive. Never after a timeout, when the program cannot start, or when
190
+ the action was skipped; each attempt has its own timeout; retrying stops when automation
191
+ is paused or the daemon is stopping; a `lock` is held across the attempts. Not with
192
+ `detach`. `actions list` and `explain` show it.
193
+ - **`lock = "name"`** on an action (`actions add --lock NAME`, `actions set --lock NAME`
194
+ / `--no-lock`): actions that share a lock name never run at the same time, and run in
195
+ the order their events arrived, so the actions of a plug are never overtaken by those of
196
+ the unplug that follows. Not affected: actions without a lock, other lock names, and the
197
+ file order within one event. Released when the command exits (or, for `detach`, has
198
+ started), also after a crash. `actions list` and `explain` show it, and the log says
199
+ when an action had to wait. See "Running actions one at a time" in the README.
200
+ - **`detach = true`** on an action (`actions add --detach`, `actions set --detach` /
201
+ `--no-detach`): the command is started in its own session and not waited for, for a
202
+ command that is itself a long-running program such as `keepassxc`. No timeout (setting
203
+ one is an error), the actions after it and `shutdown` are not held up, its output is
204
+ logged and its exit status is logged when it ends. `actions list` and `explain` show
205
+ it. See "Programs that keep running" in the README.
206
+ - **`[daemon] script_dir`**, a folder or a list of folders for your own scripts. A
207
+ command given by bare name is looked for in them, in order, and then only in the
208
+ standard system paths (`/usr/local/sbin`, `/usr/local/bin`, `/usr/sbin`, `/usr/bin`,
209
+ `/sbin`, `/bin`), never in the daemon's own `PATH`, so it is found the same way
210
+ under systemd and in a terminal. Resolved at every event (a script added later
211
+ needs no reload), shown in full by `explain`, and checked by `check`. Without it
212
+ nothing changes.
213
+ - **`scan --all-props` and `present --all-props`** print every property of each thing,
214
+ one per line, including the raw `udev_*` ones that the normal output hides, and mark
215
+ those that change on every plug (they cannot be used in tag rules). It shows the
216
+ names to use in a rule or as `{udev_NAME}` in a command.
217
+
218
+ ### Fixed
219
+
220
+ - **A script that leaves a program running in the background** (`keepassxc &`, then
221
+ exit) made the action look stuck until its `timeout`, log "timed out" and be
222
+ reported as failed: the program kept the output pipe open and udevmon waited for the
223
+ pipe, not for the script. udevmon now waits for the command to exit; the program
224
+ keeps running, and whatever it writes is still logged. Output is also logged line by
225
+ line as it arrives instead of at the end.
226
+ - **`{udev_NAME}` placeholders** (for example `{udev_id_model}`) in a command made the
227
+ configuration fail to load, although the documentation said raw udev properties can
228
+ be used. They are accepted for every event; a thing that lacks the property makes the
229
+ action be skipped, with an error in the log, as for any unavailable placeholder.
230
+
231
+ ## [0.4.1] - 2026-09-21
232
+
233
+ ### Changed
234
+
235
+ - **Devices without a serial number are no longer given a suggested match.** `scan`
236
+ used to pre-select `vendor_id`, `product_id` and `port_path`, which ties the rule to
237
+ a USB port (it silently stops matching in another port or behind a hub, and matches
238
+ any unit of the model in that port). It now says nothing identifies the unit and asks
239
+ you to choose between "this model, in this port only" and "this model, in any port",
240
+ or type a list.
241
+ - `present` and the scan list mark such a device `[medium: no serial, matches the
242
+ port]`, and `check` notes any device or tag rule that uses `port_path`.
243
+
244
+ ## [0.4.0] - 2026-09-21
245
+
246
+ ### Added
247
+
248
+ - **Wildcards in tag rules.** `*` (any run of characters) and `?` (one) in a rule
249
+ value, for example `fs_label = "BACKUP*"`. The whole value must match, ignoring
250
+ capitals; nothing else is special. A rule with a pattern is rated weak. In a
251
+ device's own `match` they stay ordinary characters, and `check` warns.
252
+ - **"All of" for tags.** An action with several tags can require every one of them:
253
+ `tag_mode = "all"` (default `"any"`), `actions add --all-tags`, and
254
+ `actions set --all-tags` / `--any-tags`. `actions list` and `explain` show it.
255
+ - **Opting a device out of a rule tag.** `exclude_tags` on a device, and
256
+ `devices exclude` / `devices include` (also `devices add --exclude-tag`). The
257
+ device stays named and keeps its own actions; explicit tags are never affected.
258
+ `explain`, `scan` and `present` show `excluded from: TAG`, and renaming a tag
259
+ updates `exclude_tags`.
260
+ - **Per-device variables.** `vars = { dest = "..." }` on a device, used as
261
+ `{var_dest}` in commands and as `UDEVMON_VAR_DEST` in the environment, so one
262
+ action can serve several devices differently. `devices add --var NAME=VALUE`,
263
+ `devices set --var NAME=VALUE --unset-var NAME`, listed by `devices show`. A
264
+ variable no device defines is an error; `check` warns about devices that lack
265
+ one, and about `allow_unnamed` actions that use one (unnamed things have none).
266
+
267
+ - **`actions set NAME -- COMMAND...`** replaces an action's command, as `actions add`
268
+ takes it. A placeholder the action's events don't provide is refused, and a
269
+ comment after the old `run` line is kept.
270
+ - **`tags set NAME`** changes a declared tag: `--match` replaces its rule,
271
+ `--no-rule` removes it, `--description` changes the text. Comments and layout
272
+ are kept, and a rule with a property that changes from plug to plug is refused.
273
+
274
+ ## [0.3.0] - 2026-09-21
275
+
276
+ ### Added
277
+
278
+ - **Device status: `disabled` and `ignored`.** A `status` key on a device (absent
279
+ or empty means active). A disabled device's own actions don't run, but actions
280
+ reached through its tags still do; an ignored device runs nothing at all, and
281
+ ignore wins if a thing matches several devices. Both stay "named", so they never
282
+ fall into the unnamed category. New commands: `devices disable`, `devices ignore`
283
+ and `devices enable`, which take several names and change nothing unless all exist.
284
+ - **Renaming.** `devices rename`, `tags rename` and `actions rename` change a name
285
+ everywhere it is used (headers and sub-tables, every action's `device` or `tag`,
286
+ the `tags` of devices), keep comments, layout and order, and refuse (writing
287
+ nothing) unless the result is exactly the old config with that one name changed.
288
+ They report what was updated, made-up action names that changed, and the comments,
289
+ descriptions and commands that still mention the old name (left alone).
290
+ - **`devices set NAME`** replaces a device's match (`--match`) and/or its
291
+ `--description`, keeping a comment after the old value.
292
+ - **Global pause.** `udevmon pause` stops every action without touching the config,
293
+ `udevmon pause --for 30m` (`90s`, `2h`, `1d`, `1h30m`) for a while, and
294
+ `udevmon resume` lets them run again. Nothing runs while paused, not even
295
+ `startup` and `shutdown` actions, and events during a pause are not replayed. The
296
+ state is a file in the runtime directory, so it survives a daemon restart but
297
+ not a logout or reboot. `explain` and the editing commands say when automation
298
+ is paused, and the daemon's start-up line shows `[PAUSED]`.
299
+ - **Surprise removal.** A mounted filesystem that vanishes without being unmounted
300
+ (a pulled stick) now fires a new **`removed`** event after `unmounted`, so an
301
+ action can warn about it (`on = ["removed"]`). The new `surprise` value (`{surprise}`,
302
+ `$UDEVMON_SURPRISE`) is `1` for a pulled stick and `0` after a clean unmount, on
303
+ `unmounted` and `removed`. Existing `on = ["unmounted"]` actions still fire.
304
+ - **`enabled = false` on actions**, with `actions disable` and `actions enable`.
305
+ - **`actions set NAME`** changes `allow_unnamed` (`--allow-unnamed`, `--named-only`,
306
+ `--default-unnamed` to clear it), the events (`--on`) or the `--timeout` of an
307
+ action without removing and re-adding it.
308
+ - **Retargeting actions:** `actions set NAME` also takes `--add-device`,
309
+ `--remove-device`, `--add-tag` and `--remove-tag` (repeatable) to change which
310
+ devices or tags an action applies to. Comments and layout are kept; removing a
311
+ target the action lacks, or its last target, is refused.
312
+ - **`scan` can ignore things:** answer `i3` (or `i1,3`) instead of a number to save
313
+ them as ignored devices, without asking for tags.
314
+ - `devices list`, `devices show`, `actions list`, `present`, `scan` and `explain`
315
+ mark what is switched off; `explain` says why an action was skipped (disabled
316
+ action, disabled device) and `check` notes it.
317
+ - **Manual pages:** `udevmon(1)` for every command and option, and
318
+ `udevmon.toml(5)` for the configuration file, the events, the properties and
319
+ how commands are run. They are installed by `pip` (under `share/man`) and can
320
+ be read from a checkout with `man -l man/udevmon.1`. A test fails when a
321
+ command, option, config key, event or property is missing from them.
322
+
323
+ ### Fixed
324
+
325
+ - **Names containing a dot** (`usb.1`) were accepted but written as a nested header, so
326
+ the file was rejected. They are now written quoted (`[devices."usb.1"]`).
327
+ - **Clearer reasons** when an edit cannot proceed: the message names the entry it could
328
+ not find, and says how to give an unnamed action a name so it can be changed.
329
+ - **Documentation:** the README described property matching as exact; values are
330
+ compared without regard to capitals (`fs_label = "backup"` matches `BACKUP`).
331
+
332
+ ### Internal
333
+
334
+ - **`tools/usb-test.py`**, a guided real-hardware test: it asks you to plug a
335
+ stick in, pull it out and plug it in again, and checks events, status, pause,
336
+ the unnamed gate and surprise removal (16 checks) on a throwaway config. The
337
+ `removed` event and `surprise` value were confirmed on a real stick with it.
338
+ - The test suite no longer leaves temporary folders behind: it works in one
339
+ per-run folder that is removed on exit, and a test guards this.
340
+
341
+ ## [0.2.0] - 2026-09-20
342
+
343
+ ### Added
344
+
345
+ - **Bash tab completion.** `udevmon completion install` (or `completion bash`
346
+ to print the script) completes commands, sub-commands and options, and reads
347
+ your config for live names: devices, tags, actions, event lists and
348
+ `--match` keys. `udevmon completion uninstall` removes it.
349
+ - **`udevmon explain`** shows what would run for everything plugged in now, or
350
+ for a simulated thing (`--device NAME`, `--prop KEY=VALUE`). It applies the
351
+ same rules as the daemon (named or unnamed, tags, `allow_unnamed`), shows
352
+ each command with its values filled in, and says why an action is skipped.
353
+ - **`udevmon service uninstall`** stops and disables the service, removes the
354
+ unit file and reloads systemd. Files that are not udevmon's own are left
355
+ alone.
356
+ - **Automatic reload after edits.** The commands that add, remove, tag or
357
+ untag devices, tags and actions, and `scan` when it saves a device, reload a
358
+ running daemon that uses the same config file. `--no-reload` skips it.
359
+ - **Warning for programs that cannot run.** `check` warns when an action's
360
+ program is not found in `PATH` or is not executable. The daemon logs the same
361
+ warning at start and on every reload, using its own `PATH`.
362
+
363
+ ### Changed
364
+
365
+ - **Editing existing entries uses `tomlkit`.** `devices remove|tag|untag`,
366
+ `tags remove` and `actions remove` keep comments and layout however the file
367
+ was written by hand, and keep CRLF line endings (the old editor turned them
368
+ into LF). Adding new entries still appends text as before.
369
+ - **New dependency: `tomlkit` 0.11.4 or newer**, installed automatically by
370
+ `pip`. Only the commands above need it; adding entries, `check`, `explain`
371
+ and the daemon do not, and a missing `tomlkit` produces a message saying how
372
+ to install it. The system package is recent enough on Debian 13 and Ubuntu
373
+ 24.04; Ubuntu 22.04's is too old, so `pip` fetches a current one there.
374
+ - **The daemon's lock file** (`$XDG_RUNTIME_DIR/udevmon.lock`) now holds the PID
375
+ on the first line and the resolved config path on the second.
376
+
377
+ ### Fixed
378
+
379
+ - **Commented table headers.** A comment after a header (`[devices.x] # note`,
380
+ `[[actions]] # note`) or after an action's `name = "x"` made `remove` and
381
+ `tag` fail with "could not locate block" or "has no explicit name".
382
+ - **Hand-formatted files.** Editing failed on a device written as a sub-table
383
+ (`[devices.x]` plus `[devices.x.match]`), on a block whose multi-line string
384
+ has a line starting with `[`, and on a multi-line `tags` array.
385
+ - **Documentation.** A doubled code fence in the README and SPECIFICATIONS
386
+ rendered the command list as an empty box followed by plain text.
387
+
388
+ ### Internal
389
+
390
+ - The test suite grew from 94 to 194 tests. It gets its own runtime directory,
391
+ so it can never signal a real daemon. New tests keep the docs renderable
392
+ (balanced code fences, code lines that fit an 80-column terminal) and fail
393
+ when the CLI gains a command, positional argument or value option that shell
394
+ completion does not cover.
395
+
396
+ ### Upgrading
397
+
398
+ - If you run the service, restart it after upgrading
399
+ (`systemctl --user restart udevmon`). An old `udevmon reload` cannot read the
400
+ new two-line lock file and would report that no daemon is running. A new CLI
401
+ works with an old daemon.
402
+ - Editing commands now reload the daemon by themselves; add `--no-reload` to
403
+ get the previous behaviour.
404
+
405
+ ## [0.1.0] - 2026-09-20
406
+
407
+ First version.
408
+
409
+ ### Added
410
+
411
+ - **Automation without root.** A daemon watches USB devices (udev) and
412
+ filesystem mounts (udisks2 over D-Bus) and runs your commands on `connected`,
413
+ `disconnected`, `mounted`, `unmounted`, and, for things already present,
414
+ `startup` and `shutdown`.
415
+ - **Named devices** recognised by serial number, filesystem UUID or partition
416
+ UUID, with a strength rating (strong, medium, weak) and warnings for weak
417
+ matches. **Tags**, set on devices or selected by rules on properties, and
418
+ actions that target one device, several devices or a tag.
419
+ - **Safe autorun.** Things you have not named run only actions that set
420
+ `allow_unnamed = true`; `check` explains the choice.
421
+ - **Configuration** in a TOML file (`~/.config/udevmon/config.toml`), with
422
+ placeholders (`{mountpoint}`, `{device}`, `{tag}`, ...) and `UDEVMON_*`
423
+ environment variables. Commands are never run through a shell, each action has
424
+ a timeout, and a config that is not yours or is writable by others is refused.
425
+ - **Commands:** `scan` (watch events and name devices, also for things already
426
+ present with `--present`), `present`, `devices`, `tags`, `actions`, `check`,
427
+ `run` (with `--dry-run`), `reload` and `service install`.
428
+ - **Version taken from git**, in the format setuptools-scm uses.
429
+ - **Python 3.10 or newer** (with `tomli` as the reader on 3.10), a README and a
430
+ detailed SPECIFICATIONS, and 94 tests.
@@ -0,0 +1,32 @@
1
+ # udevmon: notes for Claude sessions
2
+
3
+ Read `DEVELOPMENT.md` first: tests, the checks that keep CLI, completion, man
4
+ pages and docs in step, live testing, branches and releasing. `SPECIFICATIONS.md`
5
+ is the design (§12 lists every open idea); `README.md` is the user guide;
6
+ `CHANGELOG.md` the history.
7
+
8
+ ## Quick reference
9
+
10
+ ```
11
+ PYTHONPATH=src python3 -m unittest discover -s tests -t .
12
+ udevmon check # the user's real config
13
+ VENV/bin/python tools/docs.py build # HTML manual; needs the docs extra
14
+ git log --oneline -5; git branch -a
15
+ ```
16
+
17
+ ## Rules
18
+
19
+ - **Branches:** work on a branch off `main`; commits there need no asking.
20
+ Merging into `main`, tagging and pushing only when the user says so.
21
+ - **No backwards compatibility** while udevmon is preliminary: change formats
22
+ and names in place, no aliases or migration code; fix the user's own config
23
+ (`~/.config/udevmon/config.toml`, `config.d/`) by hand if it is affected.
24
+ - **A new option is not done** until `completion.py`, the man pages, README,
25
+ SPECIFICATIONS and the changelog have it; the tests enforce most of this.
26
+ - **Live tests** use a private daemon (own config and `XDG_RUNTIME_DIR`), never
27
+ the user's running service, and leave nothing behind (DEVELOPMENT.md, "Live
28
+ testing"). Ask before restarting the real service.
29
+ - **Untracked notes and run books** go in files named `*.local.md` (ignored
30
+ by Git); delete them once their work is done.
31
+ - **The owner's environment** (remotes, other machines, projects that use
32
+ udevmon) is in `CLAUDE.local.md`, which Git ignores, when it exists.
@@ -0,0 +1,156 @@
1
+ # Working on udevmon
2
+
3
+ How the project is worked on: tests, the checks that keep the docs in step with
4
+ the code, live testing, and releasing. `SPECIFICATIONS.md` describes the design;
5
+ `README.md` is for users.
6
+
7
+ ## Tests
8
+
9
+ ```
10
+ PYTHONPATH=src python3 -m unittest discover -s tests -t .
11
+ ```
12
+
13
+ Running from the source tree needs the dependencies installed (`pyudev`,
14
+ `PyGObject`, `tomlkit`). Run one module with `python3 -m unittest tests.test_x`,
15
+ not `python3 tests/test_x.py`: importing the `tests` package is what isolates
16
+ the suite (`tests/__init__.py`):
17
+
18
+ - every temporary file goes in one per-run folder, deleted at the end, so
19
+ nothing is left in `/tmp`;
20
+ - the tests get their own `XDG_RUNTIME_DIR`, so they never reach a real daemon,
21
+ its pause flag or its control socket;
22
+ - a fake `DISPLAY`, and no `DBUS_SESSION_BUS_ADDRESS`, so nothing reaches the
23
+ real desktop or sends a notification.
24
+
25
+ A new test that needs a daemon, a session bus or the desktop must fake it the
26
+ same way; `tests/test_notify.py` and `tests/test_trigger.py` show how.
27
+
28
+ After writing a test for a rule, break the rule in the code on purpose and check
29
+ that the test fails (a quick mutation check). It catches a test that passes for
30
+ the wrong reason.
31
+
32
+ ## Checks that keep things in step
33
+
34
+ These tests fail when a change is only half done. Fix them by finishing the
35
+ change, not by loosening the test.
36
+
37
+ | Test | Fails when |
38
+ |---|---|
39
+ | `test_completion` | a command, positional argument or value option of the CLI is missing from the completion tables in `completion.py` |
40
+ | `test_manpages` | a command, option, config key, event or property is not described in `man/`, the example config there is invalid, or `groff` warns |
41
+ | `test_documentation` | the HTML manual has a warning (broken reference, bad docstring markup), or building it reads the config, starts a source, opens a socket or runs a command other than `git`; skipped without Sphinx |
42
+ | `test_docs` | a code block in `README.md`, `SPECIFICATIONS.md`, `CHANGELOG.md` or this file is unbalanced, empty, or has a line wider than 76 characters (it must fit an 80-column `glow`) |
43
+ | `test_license` | a Python file lacks the two-line SPDX header (after any `#!` line), or `LICENSE` or the license metadata of udevmon or `contrib/rclone-config` is missing |
44
+
45
+ So a new option normally touches: the parser in `cli.py`, `completion.py`, both
46
+ man pages where relevant, README, SPECIFICATIONS, and the changelog.
47
+
48
+ ## Docs
49
+
50
+ - `CHANGELOG.md`: add a line under **Unreleased** for any change a user would
51
+ notice, in the section it belongs to (Added, Changed, Fixed, ...).
52
+ - Man pages: when a page's content changes, set the date in its `.TH` line to
53
+ the day of the change (`man/udevmon.1`, `man/udevmon.toml.5`).
54
+ - Prose is plain and short; examples are real commands that work.
55
+ - Every Python file starts with the license header (after a `#!` line):
56
+
57
+ ```
58
+ # SPDX-License-Identifier: GPL-3.0-or-later
59
+ # Copyright (C) 2026 Juan Domingo Martín
60
+ ```
61
+ - No personal data in tracked files: examples use neutral drive labels
62
+ (`KEYDRIVE`), serials (`1111222233334444`), UUIDs (`AB12-CD34`) and host
63
+ names (`alpha`, `beta`).
64
+ - No backwards compatibility while udevmon is preliminary: a format change is
65
+ made in place, and existing configs are fixed by hand, not by migration code.
66
+
67
+ ### The HTML manual
68
+
69
+ `docs/` is a Sphinx manual for integrators and maintainers: integration
70
+ contracts, what is provisional and what internal, the command reference
71
+ (generated from the parser) and the module reference (from docstrings).
72
+ Sphinx is optional (the `docs` extra); install it in a private virtual
73
+ environment, never in `~/.venvs/tools`:
74
+
75
+ ```
76
+ python3 -m venv --system-site-packages /path/to/venv
77
+ /path/to/venv/bin/python -m pip install -e '.[docs]'
78
+ /path/to/venv/bin/python tools/docs.py build
79
+ /path/to/venv/bin/python tools/docs.py serve --port 8000
80
+ ```
81
+
82
+ The build is fresh, nitpicky and fails on any warning; the output, in
83
+ `docs/_build/html` (ignored by git) or `--output PATH`, is never committed.
84
+ The preview listens on 127.0.0.1 only, does not rebuild after edits, and
85
+ stops with Ctrl-C. Run the test suite with that venv's Python too, so that
86
+ `test_documentation` builds the manual instead of being skipped.
87
+
88
+ Docstrings are PEP 257 and reStructuredText (double backquotes for code);
89
+ Napoleon reads Google-style `Args:`, `Returns:` and `Raises:` sections. Give
90
+ the contract a caller needs (blocking work, files written, subprocesses,
91
+ failure), not boilerplate for trivial helpers. A new module shows up in the
92
+ manual by itself, under "Other modules"; list a core one in
93
+ `docs/internals.rst`. `docs/contributing.rst` has the details.
94
+
95
+ ## Live testing
96
+
97
+ The unit tests use synthetic events. For the real thing:
98
+
99
+ - `tools/usb-test.py` walks through plugging and pulling a USB stick (see
100
+ README, "Real-hardware test").
101
+ - A private daemon, so the real service is not disturbed: give it its own
102
+ config and runtime folder, and stop it at the end.
103
+
104
+ ```
105
+ rt=$(mktemp -d /tmp/udm-XXXX) # short: a socket path is limited
106
+ trap 'rm -rf "$rt"' EXIT
107
+ XDG_RUNTIME_DIR=$rt udevmon --config test.toml run --dry-run
108
+ ```
109
+
110
+ Commands that talk to it (`trigger`, `pause`, `reload`) need the same
111
+ `XDG_RUNTIME_DIR`.
112
+
113
+ While testing: leave nothing behind in `/tmp` (a `trap` as above), keep the
114
+ desktop from opening a file-manager window for each mounted stick
115
+ (`usb-test.py` turns that setting off and back on), and avoid anything that asks
116
+ for a password, such as `udisksctl` without `--no-user-interaction`.
117
+
118
+ ## Branches
119
+
120
+ Work goes on a branch off `main` (`feature/...`, `fix/...`, `docs/...`), one
121
+ commit per step that was tested. It is merged into `main` as one clean commit,
122
+ squashed if the branch has a long trial-and-error history.
123
+
124
+ ## Releasing
125
+
126
+ The version comes from git tags (`src/udevmon/_scm.py`, SPECIFICATIONS §13.1);
127
+ nothing in the source holds it.
128
+
129
+ 1. Choose the number. Before 1.0: a new feature or a change in behaviour bumps
130
+ the minor version (0.7.0 to 0.8.0); fixes only bump the patch (0.4.0 to
131
+ 0.4.1).
132
+ 2. In `CHANGELOG.md`, add `## [X.Y.Z] - YYYY-MM-DD` under `## [Unreleased]`,
133
+ which stays, empty, above it.
134
+ 3. Check the man page dates (see Docs) and run the tests.
135
+ 4. Commit as `Release X.Y.Z`, then tag: `git tag -a vX.Y.Z -m "udevmon X.Y.Z"`.
136
+ 5. Push `main` and the tag. On GitHub the tag starts the release workflow
137
+ (`.github/workflows/release.yml`): it checks the tag against the version
138
+ from git, builds, and publishes to PyPI. Never re-upload a version.
139
+ 6. Reinstall where it is used, and restart the service if the daemon changed.
140
+
141
+ ## Installing a development build
142
+
143
+ udevmon is installed in a virtual environment made with
144
+ `include-system-site-packages = true`, so it uses the system's PyGObject (`gi`),
145
+ `pyudev` and `tomlkit`, with the commands linked into `~/.local/bin`. With `uv`:
146
+
147
+ ```
148
+ uv pip install --python ~/.venvs/tools/bin/python --no-deps .
149
+ udevmon completion install
150
+ ```
151
+
152
+ `--no-deps` is needed because `uv` does not count the system packages as
153
+ installed and would fetch and build PyGObject from PyPI. The man
154
+ pages are installed into the environment's `share/man`; `man` finds them only
155
+ if they are linked from `~/.local/share/man/man1` and `man5`. The links
156
+ survive reinstalls.