89 lines
7.0 KiB
Markdown
89 lines
7.0 KiB
Markdown
# Invariants: the session file, the export and the source chooser
|
|
|
|
What a session persists and in what order, when a save is refused, and when an export is collected.
|
|
|
|
Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to learn; read the ones touching the area you are editing before you edit it.
|
|
|
|
- **The session is the layout.** `LayoutState` persists what makes a reopened scope look like the
|
|
one that was closed: the window, the rows and their slot signals, the calibration, **the enabled
|
|
signals**, **the chosen trace colours** (`[[color]]`, `name` + `rgb = "#RRGGBB"` — only the
|
|
*overrides*, so a stream's new signals still take the positional palette and a saved session does
|
|
not freeze a colour that is meant to follow the signal list), **the math channels with their
|
|
graphs**, **the measurements pane**, and **`measure_stats`** — the measurements statistics
|
|
toggle; written as a bare top-level boolean (`measure_stats = true/false`), defaults to `false`
|
|
when absent. Signal identity
|
|
is the *name* throughout (a CONFIG renumbers channels), 'enabled' is read off the channels the
|
|
model built -- an enabled signal is exactly one that produced a channel -- and `apply_layout()`
|
|
installs the graphs **before** assigning the rows, since a row may hold a math channel's name and
|
|
that name is only a signal once its graph is in. Two format rules, both learned the hard way:
|
|
write an op or a measure *type* as its index through `math_op_at()`/`measure_type_at()`, never as
|
|
the raw enum value -- those accessors' orders are the dialog's, not the declaration order, so
|
|
writing one and reading the other silently renames operations. And a top-level key written after
|
|
a `[[table]]` belongs to *that table* in TOML: `enabled = [...]` emitted after `[[calibration]]`
|
|
was parsed into the last calibration record and the enable state was lost without a word.
|
|
`mkdirs()` used to create only components with a trailing slash, so `~/.config/udpscope` was never
|
|
made and the *first* save failed silently -- worth knowing when a layout change "does not stick".
|
|
- **The export is collected at click time, not in the file-dialog callback.** The live
|
|
view keeps scrolling while the chooser is up, so a late collection would write a
|
|
window the user never saw. The writer is picked by the chosen path's extension.
|
|
- **The chooser runs before `ScopeModel` exists, and `--server` is what decides.** `Config` carries
|
|
`server_specified`, set only by the `--server` branch, and `main()` asks only when it is false —
|
|
so declining costs no buffer and no socket, and `ScopeModel`, `Acquisition`, `RowSet` and
|
|
`run_ui()` need no notion of "no source yet". It is `--server` alone and not "no arguments":
|
|
`--multicast` still needs a server to fetch CONFIG from. **Every documented probe passes
|
|
`--server`**, which is what makes the feature invisible to the regression net — a probe added
|
|
without it would reach the chooser and hang. `gtk_init()` plus a nested `GMainLoop`, and then
|
|
`run_ui()`'s `GtkApplication` afterwards in the same process, is a verified combination
|
|
(`gtk_init()` is idempotent; running your own loop is the documented GTK4 replacement for the
|
|
removed `gtk_dialog_run()`).
|
|
- **A session file's name cannot say how its source was reached, so the file says it.**
|
|
`<host>_<port>.toml` carries neither the multicast group nor the interface, so a chooser row
|
|
could not reconnect to a multicast source at all. `LayoutState::multicast`/`interface_addr` are
|
|
written in the **top block, before the first `[[table]]`** — a bare key after a table header is
|
|
that table's key in TOML, which is exactly how `enabled` was parsed into the last
|
|
`[[calibration]]` record and lost without a word. A file from an older build has neither key and
|
|
loads as unicast on the default route. The chooser's list filters to sessions that **name at
|
|
least one signal**, because a probe run leaves a file per throwaway port and all of them hold
|
|
`signals = ["", "", "", ""]`; `known_sources()` splits the filename at the **last** `_`, since
|
|
`sanitize()` turns anything unsafe in a host into one.
|
|
- **`save_layout_now()` refuses to write while `signals()` is empty.** A run against a source
|
|
whose streamer was down receives no CONFIG, so every slot name stays empty and the save would
|
|
overwrite a configured session with `signals = ["", ...]`. The chooser then filters that file
|
|
out — a source whose streamer was merely down during one run would silently vanish from the
|
|
only list it can be picked from, for ever. Keeping the old file loses nothing: a run that
|
|
configured nothing had nothing worth persisting.
|
|
|
|
## A configuration is a named session, and a source may hold several
|
|
|
|
One source, several arrangements: the same streamer serves different experiments, and retyping a
|
|
layout to look at the second one is how a session gets lost. Storage is therefore a **directory
|
|
per source** — `~/.config/udpscope/<host>_<port>/` — holding one `<name>.toml` per configuration
|
|
plus an `active` file naming the one the source is on. `configuration_path()` is the only
|
|
expression of the scheme, and the name is `sanitize()`d exactly as the host is, so a name typed
|
|
with a slash cannot escape the directory.
|
|
|
|
A configuration carries the whole session *and* the settings that can only be decided before the
|
|
first socket: `buffer_mb`, `relative_time`, `capture_packets`. The startup chooser applies them
|
|
(`apply_source()`), which works because `choose_source()` runs before `ScopeModel` is constructed
|
|
— the one moment a buffer size can be applied at all. Switching configurations inside a running
|
|
app applies what can change live and says in the status line that the rest takes effect on the
|
|
next start; pretending otherwise would be a configuration that half-applies.
|
|
|
|
A configuration's *startup* settings reach the model from two directions, and both are needed:
|
|
`apply_source()` for a source picked in the chooser, and `apply_stored_settings()` for a source
|
|
named with `--server`, which never reaches the chooser at all. Without the second, a saved host's
|
|
buffer size, clock and capture ring were silently ignored — the layout still applied, because the
|
|
app loads that separately once a CONFIG names the signals — so an edit to any of the three read as
|
|
one that never took effect. **A flag the user typed wins over the file** (`--buffer-mb`,
|
|
`--relative-time`, `--capture-packets`): the file fills in what the command line did not say, and
|
|
`Config::buffer_mb_specified` is what tells "not asked" from "asked for the default".
|
|
|
|
The `active` file is written on every save and by the chooser's pick, so "which configuration was
|
|
I on" survives a restart. A session written before configurations existed is **imported once** as
|
|
the configuration named `default`, and the old file is renamed to `<host>_<port>.toml.imported`
|
|
rather than deleted: an import that cannot be undone by hand is a migration nobody can back out
|
|
of.
|
|
|
|
Nothing is lost by switching: the dialog saves the current session *before* loading the picked
|
|
one, which is the only ordering in which a switch cannot lose what it switches from.
|