Skip to content

Commit 633b0dd

Browse files
committed
launcher: wait for network-online on resume before nudging (Phase 6)
Adds netprobe.{hpp,cpp}: dependency-free TCP reachability with backoff (tcp_reachable / wait_online). PlayerController::on_resume now waits until the configured host:port is reachable (default www.youtube.com:443, 8s budget) before the play nudge, so we don't drive the player into an error state before the network is back after wake. Bounded and skippable (timeout 0). Config: resume_probe_host / resume_probe_port / resume_online_timeout_ms in app.json. player_test gains netprobe coverage (reachable, dead-port timeout is bounded, timeout-0 skip) and a resume-with-live-probe case. Green under gcc and ASan+UBSan.
1 parent 4329eb1 commit 633b0dd

10 files changed

Lines changed: 157 additions & 7 deletions

File tree

config/app.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@
2121
"power": {
2222
"$comment": "Phase 6 (doc §2 P6). Play-state is polled over DevTools (needs remote_debugging_port) to hold the logind idle inhibitor while a video plays. Synthetic fallback is off by default.",
2323
"devtools_poll_ms": 1000,
24-
"idle_inhibit_synthetic_fallback": false
24+
"idle_inhibit_synthetic_fallback": false,
25+
"resume_probe_host": "www.youtube.com",
26+
"resume_probe_port": 443,
27+
"resume_online_timeout_ms": 8000
2528
},
2629
"log": {
2730
"$comment": "Size-rotating file sink owned by launcher/ (doc §2 P2). Empty log_directory derives from DECKBACK_LOG_DIR / XDG_STATE_HOME at runtime. Lines also mirror to stderr for journald/Steam.",

launcher/CMakeLists.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ add_executable(deckback-launcher
2626
src/cdm_fetcher.cpp
2727
src/devtools.cpp
2828
src/player.cpp
29+
src/netprobe.cpp
2930
)
3031
target_include_directories(deckback-launcher PRIVATE src)
3132

@@ -61,7 +62,8 @@ if(BUILD_TESTING)
6162
target_link_libraries(devtools_test PRIVATE Threads::Threads)
6263
add_test(NAME devtools_test COMMAND devtools_test)
6364

64-
add_executable(player_test tests/player_test.cpp src/player.cpp src/devtools.cpp src/log.cpp)
65+
add_executable(player_test tests/player_test.cpp src/player.cpp src/devtools.cpp
66+
src/netprobe.cpp src/log.cpp)
6567
target_include_directories(player_test PRIVATE src tests)
6668
target_link_libraries(player_test PRIVATE Threads::Threads)
6769
add_test(NAME player_test COMMAND player_test)

launcher/src/config.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ std::optional<Config> Config::load(const std::string& path) {
114114
if (auto v = read_int(s, "devtools_poll_ms")) c.devtools_poll_ms = static_cast<int>(*v);
115115
if (auto v = read_bool(s, "idle_inhibit_synthetic_fallback"))
116116
c.idle_inhibit_synthetic_fallback = *v;
117+
if (auto v = read_string(s, "resume_probe_host")) c.resume_probe_host = *v;
118+
if (auto v = read_int(s, "resume_probe_port")) c.resume_probe_port = static_cast<int>(*v);
119+
if (auto v = read_int(s, "resume_online_timeout_ms"))
120+
c.resume_online_timeout_ms = static_cast<int>(*v);
117121
return c;
118122
}
119123

launcher/src/config.hpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ struct Config {
2525
// to fall back to synthetic activity when no logind idle inhibitor is available.
2626
int devtools_poll_ms = 1000;
2727
bool idle_inhibit_synthetic_fallback = false;
28+
// On resume, wait until this host:port is reachable before nudging the player (0 ms = skip).
29+
std::string resume_probe_host = "www.youtube.com";
30+
int resume_probe_port = 443;
31+
int resume_online_timeout_ms = 8000;
2832

2933
// Loads and parses the JSON at `path`. Returns nullopt on read/parse failure.
3034
// NOTE: this is a minimal top-level-key extractor sufficient for app.json's shape; replace with a

launcher/src/main.cpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,8 +130,10 @@ int main(int argc, char** argv) {
130130
// Cobalt's remote-debugging port; without it we fall back to logging-only hooks.
131131
std::optional<PlayerController> player;
132132
if (cfg->remote_debugging_port > 0) {
133-
player.emplace(*platform, "127.0.0.1", cfg->remote_debugging_port, cfg->devtools_poll_ms,
134-
cfg->idle_inhibit_synthetic_fallback);
133+
player.emplace(
134+
*platform, "127.0.0.1", cfg->remote_debugging_port, cfg->devtools_poll_ms,
135+
cfg->idle_inhibit_synthetic_fallback,
136+
ResumeProbe{cfg->resume_probe_host, cfg->resume_probe_port, cfg->resume_online_timeout_ms});
135137
platform->on_suspend([&] { player->on_suspend(); });
136138
platform->on_resume([&] { player->on_resume(); });
137139
player->start();

launcher/src/netprobe.cpp

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#include "netprobe.hpp"
2+
3+
#include <fcntl.h>
4+
#include <netdb.h>
5+
#include <poll.h>
6+
#include <sys/socket.h>
7+
#include <time.h>
8+
#include <unistd.h>
9+
10+
#include <cerrno>
11+
12+
namespace deckback {
13+
namespace {
14+
15+
long mono_ms() {
16+
timespec ts{};
17+
clock_gettime(CLOCK_MONOTONIC, &ts);
18+
return ts.tv_sec * 1000L + ts.tv_nsec / 1'000'000L;
19+
}
20+
21+
} // namespace
22+
23+
bool tcp_reachable(const std::string& host, int port, int timeout_ms) {
24+
addrinfo hints{};
25+
hints.ai_family = AF_UNSPEC;
26+
hints.ai_socktype = SOCK_STREAM;
27+
addrinfo* res = nullptr;
28+
const std::string port_s = std::to_string(port);
29+
if (getaddrinfo(host.c_str(), port_s.c_str(), &hints, &res) != 0 || !res) return false;
30+
31+
bool ok = false;
32+
for (addrinfo* ai = res; ai && !ok; ai = ai->ai_next) {
33+
int fd = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol);
34+
if (fd < 0) continue;
35+
int flags = fcntl(fd, F_GETFL, 0);
36+
fcntl(fd, F_SETFL, flags | O_NONBLOCK);
37+
int rc = connect(fd, ai->ai_addr, ai->ai_addrlen);
38+
if (rc == 0) {
39+
ok = true;
40+
} else if (errno == EINPROGRESS) {
41+
pollfd pfd{fd, POLLOUT, 0};
42+
if (poll(&pfd, 1, timeout_ms) > 0 && (pfd.revents & POLLOUT)) {
43+
int err = 0;
44+
socklen_t l = sizeof err;
45+
getsockopt(fd, SOL_SOCKET, SO_ERROR, &err, &l);
46+
ok = (err == 0);
47+
}
48+
}
49+
close(fd);
50+
}
51+
freeaddrinfo(res);
52+
return ok;
53+
}
54+
55+
bool wait_online(const std::string& host, int port, int max_ms) {
56+
if (max_ms <= 0) return true;
57+
const long deadline = mono_ms() + max_ms;
58+
int backoff = 200;
59+
for (;;) {
60+
int remaining = static_cast<int>(deadline - mono_ms());
61+
if (remaining <= 0) return false;
62+
if (tcp_reachable(host, port, remaining < 1000 ? remaining : 1000)) return true;
63+
int nap = backoff < remaining ? backoff : remaining;
64+
if (nap > 0) {
65+
timespec ts{nap / 1000, (nap % 1000) * 1'000'000L};
66+
nanosleep(&ts, nullptr);
67+
}
68+
backoff = backoff < 1600 ? backoff * 2 : 1600;
69+
}
70+
}
71+
72+
} // namespace deckback

launcher/src/netprobe.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
#pragma once
2+
#include <string>
3+
4+
namespace deckback {
5+
6+
// Dependency-free network-reachability probes for the Phase 6 resume path (doc §2 P6): after wake
7+
// we want the network back before nudging the player, so we don't drive it into an error state.
8+
9+
// One non-blocking TCP connect attempt to host:port, bounded by timeout_ms. True if it connects.
10+
bool tcp_reachable(const std::string& host, int port, int timeout_ms);
11+
12+
// Poll tcp_reachable with backoff until it succeeds or max_ms elapses. max_ms <= 0 disables the
13+
// wait (returns true immediately). Returns whether the host became reachable within the budget.
14+
bool wait_online(const std::string& host, int port, int max_ms);
15+
16+
} // namespace deckback

launcher/src/player.cpp

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <format>
55

66
#include "log.hpp"
7+
#include "netprobe.hpp"
78
#include "platform.hpp"
89

910
namespace deckback {
@@ -27,11 +28,12 @@ constexpr const char* kPlayExpr =
2728
} // namespace
2829

2930
PlayerController::PlayerController(Platform& platform, std::string host, int port, int poll_ms,
30-
bool synthetic_fallback)
31+
bool synthetic_fallback, ResumeProbe resume_probe)
3132
: platform_(platform),
3233
client_(std::move(host), port),
3334
poll_ms_(poll_ms < 100 ? 100 : poll_ms),
34-
synthetic_fallback_(synthetic_fallback) {}
35+
synthetic_fallback_(synthetic_fallback),
36+
resume_probe_(std::move(resume_probe)) {}
3537

3638
PlayerController::~PlayerController() { stop(); }
3739

@@ -91,6 +93,14 @@ std::optional<double> PlayerController::on_suspend() {
9193
}
9294

9395
bool PlayerController::on_resume() {
96+
if (resume_probe_.timeout_ms > 0) {
97+
if (wait_online(resume_probe_.host, resume_probe_.port, resume_probe_.timeout_ms))
98+
info(std::format("player: network reachable ({}:{}) — nudging playback", resume_probe_.host,
99+
resume_probe_.port));
100+
else
101+
warn(std::format("player: network still down after {} ms — nudging anyway",
102+
resume_probe_.timeout_ms));
103+
}
94104
const bool ok = client_.eval_void(kPlayExpr);
95105
info(ok ? "player: resume nudge sent" : "player: resume nudge failed (engine unreachable?)");
96106
return ok;

launcher/src/player.hpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ namespace deckback {
1212

1313
class Platform;
1414

15+
// On resume, wait until host:port is reachable before nudging the player (timeout_ms 0 = skip).
16+
struct ResumeProbe {
17+
std::string host;
18+
int port = 443;
19+
int timeout_ms = 0;
20+
};
21+
1522
// Drives the Phase 6 power contract through the DevTools bridge (doc §2 P6):
1623
// * a background thread polls the player's play-state and holds/releases the logind idle
1724
// inhibitor via Platform so the screen never dims mid-video;
@@ -23,7 +30,7 @@ class Platform;
2330
class PlayerController {
2431
public:
2532
PlayerController(Platform& platform, std::string host, int port, int poll_ms,
26-
bool synthetic_fallback);
33+
bool synthetic_fallback, ResumeProbe resume_probe = ResumeProbe{});
2734
~PlayerController();
2835

2936
PlayerController(const PlayerController&) = delete;
@@ -48,6 +55,7 @@ class PlayerController {
4855
DevToolsClient client_;
4956
int poll_ms_;
5057
bool synthetic_fallback_;
58+
ResumeProbe resume_probe_;
5159
bool warned_synthetic_ = false;
5260

5361
std::thread thread_;

launcher/tests/player_test.cpp

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <thread>
88

99
#include "fake_cdp_server.hpp"
10+
#include "netprobe.hpp"
1011
#include "platform.hpp"
1112

1213
using namespace deckback;
@@ -87,13 +88,41 @@ void test_thread_lifecycle() {
8788
pc.stop(); // idempotent
8889
}
8990

91+
// The network probe: a listening port is reachable; a dead port times out within its budget.
92+
void test_netprobe() {
93+
testing::FakeServer server; // a live loopback listener
94+
assert(tcp_reachable("127.0.0.1", server.port(), 500));
95+
assert(wait_online("127.0.0.1", server.port(), 2000));
96+
97+
// Dead port: wait_online must return false and respect its budget (not hang).
98+
auto t0 = std::chrono::steady_clock::now();
99+
assert(!wait_online("127.0.0.1", 8, 600));
100+
auto elapsed =
101+
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - t0)
102+
.count();
103+
assert(elapsed < 3000); // bounded by the 600ms budget (+ backoff slack), nowhere near hanging
104+
105+
assert(wait_online("127.0.0.1", 8, 0)); // timeout 0 disables the wait -> immediate true
106+
}
107+
108+
// Resume with a network probe pointed at a live port still nudges playback.
109+
void test_resume_with_probe() {
110+
testing::FakeServer server;
111+
FakePlatform plat;
112+
PlayerController pc(plat, "127.0.0.1", server.port(), 1000, false,
113+
ResumeProbe{"127.0.0.1", server.port(), 2000});
114+
assert(pc.on_resume() == true);
115+
}
116+
90117
} // namespace
91118

92119
int main() {
93120
test_poll_tracks_playstate();
94121
test_suspend_resume_hooks();
95122
test_engine_unreachable();
96123
test_thread_lifecycle();
124+
test_netprobe();
125+
test_resume_with_probe();
97126
std::puts("player_test: ok");
98127
return 0;
99128
}

0 commit comments

Comments
 (0)