Skip to content

Commit 672b2f2

Browse files
authored
Merge pull request #29 from balloobbot/task/identity-flags
Add the identity flags: --id, --manufacturer, --product-name
2 parents 998b040 + ee1edbe commit 672b2f2

13 files changed

Lines changed: 209 additions & 44 deletions

README.md

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -963,13 +963,14 @@ environment rather than as arguments:
963963
| `SENDSPIN_SERVER_ID` | the connected server's id |
964964
| `SENDSPIN_SERVER_NAME` | its friendly name |
965965
| `SENDSPIN_SERVER_URL` | the URL this run dialled — set only on an `-s` run |
966+
| `SENDSPIN_CLIENT_ID` | this player's id, when `--id` chose one |
966967
| `SENDSPIN_CLIENT_NAME` | this player's friendly name (`-n`) |
967968

968969
The vocabulary is the Python `sendspin-cli`'s, deliberately: a hook script written
969-
against one player runs unchanged against the other. `SENDSPIN_CLIENT_ID` is part
970-
of that vocabulary and reserved here, but nothing arrives in it yet: the library
971-
derives this player's id from the interface MAC and does not expose it, so the
972-
honest value waits on a flag that chooses one. A variable whose value is
970+
against one player runs unchanged against the other. `SENDSPIN_CLIENT_ID` carries the id
971+
`--id` or the `id` config key chose: the library derives one from the interface
972+
MAC when neither did, and does not expose what it derived, so a run that
973+
configured no id leaves the variable unset. A variable whose value is
973974
unknown for the event is left unset rather than exported empty, so `[ -n
974975
"$SENDSPIN_SERVER_ID" ]` means what it says — and any `SENDSPIN_*` inherited from
975976
the player's own environment is cleared first, so a wrapper script's stale export
@@ -1082,14 +1083,21 @@ which is what a foreground run whose terminal has just closed should do.
10821083
The flags follow squeezelite's: `-o` output device, `-l` list devices, `-n` name,
10831084
`-s` server, `-z` daemonize, `-P` pidfile, `-d`/`-f` logging. All but `-l` and
10841085
`-z` also have a long spelling — `--output --name --server --pidfile --logfile
1085-
--log-level` — so that every config key is a flag name. Eleven more are long-only
1086+
--log-level` — so that every config key is a flag name. Fourteen more are long-only
10861087
because they are not squeezelite's: `--port`, the port this player serves on,
10871088
`--buffer-ms`, `--static-delay`, the two mDNS flags `--no-mdns` and `--mdns-name`,
10881089
the two control-socket flags `--control-socket` and `--no-control`, the two stream
1089-
hooks `--hook-start` and `--hook-stop`, and `--config` and `--state-dir` for the
1090-
two files above. Run `--help` for the current state of each
1091-
— a few still point at [`docs/ROADMAP.md`](docs/ROADMAP.md) for behaviour that is
1092-
not built yet.
1090+
hooks `--hook-start` and `--hook-stop`, the three identity flags `--id`,
1091+
`--manufacturer` and `--product-name`, and `--config` and `--state-dir` for the
1092+
two files above. `--id` is the one to know about: it is the *stable* id a server
1093+
files this player's volume, group and pairing under, and without it the id is
1094+
derived from the network interface MAC — so two players on one host share it, and
1095+
each server-side setting lands on whichever connected last. A dual-mono pair needs
1096+
its own `--id` per instance, and its own `--port` and `--state-dir` with it — the
1097+
state file is namespaced by neither `--id` nor `--port`, so without `--state-dir`
1098+
the two overwrite each other's volume, mute, delay and last server. Run `--help` for the current
1099+
state of each — a few still point at [`docs/ROADMAP.md`](docs/ROADMAP.md) for
1100+
behaviour that is not built yet.
10931101

10941102
A **subcommand comes first**, before any flag: `sendspin-cli vol 50 --port 9000`,
10951103
not `sendspin-cli --port 9000 vol 50`. That is not getopt permutation showing
@@ -1248,8 +1256,9 @@ mode `0600`, so a player that loses power mid-write leaves either the old file o
12481256
the new one and never half of either.
12491257

12501258
Two players on one host share this file unless you give each its own
1251-
`--state-dir`. They already need different `--port`s; give them different state
1252-
directories too, or the second one to save its volume overwrites the first's.
1259+
`--state-dir`. They already need different `--id`s and `--port`s; give them
1260+
different state directories too, or the second one to save its volume overwrites
1261+
the first's.
12531262

12541263
## Tests
12551264

docs/ROADMAP.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2098,9 +2098,9 @@ no workaround either — the control socket answers questions, it does not annou
20982098
always; `SENDSPIN_SERVER_ID`, `SENDSPIN_SERVER_NAME`, `SENDSPIN_SERVER_URL` (the URL
20992099
this run dialled — outbound only, as in Python) and `SENDSPIN_CLIENT_NAME` where
21002100
known. A hook script written against one player runs unchanged against the other.
2101-
`SENDSPIN_CLIENT_ID` is reserved but never set yet: the library derives its id from
2102-
the interface MAC and does not expose it, so the honest value arrives with a future
2103-
`--id` flag. `SENDSPIN_SERVER_URL` is what this run *dialled*, which is not the same
2101+
`SENDSPIN_CLIENT_ID` carries the `--id` value item 23 chooses: the library's
2102+
MAC-derived default is not exposed, so without the flag it stays unset.
2103+
`SENDSPIN_SERVER_URL` is what this run *dialled*, which is not the same
21042104
claim as which server answered: `-s` leaves the inbound listener up, and the library
21052105
reports that a connection is up without saying where it came from — no connect callback,
21062106
and nothing exposing a connection's URL or direction — so one that dialled in while an
@@ -2140,3 +2140,32 @@ no workaround either — the control socket answers questions, it does not annou
21402140
reach into. SIGPIPE goes back to `SIG_DFL` for the same reason: the player ignores it, and
21412141
an ignored disposition survives `execve()` where a caught one does not, so `… | head -1`
21422142
inside a hook would otherwise report a failed write rather than ending.
2143+
2144+
2145+
### 23. Identity flags (`--id`, `--manufacturer`, `--product-name`) — *shipped*
2146+
2147+
The client id was the library's MAC-derived default with no way to choose one, and the
2148+
`client/hello` device info was hardcoded. The id is the half that bites: it is the
2149+
*stable* identity a server files volume, group membership and pairing under — `-n` is
2150+
only what it displays — and two players on one host derive the same MAC, so each
2151+
server-side setting lands on whichever connected last. The dual-mono
2152+
two-daemons-one-host pattern needs a flag; the Python CLI has all three.
2153+
2154+
**Shipped** in `src/cli.{h,cpp}`, `src/main.cpp`, `tests/cli_test.cpp` and
2155+
`tests/config_file_test.cpp`:
2156+
2157+
- **`--id <id>`** sets `SendspinClientConfig::client_id`, and its config key is `id`.
2158+
The default stays empty on purpose — the library derives the MAC-based id only when
2159+
nothing is set, and that remains the right identity for one fixed endpoint per host.
2160+
The flag also feeds item 22's `SENDSPIN_CLIENT_ID`, which until now was reserved but
2161+
never set: the derived id is not exposed by the library, so the flag's value is the
2162+
first honest one a hook can be handed.
2163+
- **`--manufacturer` / `--product-name`** override the hello device info, for the
2164+
integrator whose product embeds this player and should be listed as itself. Defaults
2165+
stay `sendspin-cpp-cli` / `sendspin-cli`, declaring what this really is.
2166+
- All three are refused empty, warned about on a subcommand run like every other
2167+
daemon-only flag, and settable from the config file through the same
2168+
`apply_option()` door.
2169+
2170+
Verified against a real `aiosendspin` server: the connected client reports the `--id`
2171+
value as its `client_id`, and the start hook exports it as `SENDSPIN_CLIENT_ID`.

docs/wiki/Configuration.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ already does that.
4949
|---|---|---|---|
5050
| `output` | `-o`, `--output` | a device: `hw:1,0`, `default`, `portaudio:2`, `null`, `stdout` | `default` where ALSA is built in, else `portaudio`, else `null` |
5151
| `name` | `-n`, `--name` | the friendly name a controller shows | this host's name |
52+
| `id` | `--id` | the stable client id a server files this player's settings under — two players on one host must not share it | derived from the interface MAC |
53+
| `manufacturer` | `--manufacturer` | the manufacturer `client/hello` reports | `sendspin-cpp-cli` |
54+
| `product-name` | `--product-name` | the product name `client/hello` reports | `sendspin-cli` |
5255
| `server` | `-s`, `--server` | `<host>[:<port>]`, a `ws://` URL, or `mdns:[<name>]` | none — wait to be discovered |
5356
| `port` | `--port` | the port this player's own WebSocket server listens on | `8928` |
5457
| `buffer-ms` | `--buffer-ms` | audio the output backend keeps queued, 10–2000 | `100` |
@@ -173,8 +176,8 @@ Writes go through a temporary, an `fsync` and a `rename` at mode `0600`, so a pl
173176
loses power mid-write leaves either the old file or the new one and never half of either.
174177

175178
**Two players on one host share this file** unless you give each its own `--state-dir`. They
176-
already need different `--port`s; give them different state directories too, or the second
177-
one to save its volume overwrites the first's.
179+
already need different `--id`s and `--port`s; give them different state directories too, or
180+
the second one to save its volume overwrites the first's.
178181

179182
### `static-delay` versus a remembered `static-delay-ms`
180183

docs/wiki/Controlling-the-Player.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,11 +202,12 @@ The event's facts arrive in the environment, in the same vocabulary the Python
202202
`sendspin-cli` uses — a hook script written against one runs unchanged against the other:
203203
`SENDSPIN_EVENT` (`start` or `stop`) always, and `SENDSPIN_SERVER_ID`,
204204
`SENDSPIN_SERVER_NAME`, `SENDSPIN_SERVER_URL` (`-s` runs only, and the URL this run
205-
dialled rather than a statement about which server answered — see below) and
206-
`SENDSPIN_CLIENT_NAME` where known. An unknown is left *unset* rather than exported
207-
empty, so `[ -n "$SENDSPIN_SERVER_ID" ]` means what it says. A stop event carries the
208-
same server facts as the start it pairs with — gathered when the stream started, because
209-
a stream usually ends when its connection goes and there is nothing left to ask by then.
205+
dialled rather than a statement about which server answered — see below),
206+
`SENDSPIN_CLIENT_ID` (when `--id` chose one) and `SENDSPIN_CLIENT_NAME` where known. An
207+
unknown is left *unset* rather than exported empty, so `[ -n "$SENDSPIN_SERVER_ID" ]`
208+
means what it says. A stop event carries the same server facts as the start it pairs
209+
with — gathered when the stream started, because a stream usually ends when its
210+
connection goes and there is nothing left to ask by then.
210211

211212
`SENDSPIN_SERVER_URL` is the one to read carefully: it is the URL this run dialled, not
212213
which server answered. `-s` leaves the inbound listener up, so a server that dials *in*

docs/wiki/Getting-Started-on-a-Raspberry-Pi.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,9 @@ nothing for a player running as you.
150150
- **`avahi-daemon` is what provides mDNS on a Pi**, and Raspberry Pi OS ships it running. If
151151
you have turned it off, the player warns and retries rather than failing — but nothing will
152152
discover it until it is back.
153-
- **One player per Pi.** Two on one host need different `--port`, different `--state-dir`
154-
and different control sockets. It works; it is just not what a Pi is usually for.
153+
- **One player per Pi.** Two on one host need different `--id`, different `--port`,
154+
different `--state-dir` and different control sockets. It works; it is just not what a
155+
Pi is usually for.
155156

156157
## Next
157158

docs/wiki/Troubleshooting.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,9 +266,10 @@ systemctl status sendspin-cli
266266
pgrep -a sendspin-cli
267267
```
268268

269-
To run a second player on purpose, give it its own `--port`, its own `--control-socket` and
270-
its own `--state-dir` — they share the state file otherwise, and the second to save its
271-
volume overwrites the first's.
269+
To run a second player on purpose, give it its own `--id`, its own `--port`, its own
270+
`--control-socket` and its own `--state-dir` — they share the state file otherwise, the
271+
server files both under one identity, and the second to save its volume overwrites the
272+
first's.
272273

273274
## `status` is telling me something odd
274275

packaging/sendspin-cli.conf.example

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,23 @@
2020
# here at all: -l, -z, --config, --help and --version are refused as unknown keys.
2121
#
2222
# Every line below is commented out, since a line that is not is a value chosen. Where an
23-
# option has a default, that is the value shown; `name` and `server` have none worth
24-
# printing, so those two are examples instead.
23+
# option has a default, that is the value shown; the keys with none worth printing --
24+
# `name`, `id`, `server` and the two hooks -- carry an example instead.
2525

2626
# The name a controller shows for this endpoint. Defaults to this host's name.
2727
#name = kitchen
2828

29+
# The stable client id a server files this player's volume, group and pairing under --
30+
# the name above is only what it displays. Defaults to an id derived from the network
31+
# interface MAC, which two players on one host would share: give each its own id (and
32+
# its own port and state-dir).
33+
#id = kitchen-left
34+
35+
# The device info client/hello carries, shown in server device lists. For a product
36+
# that embeds this player and should be listed as itself.
37+
#manufacturer = sendspin-cpp-cli
38+
#product-name = sendspin-cli
39+
2940
# The output device: a reserved name (null, stdout, -) or a <backend>:<device> pair.
3041
# `sendspin-cli -l` lists what this host has and what each device accepts. The default is
3142
# ALSA's own `default` PCM wherever this build has ALSA, which follows whatever the host is
@@ -53,8 +64,9 @@
5364
# Shell commands run when a stream starts and when it stops -- an amplifier relay, a
5465
# light. `/bin/sh -c`, with the event's facts in SENDSPIN_* environment variables:
5566
# SENDSPIN_EVENT (start|stop) always, and SENDSPIN_SERVER_ID, SENDSPIN_SERVER_NAME,
56-
# SENDSPIN_SERVER_URL and SENDSPIN_CLIENT_NAME where known. The hook runs without
57-
# blocking playback, its output goes to the log, and a non-zero exit is a warning.
67+
# SENDSPIN_SERVER_URL, SENDSPIN_CLIENT_ID and SENDSPIN_CLIENT_NAME where known. The hook
68+
# runs without blocking playback, its output goes to the log, and a non-zero exit is a
69+
# warning.
5870
#hook-start = amixer set Master unmute
5971
#hook-stop = amixer set Master mute
6072

src/cli.cpp

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ enum LongOnly {
7070
OPT_CONFIG,
7171
OPT_HOOK_START,
7272
OPT_HOOK_STOP,
73+
OPT_ID,
74+
OPT_MANUFACTURER,
75+
OPT_PRODUCT_NAME,
7376
};
7477

7578
/// @brief An option a config file may set: its key, and how a diagnostic names it.
@@ -108,6 +111,9 @@ const std::vector<SettableOption>& settable_options() {
108111
{Opt::StateDir, "state-dir", "--state-dir"},
109112
{Opt::HookStart, "hook-start", "--hook-start"},
110113
{Opt::HookStop, "hook-stop", "--hook-stop"},
114+
{Opt::ClientId, "id", "--id"},
115+
{Opt::Manufacturer, "manufacturer", "--manufacturer"},
116+
{Opt::ProductName, "product-name", "--product-name"},
111117
};
112118
return table;
113119
}
@@ -387,6 +393,24 @@ bool apply_option(const SettableOption& option, const std::string& value, Option
387393
}
388394
out.hook_stop = value;
389395
break;
396+
case Opt::ClientId:
397+
if (empty_value()) {
398+
return false;
399+
}
400+
out.client_id = value;
401+
break;
402+
case Opt::Manufacturer:
403+
if (empty_value()) {
404+
return false;
405+
}
406+
out.manufacturer = value;
407+
break;
408+
case Opt::ProductName:
409+
if (empty_value()) {
410+
return false;
411+
}
412+
out.product_name = value;
413+
break;
390414
case Opt::NoMdns:
391415
if (!parse_bool(value, out.no_mdns)) {
392416
error = "invalid --no-mdns '" + value + "' -- expected true or false";
@@ -581,6 +605,9 @@ bool parse_options(int argc, char* argv[], Options& out, std::FILE* err) {
581605
{"state-dir", required_argument, nullptr, OPT_STATE_DIR},
582606
{"hook-start", required_argument, nullptr, OPT_HOOK_START},
583607
{"hook-stop", required_argument, nullptr, OPT_HOOK_STOP},
608+
{"id", required_argument, nullptr, OPT_ID},
609+
{"manufacturer", required_argument, nullptr, OPT_MANUFACTURER},
610+
{"product-name", required_argument, nullptr, OPT_PRODUCT_NAME},
584611
{nullptr, 0, nullptr, 0},
585612
};
586613

@@ -741,6 +768,15 @@ bool parse_options(int argc, char* argv[], Options& out, std::FILE* err) {
741768
case OPT_HOOK_STOP:
742769
apply(Opt::HookStop, optarg);
743770
break;
771+
case OPT_ID:
772+
apply(Opt::ClientId, optarg);
773+
break;
774+
case OPT_MANUFACTURER:
775+
apply(Opt::Manufacturer, optarg);
776+
break;
777+
case OPT_PRODUCT_NAME:
778+
apply(Opt::ProductName, optarg);
779+
break;
744780
case ':':
745781
fail("option '" + offending_option(flag_argv, optind) + "' needs a value");
746782
break;
@@ -908,9 +944,10 @@ bool parse_options(int argc, char* argv[], Options& out, std::FILE* err) {
908944
// listen on anything. Left it out and it would silently produce a "this player was
909945
// started with --no-control" message about the wrong process.
910946
static constexpr Opt DAEMON_ONLY[] = {
911-
Opt::Device, Opt::Name, Opt::Server, Opt::Daemonize, Opt::Pidfile,
912-
Opt::Logfile, Opt::LogLevel, Opt::BufferMs, Opt::NoMdns, Opt::MdnsName,
913-
Opt::NoControl, Opt::StateDir, Opt::StaticDelay, Opt::HookStart, Opt::HookStop,
947+
Opt::Device, Opt::Name, Opt::Server, Opt::Daemonize, Opt::Pidfile,
948+
Opt::Logfile, Opt::LogLevel, Opt::BufferMs, Opt::NoMdns, Opt::MdnsName,
949+
Opt::NoControl, Opt::StateDir, Opt::StaticDelay, Opt::HookStart, Opt::HookStop,
950+
Opt::ClientId, Opt::Manufacturer, Opt::ProductName,
914951
};
915952
for (Opt opt : DAEMON_ONLY) {
916953
if (out.was_given(opt)) {
@@ -1018,6 +1055,12 @@ void print_usage(std::FILE* out, const char* prog) {
10181055
std::fprintf(out, " -l List output devices with their capabilities, and exit\n");
10191056
std::fprintf(out, " -n, --name <name>\n");
10201057
std::fprintf(out, " Friendly name (default: this host's name)\n");
1058+
std::fprintf(out, " --id <id> Stable client id, which is what a server files this\n");
1059+
std::fprintf(out, " player's volume, group and pairing under -- -n is only\n");
1060+
std::fprintf(out, " what it displays. Defaults to an id derived from the\n");
1061+
std::fprintf(out, " network interface MAC, which two players on one host\n");
1062+
std::fprintf(out, " would share: give each its own --id (and its own\n");
1063+
std::fprintf(out, " --port and --state-dir)\n");
10211064
std::fprintf(out, " -s, --server <server>\n");
10221065
std::fprintf(out, " Connect out to <host>[:<port>] or a ws:// URL\n");
10231066
std::fprintf(out, " (the server's port defaults to %u), retrying until it\n",
@@ -1112,14 +1155,20 @@ void print_usage(std::FILE* out, const char* prog) {
11121155
std::fprintf(out, " unit has neither, so pair StateDirectory= with this\n");
11131156
std::fprintf(out, " flag; with none of the three the player still runs and\n");
11141157
std::fprintf(out, " simply remembers nothing\n");
1158+
std::fprintf(out, " --manufacturer <text>\n");
1159+
std::fprintf(out, " --product-name <text>\n");
1160+
std::fprintf(out, " The device info client/hello carries, shown in server\n");
1161+
std::fprintf(out, " device lists (defaults: sendspin-cpp-cli, sendspin-cli).\n");
1162+
std::fprintf(out, " For a product that embeds this player and should be\n");
1163+
std::fprintf(out, " listed as itself\n");
11151164
std::fprintf(out, " --hook-start <command>\n");
11161165
std::fprintf(out, " --hook-stop <command>\n");
11171166
std::fprintf(out, " Run <command> through /bin/sh when a stream starts or\n");
11181167
std::fprintf(out, " stops -- an amplifier relay, a light. The event's facts\n");
11191168
std::fprintf(out, " arrive as SENDSPIN_EVENT (start|stop) and, where known,\n");
11201169
std::fprintf(out, " SENDSPIN_SERVER_ID, SENDSPIN_SERVER_NAME,\n");
1121-
std::fprintf(out, " SENDSPIN_SERVER_URL (outbound only) and\n");
1122-
std::fprintf(out, " SENDSPIN_CLIENT_NAME. The hook runs without blocking\n");
1170+
std::fprintf(out, " SENDSPIN_SERVER_URL (outbound only), SENDSPIN_CLIENT_ID\n");
1171+
std::fprintf(out, " and SENDSPIN_CLIENT_NAME. The hook runs without blocking\n");
11231172
std::fprintf(out, " playback; its output goes to the log, and a non-zero\n");
11241173
std::fprintf(out, " exit is a warning, not a player failure\n");
11251174
std::fprintf(out, " -h, --help Show this help\n");

0 commit comments

Comments
 (0)