Skip to content

Commit 9e60ab9

Browse files
authored
Merge pull request #42 from Sendspin/fix/stale-server-url
Stop exporting a stale dialled URL to the stream hooks
2 parents 5433bc0 + 19558e0 commit 9e60ab9

7 files changed

Lines changed: 143 additions & 28 deletions

File tree

README.md

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -992,11 +992,14 @@ the player's own environment is cleared first, so a wrapper script's stale expor
992992
cannot describe some other run to the hook.
993993

994994
`SENDSPIN_SERVER_URL` says what this run dialled, not which server answered.
995-
Those are the same thing whenever `-s` is how the player got its connection —
996-
but `-s` leaves the inbound listener up, and a server that dials *in* while an
997-
outbound attempt is outstanding or has failed is a connection the player cannot
998-
tell apart from its own: the library reports that one is up, not where it came
999-
from. A hook that must be certain which server it is acting on should read
995+
The player rules out what it can: losing a connection clears the URL rather
996+
than letting it describe whatever connects next, and when discovery chose the
997+
server, the URL is exported only if the stream arrived from the server_id it
998+
dialled. What it cannot rule out is a literal `-s URL` run — `-s` leaves the
999+
inbound listener up, and a server that dials *in* while that attempt is
1000+
outstanding or has failed is a connection the player cannot tell apart from
1001+
its own: the library reports that one is up, not where it came from. A hook
1002+
that must be certain which server it is acting on should read
10001003
`SENDSPIN_SERVER_ID`, which always describes the connection the stream arrived
10011004
on.
10021005

docs/wiki/Controlling-the-Player.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,13 @@ with — gathered when the stream started, because a stream usually ends when it
210210
connection goes and there is nothing left to ask by then.
211211

212212
`SENDSPIN_SERVER_URL` is the one to read carefully: it is the URL this run dialled, not
213-
which server answered. `-s` leaves the inbound listener up, so a server that dials *in*
214-
while an outbound attempt is outstanding or has failed is a connection the player cannot
215-
tell from its own — the library reports that one is up, not where it came from. Read
216-
`SENDSPIN_SERVER_ID` when a hook has to be certain which server it is acting on.
213+
which server answered. A lost connection clears it, and when discovery chose the server it
214+
is exported only if the stream arrived from the server it dialled. On a literal `-s URL`
215+
run the rest is out of the player's reach: `-s` leaves the inbound listener up, and a
216+
server that dials *in* while that attempt is outstanding or has failed is a connection the
217+
player cannot tell from its own — the library reports that one is up, not where it came
218+
from. Read `SENDSPIN_SERVER_ID` when a hook has to be certain which server it is acting
219+
on.
217220

218221
The hook never blocks playback: it is spawned and reaped from the main loop, its output
219222
goes to the log, and a non-zero exit is a `W hook:` warning rather than a player failure.

src/hooks.h

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,13 @@ struct HookContext {
3636
std::string server_name; ///< SENDSPIN_SERVER_NAME: its friendly name
3737
/// SENDSPIN_SERVER_URL: the URL this run dialled, on an -s run only.
3838
///
39-
/// What was dialled, not which server answered. -s leaves the inbound listener up, and
40-
/// the library reports that a connection is up without saying where it came from, so one
41-
/// that dialled in while an outbound attempt was outstanding or had failed carries the
42-
/// URL of the attempt. server_id always describes the connection the stream arrived on.
39+
/// What was dialled, not which server answered. A lost connection clears it rather than
40+
/// letting it describe whatever connects next, and a discovery dial is exported only
41+
/// when the stream arrived from the server_id it dialled. A literal -s URL is the case
42+
/// that cannot be checked: -s leaves the inbound listener up, and the library reports
43+
/// that a connection is up without saying where it came from, so a server that dialled
44+
/// in while that attempt was outstanding or had failed still reads as the dial.
45+
/// server_id always describes the connection the stream arrived on.
4346
std::string server_url;
4447
std::string client_id; ///< SENDSPIN_CLIENT_ID: this player's id, when --id chose one
4548
std::string client_name; ///< SENDSPIN_CLIENT_NAME: this player's friendly name

src/main.cpp

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,9 @@ class OutboundMode {
237237
this->pacer_.delay_ms());
238238
// The next connection may be to a different server, so it is owed its own look.
239239
this->remembered_this_connection_ = false;
240+
// Whatever the last dial produced -- or failed to produce -- went with the
241+
// connection, so its URL must not describe whatever connects next.
242+
this->last_dial_.note_lost();
240243
}
241244
// Covers an inbound connection too: a server that dialled us first is a connection,
242245
// and dialling out over the top of it would only fight with it.
@@ -249,8 +252,9 @@ class OutboundMode {
249252
}
250253

251254
std::string url;
255+
std::string server_id;
252256
if (this->opts_.discover) {
253-
if (!this->choose(url)) {
257+
if (!this->choose(url, server_id)) {
254258
return;
255259
}
256260
} else {
@@ -262,25 +266,29 @@ class OutboundMode {
262266
// Stamped before the dial rather than after, so the backoff measures from when the
263267
// attempt started -- which is the whole point of pacing from the dial.
264268
this->pacer_.note_dial(now_ms);
265-
this->dialed_url_ = url;
269+
this->last_dial_.note_dial(url, server_id);
266270
client.connect_to(url);
267271
}
268272

269-
/// The URL this run last dialled, empty before the first dial. What the stream hooks
270-
/// export as SENDSPIN_SERVER_URL.
273+
/// The URL to export as SENDSPIN_SERVER_URL for a stream arriving from `server_id`,
274+
/// empty when no dial of this run's plausibly produced that connection.
271275
///
272-
/// What was dialled, not what answered, and the two can differ: -s leaves the inbound
273-
/// listener up, so a server that dialled in while an attempt of ours was outstanding or
274-
/// had failed is a connection this cannot tell from its own. Telling them apart needs the
275-
/// library to say where the live connection came from, which it does not -- there is no
276-
/// connect callback, and nothing exposes a connection's URL or its direction.
277-
const std::string& dialed_url() const {
278-
return this->dialed_url_;
276+
/// What was dialled, never a claim about what answered: a lost connection forgets the
277+
/// dial, and a discovery dial is answered only for the server_id it dialled. A literal
278+
/// -s URL is the case that cannot be verified -- -s leaves the inbound listener up, and
279+
/// telling an inbound connection from our own needs the library to say where the live
280+
/// connection came from, which it does not: there is no connect callback, and nothing
281+
/// exposes a connection's URL or its direction.
282+
std::string url_for(const std::string& server_id) const {
283+
return this->last_dial_.url_for(server_id);
279284
}
280285

281286
private:
282287
/// Picks a discovered server, or reports that there is nothing to dial yet.
283-
bool choose(std::string& url) {
288+
///
289+
/// `server_id` is the chosen instance label, which is the protocol server_id -- the
290+
/// equality the remembered-server preference already stands on.
291+
bool choose(std::string& url, std::string& server_id) {
284292
const std::vector<DiscoveredServer> servers = this->mdns_.servers();
285293
std::string reason;
286294
const DiscoveredServer* chosen =
@@ -293,6 +301,7 @@ class OutboundMode {
293301
// Discovery already said why, at debug, when the instance first resolved.
294302
return false;
295303
}
304+
server_id = chosen->instance;
296305
// A discovered URL has no userinfo to hide -- discovered_server_url() builds it from a
297306
// resolved address and a TXT path it requires to start with '/', so the authority is
298307
// always just that address. It goes through the helper anyway so that both "Connecting
@@ -342,7 +351,7 @@ class OutboundMode {
342351
MdnsService& mdns_;
343352
StateStore& store_;
344353
std::string remembered_;
345-
std::string dialed_url_;
354+
LastDial last_dial_;
346355
bool remembered_this_connection_{false};
347356
RetryPacer pacer_;
348357
};
@@ -951,7 +960,7 @@ int main(int argc, char* argv[]) {
951960
stream_context.server_name = info->name;
952961
}
953962
if (outbound) {
954-
stream_context.server_url = outbound->dialed_url();
963+
stream_context.server_url = outbound->url_for(stream_context.server_id);
955964
}
956965
}
957966
const std::string& command = started ? opts.hook_start : opts.hook_stop;

src/outbound.cpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,21 @@ void RetryPacer::note_dial(int64_t now_ms) {
7070
}
7171
}
7272

73+
void LastDial::note_dial(const std::string& url, const std::string& server_id) {
74+
this->url_ = url;
75+
this->server_id_ = server_id;
76+
}
77+
78+
void LastDial::note_lost() {
79+
this->url_.clear();
80+
this->server_id_.clear();
81+
}
82+
83+
std::string LastDial::url_for(const std::string& connected_server_id) const {
84+
if (!this->server_id_.empty() && this->server_id_ != connected_server_id) {
85+
return {};
86+
}
87+
return this->url_;
88+
}
89+
7390
} // namespace sendspin_cli

src/outbound.h

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
// limitations under the License.
1414

1515
/// @file outbound.h
16-
/// @brief When the outbound mode is allowed to dial again
16+
/// @brief When the outbound mode is allowed to dial again, and what the last dial may claim
1717

1818
#pragma once
1919

2020
#include <cstdint>
21+
#include <string>
2122

2223
namespace sendspin_cli {
2324

@@ -87,4 +88,34 @@ class RetryPacer {
8788
bool connected_{false};
8889
};
8990

91+
/// @brief The last dial, and what SENDSPIN_SERVER_URL may honestly claim of it.
92+
///
93+
/// The library reports that a connection is up without saying where it came from -- there
94+
/// is no connect callback, and nothing exposes a connection's URL or direction -- so the
95+
/// dialled URL is exported only while nothing contradicts it being the connection's
96+
/// origin. A dial to a discovered server knows which server_id it dialled, because the
97+
/// mDNS instance label *is* the protocol server_id, so its URL is answered only for that
98+
/// server; a dial to a literal -s URL promises nothing about who answers and is taken at
99+
/// its word. Losing the connection forgets the dial either way: whatever its URL described
100+
/// is gone, and no answer beats a stale one.
101+
class LastDial {
102+
public:
103+
/// @brief Records that connect_to() has just been called with `url`.
104+
/// @param server_id The dialled server's id when discovery chose it, or empty for a
105+
/// literal -s URL.
106+
void note_dial(const std::string& url, const std::string& server_id);
107+
108+
/// @brief Forgets the dial: the connection it could have described is gone.
109+
void note_lost();
110+
111+
/// @brief The URL to export for a stream arriving from `connected_server_id`.
112+
/// @return The dialled URL, or empty when no dial is live -- or when the dial named a
113+
/// server other than the connected one, an unknown one included.
114+
std::string url_for(const std::string& connected_server_id) const;
115+
116+
private:
117+
std::string url_;
118+
std::string server_id_; ///< who url_ was expected to reach; empty means unverifiable
119+
};
120+
90121
} // namespace sendspin_cli

tests/discovery_test.cpp

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,5 +354,54 @@ TEST(RetryPacer, TheLostTransitionIsReportedExactlyOnce) {
354354
EXPECT_FALSE(pacer.note_connection_state(true, 300));
355355
}
356356

357+
// ---------------------------------------------------------------------------
358+
// What the last dial may claim
359+
// ---------------------------------------------------------------------------
360+
361+
TEST(LastDial, StartsWithNothingToExport) {
362+
const LastDial dial;
363+
364+
EXPECT_EQ(dial.url_for("srv-1"), "");
365+
}
366+
367+
TEST(LastDial, ALiteralUrlIsTakenAtItsWord) {
368+
// A -s URL promises nothing about who answers, so there is nothing to check the
369+
// connected server against: the URL is exported as dialled.
370+
LastDial dial;
371+
dial.note_dial("ws://hifi:8927/sendspin", "");
372+
373+
EXPECT_EQ(dial.url_for("srv-1"), "ws://hifi:8927/sendspin");
374+
EXPECT_EQ(dial.url_for(""), "ws://hifi:8927/sendspin");
375+
}
376+
377+
TEST(LastDial, ADiscoveryDialAnswersOnlyForTheServerItDialled) {
378+
LastDial dial;
379+
dial.note_dial("ws://192.168.1.10:8927/sendspin", "srv-1");
380+
381+
EXPECT_EQ(dial.url_for("srv-1"), "ws://192.168.1.10:8927/sendspin");
382+
// A different server answered -- it dialled in, or beat the attempt -- and an unknown
383+
// one cannot be checked at all. Either way the URL would describe the wrong connection.
384+
EXPECT_EQ(dial.url_for("srv-2"), "");
385+
EXPECT_EQ(dial.url_for(""), "");
386+
}
387+
388+
TEST(LastDial, ALostConnectionForgetsTheDial) {
389+
LastDial dial;
390+
dial.note_dial("ws://hifi:8927/sendspin", "");
391+
dial.note_lost();
392+
393+
EXPECT_EQ(dial.url_for(""), "");
394+
}
395+
396+
TEST(LastDial, ARedialAfterALossIsExportedAgain) {
397+
LastDial dial;
398+
dial.note_dial("ws://one:8927/sendspin", "srv-1");
399+
dial.note_lost();
400+
dial.note_dial("ws://two:8927/sendspin", "srv-2");
401+
402+
EXPECT_EQ(dial.url_for("srv-2"), "ws://two:8927/sendspin");
403+
EXPECT_EQ(dial.url_for("srv-1"), "");
404+
}
405+
357406
} // namespace
358407
} // namespace sendspin_cli

0 commit comments

Comments
 (0)