Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/workflows/build-armv6.yml
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,10 @@ jobs:
/etc/sendspin-cli.conf; there is an annotated example beside the installed README,
under usr/local/share/doc/sendspin-cli. Its ExecStart names /usr/local/bin/sendspin-cli
absolutely, so a binary put anywhere else needs the unit changed to match.

To run it as yourself instead, following your session's PipeWire or PulseAudio, there is a
user unit at usr/local/lib/systemd/user/sendspin-cli.service: systemctl --user enable --now
sendspin-cli, configured in ~/.config/sendspin-cli/config. Not both: they share a port.
LINUX

# Explains the build-host line, and reads the glibc floor off the binary.
Expand Down Expand Up @@ -285,6 +289,7 @@ jobs:
echo usr/local/bin/sendspin-cli
echo usr/local/lib/sysusers.d/sendspin-cli.conf
echo usr/local/lib/systemd/system/sendspin-cli.service
echo usr/local/lib/systemd/user/sendspin-cli.service
echo usr/local/share/doc/sendspin-cli/LICENSE
echo usr/local/share/doc/sendspin-cli/README.md
echo usr/local/share/doc/sendspin-cli/contributors.md
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,10 @@ jobs:
/etc/sendspin-cli.conf; there is an annotated example beside the installed README,
under usr/local/share/doc/sendspin-cli. Its ExecStart names /usr/local/bin/sendspin-cli
absolutely, so a binary put anywhere else needs the unit changed to match.

To run it as yourself instead, following your session's PipeWire or PulseAudio, there is a
user unit at usr/local/lib/systemd/user/sendspin-cli.service: systemctl --user enable --now
sendspin-cli, configured in ~/.config/sendspin-cli/config. Not both: they share a port.
LINUX
fi

Expand Down Expand Up @@ -530,6 +534,7 @@ jobs:
if [ "$RUNNER_OS" = 'Linux' ]; then
echo usr/local/lib/sysusers.d/sendspin-cli.conf
echo usr/local/lib/systemd/system/sendspin-cli.service
echo usr/local/lib/systemd/user/sendspin-cli.service
fi
} | sort >"$expected"

Expand Down Expand Up @@ -609,6 +614,9 @@ jobs:
echo "What was installed as $unit:"
cat "$unit"
systemd-analyze verify "$unit"
# --user needs a runtime directory to initialise, which a runner step may not export.
XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" \
systemd-analyze --user verify /usr/local/lib/systemd/user/sendspin-cli.service

# Run the hardened unit as its account on a real systemd runner, via the same systemd-sysusers step as README.
- name: Run the unit as its own user, with the hardening block
Expand Down
7 changes: 7 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -385,13 +385,20 @@ if(SENDSPIN_CLI_IS_TOP_LEVEL)
set(SENDSPIN_CLI_INSTALLED_BINARY "${CMAKE_INSTALL_FULL_BINDIR}/sendspin-cli")
configure_file(packaging/sendspin-cli.service.in
${CMAKE_CURRENT_BINARY_DIR}/sendspin-cli.service @ONLY)
configure_file(packaging/sendspin-cli.user.service.in
${CMAKE_CURRENT_BINARY_DIR}/sendspin-cli.user.service @ONLY)

# lib/systemd/system, not the multiarch libdir, where systemd would not look.
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/sendspin-cli.service
DESTINATION lib/systemd/system
COMPONENT ${SENDSPIN_CLI_COMPONENT}
)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/sendspin-cli.user.service
DESTINATION lib/systemd/user
RENAME sendspin-cli.service
COMPONENT ${SENDSPIN_CLI_COMPONENT}
)

# lib/sysusers.d, renamed to the one-.conf-per-package name.
install(FILES packaging/sendspin-cli.sysusers.conf
Expand Down
71 changes: 47 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,54 +1,71 @@
# sendspin-cli

## What is it?

`sendspin-cli` is a headless audio player for the
[Sendspin](https://github.com/Sendspin/spec) protocol. It appears on your network, a
Sendspin server finds it, and it plays synchronized audio with the rest of your group.

It runs on Linux and Apple-silicon macOS. On Linux, it can also run as a system service.
It runs on Linux and Apple-silicon macOS.

## How can I run it?
## Quick start

### Linux and Raspberry Pi

The quickest way to install a released build is the guided Linux installer:
```bash
curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-cpp-cli/main/scripts/get_started_linux.sh | bash
```

It downloads the release for your machine, verifies its checksum, asks for a player name
and an output device, and starts the player as a systemd service. Every command that needs
root is printed first and waits for your yes.

Prefer to read it first?

```bash
curl -fLO https://raw.githubusercontent.com/Sendspin/sendspin-cpp-cli/main/scripts/get_started_linux.sh
less get_started_linux.sh
chmod +x get_started_linux.sh
./get_started_linux.sh
bash get_started_linux.sh
```

The script downloads the right release for your machine, verifies it, and guides you
through choosing an audio device. See the
[Linux quick start](https://github.com/Sendspin/sendspin-cpp-cli/wiki/Getting-Started-on-Linux)
or [Raspberry Pi guide](https://github.com/Sendspin/sendspin-cpp-cli/wiki/Getting-Started-on-a-Raspberry-Pi)
for the complete walkthrough.
`--help` lists the flags, among them `--yes --name <name> --output <device>` for an
unattended install and `--user-service` to run as you and follow your desktop's PipeWire or
PulseAudio. The [Linux guide](https://github.com/Sendspin/sendspin-cpp-cli/wiki/Getting-Started-on-Linux)
and the [Raspberry Pi guide](https://github.com/Sendspin/sendspin-cpp-cli/wiki/Getting-Started-on-a-Raspberry-Pi)
have the full walkthrough.

### macOS

Download the Apple-silicon `.pkg` installer or tarball from
[Releases](https://github.com/Sendspin/sendspin-cpp-cli/releases), then follow the
[installation guide](https://github.com/Sendspin/sendspin-cpp-cli/wiki/Installation#macos).
[installation guide](https://github.com/Sendspin/sendspin-cpp-cli/wiki/Installation#macos)
and start a player from a terminal with `sendspin-cli -n living-room`.

### Start playing
### Pair it with your server

Once installed and configured, start a player from a terminal:
The player advertises itself on the local network and a Sendspin server finds it.
Connections are encrypted, and a server must pair with the player before it can play.
Print the token to paste into the server:

```bash
sendspin-cli -n living-room
# Linux service, as installed above
sudo sendspin-cli pair-token --control-socket /run/sendspin-cli/control.sock

# --user-service, macOS, or a player started from your own shell
sendspin-cli pair-token
```

It advertises itself on the local network and waits for a Sendspin server to connect.
Connections are encrypted, and a server must pair with the player before it can play:
`sendspin-cli pair-token` prints the token to paste into the server, and
[Pairing a player](https://github.com/Sendspin/sendspin-cpp-cli/wiki/Pairing-a-Player)
covers that and the two code-based ways. `--allow-unpaired` lets any server on the network
play without pairing.
For a Linux system-service installation, the quick-start guide explains how to enable
and check the service.

## Everyday use

The service reads its options from a
[config file](https://github.com/Sendspin/sendspin-cpp-cli/wiki/Configuration); the same
options are flags when you run the player yourself:

```bash
sendspin-cli -n living-room
```

The options most people need beyond a name:

Expand Down Expand Up @@ -76,9 +93,6 @@ sendspin-cli -n den --input default
| `--pairing-code <8 digits>` | A fixed code a server can pair with, confirmed with `sendspin-cli pair confirm`. Without it the player shows a fresh 6-digit code per attempt. |
| `-s, --server <host[:port]>` | Connect out to a server rather than waiting to be discovered, or `mdns:[<name>]` to discover one over mDNS. Turns off the mDNS advertisement. |

Any of these can go in a config file instead of on the command line — see
[Configuration](https://github.com/Sendspin/sendspin-cpp-cli/wiki/Configuration).

Common local controls are available from the same host:

```bash
Expand All @@ -87,6 +101,15 @@ sendspin-cli pause
sendspin-cli vol 40
```

## Uninstall

```bash
curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-cpp-cli/main/scripts/uninstall_linux.sh | bash
```

It removes the service and the installed files, and keeps your config and the player's
remembered state unless you pass `--purge`.

## Need more help?

The [wiki](https://github.com/Sendspin/sendspin-cpp-cli/wiki) is the complete end-user
Expand Down
4 changes: 2 additions & 2 deletions contributors.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ separate because it runs under emulation; it is required for releases.
- `src/` contains the player, command-line interface, output backends, discovery, and
local control implementation.
- `tests/` contains the GoogleTest unit suite.
- `packaging/` contains the service, sysusers, and configuration-file payload.
- `scripts/` contains installer, packaging, and smoke-test scripts.
- `packaging/` contains the system and user service units, sysusers, and configuration-file payload.
- `scripts/` contains installer, uninstaller, packaging, and smoke-test scripts.
- `docs/wiki/` contains the source for the published wiki.

## Releases
Expand Down
119 changes: 84 additions & 35 deletions docs/wiki/Getting-Started-on-Linux.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,56 @@ OS), systemd, a sound card, and root.

## The short way

```bash
curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-cpp-cli/main/scripts/get_started_linux.sh | bash
```

Or read it before it runs anything:

```bash
curl -fLO https://raw.githubusercontent.com/Sendspin/sendspin-cpp-cli/main/scripts/get_started_linux.sh
less get_started_linux.sh # it is about to run things as root; read it
chmod +x get_started_linux.sh
./get_started_linux.sh
less get_started_linux.sh
bash get_started_linux.sh
```

It installs the release for this machine's architecture and sets the service up. It does
not pipe into a shell, and it does not run anything as root without printing the exact
commands first and waiting for you to say yes:
Both behave the same. It installs the release for this machine's architecture, asks for a
player name and an output device, writes them to the config, and starts the service. Piped
into `bash`, its questions are still asked on your terminal. It does not run anything as
root without printing the exact commands first and waiting for you to say yes:

```
==> These are the commands that need root

sudo tar -xzf /tmp/tmp.XXXX/sendspin-cli-0.1.0-linux-arm64.tar.gz --strip-components=1 -C / sendspin-cli-0.1.0-linux-arm64/usr
sudo cp /usr/local/share/doc/sendspin-cli/sendspin-cli.conf.example /etc/sendspin-cli.conf
sudo tar -xzf /tmp/tmp.XXXX/sendspin-cli-0.3.0-linux-arm64.tar.gz --strip-components=1 -C / sendspin-cli-0.3.0-linux-arm64/usr
sudo systemd-sysusers
sudo systemctl daemon-reload
sudo systemctl enable sendspin-cli
sudo /usr/local/bin/sendspin-cli -l
sudo install -m 0644 /tmp/tmp.XXXX/sendspin-cli.conf /etc/sendspin-cli.conf
sudo systemctl restart sendspin-cli

Run them? [y/N]
```

`--yes` skips the prompt, and is required when stdin is not a terminal — a script that
cannot ask refuses rather than assuming. `--version v0.1.0` installs a specific release
instead of the newest.
The config it installs is composed in the temporary directory from the annotated example
plus your two answers, and the lines it sets are printed again just before it is copied.

| Flag | What it does |
|---|---|
| `--version v0.3.0` | Install that release instead of the newest. |
| `--name <name>` | The player's name, instead of being asked. |
| `--output <device>` | The output device, instead of being asked. |
| `--user-service` | Run as you rather than as the `sendspin-cli` account — see [below](#as-a-system-service-or-as-you). |
| `--system-service` | The default; also switches a user-service install back. |
| `--yes` | Ask nothing: run the root commands as printed, and take the name and output only from the flags. |

Piped, flags go after `bash -s --`:

```bash
curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-cpp-cli/main/scripts/get_started_linux.sh | bash -s -- --yes --name kitchen --output hw:1,0
```

With no terminal to ask on and no `--yes`, it refuses and installs nothing.

### What it actually does

Expand All @@ -47,33 +70,58 @@ instead of the newest.
and older is refused with the reason rather than an "unsupported" shrug.
2. **Finds the newest release** and downloads that archive plus `SHA256SUMS`.
3. **Verifies the checksum**, and stops without installing anything if it does not match.
4. **Unpacks it into `/`** with the member-selected `tar` form, so `BUILD-INFO.txt` stays in
4. **Checks the binary can load here.** Shared libraries this host lacks are mapped to
their `apt`, `dnf` or `pacman` packages and the install command joins the list you
confirm. A library it cannot map, a host with none of those package managers, or a glibc
older than the build needs stops it there, naming what is missing, with nothing installed.
5. **Unpacks it into `/`** with the member-selected `tar` form, so `BUILD-INFO.txt` stays in
the archive.
5. **Copies the annotated example config** to `/etc/sendspin-cli.conf` if you have none.
Every line in it is commented out, so it chooses nothing.
6. **Runs `systemd-sysusers`**, which creates the unprivileged `sendspin-cli` account the
unit runs as out of the declaration the payload just installed. This is the one step a
tarball cannot do for itself, and without it the unit does not start at all — see
unit runs as out of the declaration the payload just installed — see
[Running as a Service](Running-as-a-Service#it-runs-as-its-own-account).
7. **Enables the unit — and starts it only if an output is already configured.** More on
that below; it is the one surprising thing the script does.
8. **Lists this host's sound devices** and prints the two commands that finish the job.
7. **Enables the unit.**
8. **Asks for a name and an output device**, after listing what this host can play
through, and writes `name` and `output` into `/etc/sendspin-cli.conf` — seeded from the
annotated example if you have no config yet. A key your config already sets is never
asked for and never replaced.
9. **Starts the service**, waits two seconds, and either confirms it is running or shows
the journal's account of why it is not.

Re-running it is how you upgrade: it overwrites the same paths and restarts the service.
On a host whose config already names an `output` it asks nothing but the root confirmation.

### As a system service, or as you

On a fresh host the script asks which:

- **A system service** (the default, `--system-service`). The hardened unit, running as the
unprivileged `sendspin-cli` account, configured in `/etc/sendspin-cli.conf`. It has no
desktop session, so name a card — `hw:1,0` — rather than `default`. Right for a headless
box or a Pi.
- **A user service** (`--user-service`). Runs as you under `systemctl --user`, configured in
`~/.config/sendspin-cli/config`, so `output = default` follows your session's PipeWire or
PulseAudio. The script enables lingering so it starts at boot and survives logout, and adds
you to the `audio` group for direct `hw:` devices. Subcommands need no `sudo` and no
flags: `sendspin-cli status`.

Re-running it is how you upgrade: it overwrites the same paths, and restarts the service if
step 7 finds an `output` configured — which after a first run it will. Both step 6 and step 7
are idempotent, so nothing there minds being run twice.
Both listen on the same port, so choosing one stops and disables the other. See
[Running as a Service](Running-as-a-Service#a-user-unit-instead) for what the user unit is.

### Why it does not start the player
### When it does not start the player

A systemd **system** unit has no user session, so there is no PipeWire or PulseAudio for
ALSA's `default` PCM to follow — and the device that opens perfectly from your shell usually
will not open under `systemctl`. The unit is `Restart=on-failure` with `RestartSec=5`, so a
player started before you have named a card fails and is retried every five seconds forever,
while the script that started it prints congratulations.
One case: `--yes` with no `--output`, on a host whose config names none. There is nobody to
ask, and a system unit started without a card fails and is retried every five seconds —
`Restart=on-failure`, and no session for ALSA's `default` to follow. So the unit is enabled,
the device list is printed, and the two commands that finish the job are the last thing it
says. Answering the output question with nothing ends the same way.

### Removing it

```bash
curl -fsSL https://raw.githubusercontent.com/Sendspin/sendspin-cpp-cli/main/scripts/uninstall_linux.sh | bash
```

So it enables the unit, shows you the devices, and leaves starting it to you. Once
`/etc/sendspin-cli.conf` names an `output`, the script starts the player itself on every
subsequent run.
See [Uninstalling](Running-as-a-Service#uninstalling).

## The long way

Expand Down Expand Up @@ -130,8 +178,8 @@ Three forms are worth knowing, and there are more in
| `default` | follow the host's own configuration — PipeWire, PulseAudio or bare hardware |

`default` is the right answer from a login shell and usually the wrong one under a system
unit, for the reason above. Under a **user** unit (`systemctl --user`) it is right again,
because there the session and its sound server exist.
unit, which has no session to follow. Under the **user** service (`--user-service`) it is
right again, because there the session and its sound server exist.

## Check it worked

Expand Down Expand Up @@ -163,8 +211,9 @@ see [Controlling the Player](Controlling-the-Player).
## Now find it from your server

The player advertises `_sendspin._tcp` and waits. Open your Sendspin controller and it
should appear under the name it logged — which is `-n`, falling back to this host's name.
Nothing needs configuring on the server side.
should appear under the name it logged — which is `name`, falling back to this host's name.
A server has to pair with it before it may play; the script prints the `pair-token` command
for that as its last step, and [Pairing a Player](Pairing-a-Player) has the rest.

To go the other way and have the player dial the server instead, set `server` in the config:

Expand Down
Loading
Loading