Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/wiki/Controlling-the-Player.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ name: living-room
client id: fLNgT-h015The-sALW0IVza7J0FThgDU1leu1MLDww4
server: Music Assistant (connected)
trust: paired
paired servers: 1
pairing: none
state: playing
stream: receiving
Expand Down
6 changes: 5 additions & 1 deletion docs/wiki/Pairing-a-Player.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@ There are three ways to pair. All of them work from a terminal on the player's o
and all of them end the same way:

```console
$ sendspin-cli status | grep -E '^(trust|pairing)'
$ sendspin-cli status | grep -E '^(trust|paired|pairing)'
trust: paired
paired servers: 1
pairing: none
```

`trust` is about the server connected right now; `paired servers` counts the servers this
player holds a pairing for, connected or not. An unpaired server drops out of it.

| Method | You carry to the server | Player setup | Use it when |
|---|---|---|---|
| [Pairing token](#1-pairing-token) | a long `SP:…` string, pasted | none | you can copy and paste between the player and the server |
Expand Down
1 change: 1 addition & 0 deletions src/control.h
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ struct StatusSnapshot {

/// The connection's trust; absent before its handshake reports one.
std::optional<sendspin::ConnectionTrust> trust{};
size_t paired_servers{0}; ///< servers with a stored pairing, connected or not

std::string pairing_server_id; ///< the server a pairing is in progress with, else empty
std::string pairing_code; ///< the dynamic code on show, else empty
Expand Down
1 change: 1 addition & 0 deletions src/control_common.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ std::string format_status(const StatusSnapshot& snapshot) {
append_line(out, "trust",
*snapshot.trust == sendspin::ConnectionTrust::USER ? "paired" : "unpaired");
}
append_line(out, "paired servers", std::to_string(snapshot.paired_servers));

append_line(out, "pairing",
snapshot.pairing_server_id.empty()
Expand Down
10 changes: 7 additions & 3 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,8 @@ class ControlDispatcher final : public ControlHandler {
const MetadataLogger& metadata_logger,
const PairingListener& pairing_listener,
const PlayerListener& player_listener, sendspin::PlayerRole& player,
const AudioSink& sink, const SourceCapture* capture)
const AudioSink& sink, const SourceCapture* capture,
const StateStore& state_store)
: opts_(opts),
client_(client),
controller_(controller),
Expand All @@ -414,7 +415,8 @@ class ControlDispatcher final : public ControlHandler {
player_listener_(player_listener),
player_(player),
sink_(sink),
capture_(capture) {}
capture_(capture),
state_store_(state_store) {}

std::string handle_control_request(const std::string& line) override {
std::string name;
Expand Down Expand Up @@ -495,6 +497,7 @@ class ControlDispatcher final : public ControlHandler {
snapshot.client_id = this->client_.client_id();
snapshot.connected = this->client_.is_connected();
snapshot.trust = this->pairing_listener_.trust();
snapshot.paired_servers = this->state_store_.pairing_record_count();
Comment on lines 497 to +500
// A dropped connection ends its attempt without a callback.
if (snapshot.connected) {
snapshot.pairing_server_id = this->pairing_listener_.server_id();
Expand Down Expand Up @@ -560,6 +563,7 @@ class ControlDispatcher final : public ControlHandler {
sendspin::PlayerRole& player_;
const AudioSink& sink_;
const SourceCapture* capture_; ///< null without --input
const StateStore& state_store_;
};

/// Binds the control socket, or logs why this run has none.
Expand Down Expand Up @@ -982,7 +986,7 @@ int main(int argc, char* argv[]) {

ControlDispatcher control_dispatcher(opts, client, controller, metadata, metadata_logger,
pairing_listener, player_listener, player, *sink,
capture.get());
capture.get(), state_store);

std::unique_ptr<OutboundMode> outbound;
if (opts.was_given(Opt::Server)) {
Expand Down
18 changes: 12 additions & 6 deletions src/state_store.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

#include "key_value_file.h"
#include "sendspin/config.h"
#include "sendspin/persistence_codec.h"
#include "sendspin/persistence_keys.h"

#ifdef _WIN32
Expand All @@ -31,7 +32,6 @@
#include <unistd.h>
#endif

#include <algorithm>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
Expand Down Expand Up @@ -162,18 +162,24 @@ bool StateStore::set_blob(const std::string& key, const uint8_t* data, size_t le
return this->set_all({{KEY_BLOB_PREFIX + key, to_hex(data, len)}});
}

bool StateStore::has_pairing_record() const {
size_t StateStore::pairing_record_count() const {
const size_t slots = sendspin::SendspinClientConfig::DEFAULT_MAX_PAIRING_RECORDS;
size_t count = 0;
for (size_t slot = 0; slot < slots; ++slot) {
const std::optional<std::vector<uint8_t>> record =
this->blob(sendspin::persistence_keys::record_slot_key(slot));
// A freed slot is kept as zeroes.
// Counted as the library loads them: a freed slot is kept as zeroes, which it rejects
// along with any other blob it could not use.
if (record.has_value() &&
std::any_of(record->begin(), record->end(), [](uint8_t byte) { return byte != 0; })) {
return true;
sendspin::decode_pairing_record(record->data(), record->size()).has_value()) {
++count;
}
}
return false;
return count;
}

bool StateStore::has_pairing_record() const {
return this->pairing_record_count() > 0;
}

std::optional<uint8_t> StateStore::volume() const {
Expand Down
3 changes: 3 additions & 0 deletions src/state_store.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class StateStore {
std::optional<std::vector<uint8_t>> blob(const std::string& key) const;
bool set_blob(const std::string& key, const uint8_t* data, size_t len);

/// How many servers the library has a pairing record stored for.
size_t pairing_record_count() const;

/// True if the library has a pairing record stored for any server.
bool has_pairing_record() const;

Expand Down
10 changes: 10 additions & 0 deletions tests/control_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,16 @@ TEST(FormatStatus, TrustFollowsTheConnection) {
EXPECT_EQ(field(format_status(snapshot), "trust"), "not connected");
}

TEST(FormatStatus, PairedServersAreReportedEvenWhileDisconnected) {
StatusSnapshot snapshot;
EXPECT_EQ(field(format_status(snapshot), "paired servers"), "0");

// What an installer reads to tell "never paired" from "paired, server not here yet".
snapshot.paired_servers = 2;
EXPECT_EQ(field(format_status(snapshot), "trust"), "not connected");
EXPECT_EQ(field(format_status(snapshot), "paired servers"), "2");
}

TEST(FormatStatus, AnIdlePlayerReportsNoPairing) {
const std::string block = format_status(playing_snapshot());
EXPECT_EQ(field(block, "pairing"), "none");
Expand Down
40 changes: 40 additions & 0 deletions tests/state_store_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,46 @@ TEST(StateStore, APairingRecordIsAnOccupiedSlotNotAFreedOne) {
EXPECT_TRUE(loaded(scratch.file()).has_pairing_record());
}

TEST(StateStore, CountsEachOccupiedPairingSlot) {
const ScratchDir scratch;
ASSERT_TRUE(scratch.created());
StateStore store(scratch.file());
EXPECT_EQ(store.pairing_record_count(), 0U);

const std::vector<uint8_t> record(sendspin::persistence_keys::RECORD_SLOT_SIZE, 0x5a);
const std::vector<uint8_t> freed(sendspin::persistence_keys::RECORD_SLOT_SIZE, 0);
ASSERT_TRUE(store.set_blob(sendspin::persistence_keys::record_slot_key(0), record.data(),
record.size()));
ASSERT_TRUE(
store.set_blob(sendspin::persistence_keys::record_slot_key(1), freed.data(), freed.size()));
ASSERT_TRUE(store.set_blob(sendspin::persistence_keys::record_slot_key(2), record.data(),
record.size()));
EXPECT_EQ(store.pairing_record_count(), 2U);
EXPECT_EQ(loaded(scratch.file()).pairing_record_count(), 2U);

// Unpairing zeroes the slot, which no longer counts.
ASSERT_TRUE(
store.set_blob(sendspin::persistence_keys::record_slot_key(0), freed.data(), freed.size()));
EXPECT_EQ(store.pairing_record_count(), 1U);
}

TEST(StateStore, DoesNotCountASlotTheLibraryWouldNotLoad) {
const ScratchDir scratch;
ASSERT_TRUE(scratch.created());
StateStore store(scratch.file());

// Too short to be a record, and a record whose PSK half is all zeroes.
const std::vector<uint8_t> short_blob(sendspin::persistence_keys::RECORD_SLOT_SIZE / 2, 0x5a);
std::vector<uint8_t> zero_psk(sendspin::persistence_keys::PAIRING_PSK_SIZE, 0);
zero_psk.resize(sendspin::persistence_keys::RECORD_SLOT_SIZE, 0x5a);
ASSERT_TRUE(store.set_blob(sendspin::persistence_keys::record_slot_key(0), short_blob.data(),
short_blob.size()));
ASSERT_TRUE(store.set_blob(sendspin::persistence_keys::record_slot_key(1), zero_psk.data(),
zero_psk.size()));
EXPECT_EQ(store.pairing_record_count(), 0U);
EXPECT_FALSE(store.has_pairing_record());
}

// Upgrading a pre-rc1 state file

/// The output-delay blob as milliseconds, or nothing when absent or the wrong size.
Expand Down
Loading