Loading...
Searching...
No Matches
PuppetClient.hpp
1#pragma once
2
3// Shared WebSocket client + main-loop boilerplate for the plug-in scanner
4// puppets. Header-only; must only be included from the puppet executables
5// (pulls in asio / ossia websockets).
6//
7// Behaviour (aligned on the battle-tested lv2puppet implementation):
8// * connect to ws://127.0.0.1:<port>, send the payload once both the
9// socket and the scan are ready;
10// * wait 500ms before exiting so the (async) send actually flushes,
11// then _Exit: websocketpp/asio destructors can throw from internals
12// when the server closes the connection first, which would turn a
13// perfectly successful scan into a non-zero exit;
14// * a watchdog kills the process if the socket never becomes ready.
15
16#include <score/tools/PuppetJson.hpp>
17
18#include <ossia/detail/fmt.hpp>
19#include <ossia/network/sockets/websocket.hpp>
20
21#include <chrono>
22#include <cstdio>
23#include <cstdlib>
24#include <memory>
25#include <string>
26
27namespace score::puppet
28{
29struct client
30{
31 boost::asio::io_context ctx;
32 ossia::net::websocket_simple_client socket;
33
34 bool socket_ready{}, payload_ready{};
35 std::string payload;
36 std::string log_name;
37 bool echo_stdout{};
38
39 explicit client(int port)
40 : socket{{.url = fmt::format("ws://127.0.0.1:{}", port)}, ctx}
41 {
42 socket.on_open.connect<&client::on_open>(*this);
43 socket.on_fail.connect<&client::on_error>(*this);
44 socket.on_close.connect<&client::on_error>(*this);
45
46 socket.websocket_client::connect(fmt::format("ws://127.0.0.1:{}", port));
47 }
48
49 void set_payload(std::string json)
50 {
51 payload = std::move(json);
52 if(echo_stdout && !payload.empty())
53 {
54 std::fwrite(payload.data(), 1, payload.size(), stdout);
55 std::fwrite("\n", 1, 1, stdout);
56 std::fflush(stdout);
57 }
58 payload_ready = true;
59 on_ready();
60 }
61
62 void on_ready()
63 {
64 if(socket_ready && payload_ready)
65 {
66 try
67 {
68 socket.send_message(payload);
69 }
70 catch(...)
71 {
72 on_error();
73 return;
74 }
75
76 // Flush delay: send_message is async; large payloads (~MB) need time
77 auto delay = std::make_shared<boost::asio::steady_timer>(ctx);
78 delay->expires_after(std::chrono::milliseconds(500));
79 delay->async_wait(
80 [this, delay](auto) { std::_Exit(payload.empty() ? 1 : 0); });
81 }
82 }
83
84 void on_error()
85 {
86 auto line = fmt::format("[{}] socket error\n", log_name);
87 std::fwrite(line.data(), 1, line.size(), stderr);
88 std::fflush(stderr);
89 std::_Exit(1);
90 }
91
92 void on_open()
93 {
94 socket_ready = true;
95 on_ready();
96 }
97};
98
113template <typename ScanFn>
114int puppet_main(
115 int argc, char** argv, int default_port, const char* name, bool echo_stdout,
116 int watchdog_seconds, ScanFn&& scan_fn)
117{
118 const auto args = parse_arguments(argc, argv, default_port);
119 if(!args.valid)
120 return 1;
121
122 client c{args.port};
123 c.log_name = name;
124 c.echo_stdout = echo_stdout && args.token.empty();
125
126 boost::asio::post(c.ctx, [&] {
127 c.set_payload(scan_fn(args.path, args.request_id, args.token));
128 });
129
130 boost::asio::steady_timer watchdog{c.ctx};
131 watchdog.expires_after(std::chrono::seconds(watchdog_seconds));
132 watchdog.async_wait([&](auto ec) {
133 if(ec)
134 return;
135 // The send is already in flight (a scan longer than the watchdog leaves
136 // both the expired timer and the socket ready; asio dispatches the
137 // socket first): let the flush timer finish the exit instead
138 if(c.socket_ready && c.payload_ready)
139 return;
140 std::fprintf(stderr, "[%s] timeout\n", name);
141 std::_Exit(1);
142 });
143
144 c.ctx.run();
145 c.ctx.restart();
146 c.ctx.run();
147 return c.payload.empty() ? 1 : 0;
148}
149}
Definition PuppetClient.hpp:30