OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
qml_unix_outbound_socket.hpp
1#pragma once
2#include <ossia/network/context.hpp>
3#if defined(BOOST_ASIO_HAS_LOCAL_SOCKETS)
4#include <ossia/detail/variant.hpp>
5#include <ossia/network/sockets/configuration.hpp>
6#include <ossia/network/sockets/cobs_framing.hpp>
7#include <ossia/network/sockets/fixed_length_framing.hpp>
8#include <ossia/network/sockets/line_framing.hpp>
9#include <ossia/network/sockets/no_framing.hpp>
10#include <ossia/network/sockets/size_prefix_framing.hpp>
11#include <ossia/network/sockets/slip_framing.hpp>
12#include <ossia/network/sockets/stx_etx_framing.hpp>
13#include <ossia/network/sockets/unix_socket.hpp>
14#include <ossia/network/sockets/var_size_prefix_framing.hpp>
15
16#include <ossia-qt/protocols/utils.hpp>
17
18#include <QJSValue>
19#include <QObject>
20#include <QQmlEngine>
21
22#include <nano_observer.hpp>
23
24#include <mutex>
25
26#include <verdigris>
27
28namespace ossia::qt
29{
30class qml_unix_datagram_outbound_socket
31 : public QObject
32 , public Nano::Observer
33 , public protocols_sender
34{
35 W_OBJECT(qml_unix_datagram_outbound_socket)
36public:
37 struct state
38 {
39 ossia::net::unix_datagram_socket socket;
40 std::atomic_bool alive{true};
41 ossia::net::encoding enc{ossia::net::encoding::none};
42
43 state(
44 const ossia::net::fd_configuration& conf, boost::asio::io_context& ctx,
45 ossia::net::encoding e = ossia::net::encoding::none)
46 : socket{conf, ctx}
47 {
48 enc = e;
49 }
50 };
51
52 ossia::net::unix_datagram_socket* socket = nullptr;
53
54 qml_unix_datagram_outbound_socket() { }
55
56 ~qml_unix_datagram_outbound_socket()
57 {
58 if(m_state)
59 {
60 m_state->alive = false;
61 close();
62 }
63 }
64
65 bool isOpen() const noexcept { return m_state != nullptr; }
66
67 void open(
68 const ossia::net::fd_configuration& conf, boost::asio::io_context& ctx,
69 ossia::net::encoding e = ossia::net::encoding::none)
70 {
71 m_state = std::make_shared<state>(conf, ctx, e);
72 socket = &m_state->socket;
73
74 if(onClose.isCallable())
75 m_state->socket.on_close.connect<&qml_unix_datagram_outbound_socket::on_close>(this);
76
77 m_state->socket.connect();
78
79 if(onOpen.isCallable())
80 onOpen.call({qjsEngine(this)->newQObject(this)});
81 }
82
83 void close()
84 {
85 if(!m_state)
86 return;
87 if(!m_state->socket.m_socket.is_open())
88 return;
89 auto st = m_state;
90 boost::asio::post(st->socket.m_context, [st] {
91 try
92 {
93 st->socket.m_socket.shutdown(boost::asio::ip::udp::socket::shutdown_both);
94 }
95 catch(...)
96 {
97 }
98 st->socket.m_socket.close();
99 st->socket.on_close();
100 });
101 }
102 W_SLOT(close)
103
104 void on_close()
105 {
106 if(!m_state || !m_state->alive)
107 return;
108 ossia::qt::run_async(this, [=, this] { onClose.call(); }, Qt::AutoConnection);
109 }
110
111 void write(QByteArray buffer)
112 {
113 if(!m_state)
114 return;
115 auto st = m_state;
116 if(st->enc != ossia::net::encoding::none)
117 buffer = apply_encoding(st->enc, buffer);
118 boost::asio::dispatch(st->socket.m_context, [st, buffer = std::move(buffer)] {
119 if(st->alive)
120 st->socket.write(buffer.data(), buffer.size());
121 });
122 }
123 W_SLOT(write)
124
125 void osc(QByteArray address, QJSValueList values)
126 {
127 if(socket)
128 this->send_osc(address, values);
129 }
130 W_SLOT(osc)
131
132 QJSValue onOpen;
133 QJSValue onClose;
134 QJSValue onError;
135
136private:
137 std::shared_ptr<state> m_state;
138};
139
140class qml_unix_stream_outbound_socket : public QObject
141{
142 W_OBJECT(qml_unix_stream_outbound_socket)
143public:
144 using socket_t = boost::asio::local::stream_protocol::socket;
145 using decoder_type = ossia::slow_variant<
146 ossia::net::no_framing::decoder<socket_t>,
147 ossia::net::slip_decoder<socket_t>,
148 ossia::net::size_prefix_decoder<socket_t>,
149 ossia::net::line_framing_decoder<socket_t>,
150 ossia::net::cobs_decoder<socket_t>,
151 ossia::net::stx_etx_framing::decoder<socket_t>,
152 ossia::net::size_prefix_1byte_framing::decoder<socket_t>,
153 ossia::net::size_prefix_2byte_be_framing::decoder<socket_t>,
154 ossia::net::size_prefix_2byte_le_framing::decoder<socket_t>,
155 ossia::net::size_prefix_4byte_le_framing::decoder<socket_t>,
156 ossia::net::fixed_length_decoder<socket_t>>;
157
164 struct state : Nano::Observer
165 {
166 ossia::net::unix_stream_client socket;
167 std::atomic_bool alive{true};
168 // on_close is emitted at most once, be it on EOF or on an explicit close;
169 // both emitters run on the io_context thread.
170 std::atomic_bool closed{false};
171 ossia::net::framing framing{ossia::net::framing::none};
172 ossia::net::encoding enc{ossia::net::encoding::none};
173 char line_delimiter[8] = {};
174 decoder_type decoder;
175
176 state(
177 const ossia::net::fd_configuration& conf, boost::asio::io_context& ctx,
178 ossia::net::framing f = ossia::net::framing::none,
179 const std::string& delim = {},
180 ossia::net::encoding e = ossia::net::encoding::none)
181 : socket{conf, ctx}
182 , decoder{ossia::in_place_index<0>, socket.m_socket}
183 {
184 framing = f;
185 enc = e;
186 if(!delim.empty())
187 {
188 auto sz = std::min(delim.size(), (size_t)7);
189 std::copy_n(delim.begin(), sz, line_delimiter);
190 }
191 switch(f)
192 {
193 default:
194 case ossia::net::framing::none:
195 break;
196 case ossia::net::framing::slip:
197 decoder.template emplace<1>(socket.m_socket);
198 break;
199 case ossia::net::framing::size_prefix:
200 decoder.template emplace<2>(socket.m_socket);
201 break;
202 case ossia::net::framing::line_delimiter:
203 decoder.template emplace<3>(socket.m_socket);
204 {
205 auto& dec = ossia::get<3>(decoder);
206 std::copy_n(line_delimiter, 8, dec.delimiter);
207 }
208 break;
209 case ossia::net::framing::cobs:
210 decoder.template emplace<4>(socket.m_socket);
211 break;
212 case ossia::net::framing::stx_etx:
213 decoder.template emplace<5>(socket.m_socket);
214 break;
215 case ossia::net::framing::size_prefix_1byte:
216 decoder.template emplace<6>(socket.m_socket);
217 break;
218 case ossia::net::framing::size_prefix_2byte_be:
219 decoder.template emplace<7>(socket.m_socket);
220 break;
221 case ossia::net::framing::size_prefix_2byte_le:
222 decoder.template emplace<8>(socket.m_socket);
223 break;
224 case ossia::net::framing::size_prefix_4byte_le:
225 decoder.template emplace<9>(socket.m_socket);
226 break;
227 case ossia::net::framing::fixed_length:
228 decoder.template emplace<10>(socket.m_socket);
229 if(!delim.empty())
230 ossia::get<10>(decoder).frame_size = std::stoul(delim);
231 break;
232 }
233 }
234
235 void write_encoded(const char* data, std::size_t sz)
236 {
237 switch(framing)
238 {
239 default:
240 case ossia::net::framing::none:
241 socket.write(data, sz);
242 break;
243 case ossia::net::framing::slip:
244 ossia::net::slip_encoder<socket_t>{socket.m_socket}.write(data, sz);
245 break;
246 case ossia::net::framing::size_prefix:
247 ossia::net::size_prefix_encoder<socket_t>{socket.m_socket}.write(data, sz);
248 break;
249 case ossia::net::framing::line_delimiter: {
250 ossia::net::line_framing_encoder<socket_t> enc{socket.m_socket};
251 std::copy_n(line_delimiter, 8, enc.delimiter);
252 enc.write(data, sz);
253 break;
254 }
255 case ossia::net::framing::cobs:
256 ossia::net::cobs_encoder<socket_t>{socket.m_socket}.write(data, sz);
257 break;
258 case ossia::net::framing::stx_etx:
259 ossia::net::stx_etx_framing::encoder<socket_t>{socket.m_socket}.write(data, sz);
260 break;
261 case ossia::net::framing::size_prefix_1byte:
262 ossia::net::size_prefix_1byte_framing::encoder<socket_t>{socket.m_socket}.write(
263 data, sz);
264 break;
265 case ossia::net::framing::size_prefix_2byte_be:
266 ossia::net::size_prefix_2byte_be_framing::encoder<socket_t>{socket.m_socket}
267 .write(data, sz);
268 break;
269 case ossia::net::framing::size_prefix_2byte_le:
270 ossia::net::size_prefix_2byte_le_framing::encoder<socket_t>{socket.m_socket}
271 .write(data, sz);
272 break;
273 case ossia::net::framing::size_prefix_4byte_le:
274 ossia::net::size_prefix_4byte_le_framing::encoder<socket_t>{socket.m_socket}
275 .write(data, sz);
276 break;
277 case ossia::net::framing::fixed_length:
278 ossia::net::fixed_length_encoder<socket_t>{socket.m_socket}.write(data, sz);
279 break;
280 }
281 }
282
285 std::mutex qt_mutex;
286 qml_unix_stream_outbound_socket* self{};
287
288 void detach()
289 {
290 std::lock_guard g{qt_mutex};
291 self = nullptr;
292 }
293
294 template <typename F>
295 void post_to_qt(F&& f)
296 {
297 std::lock_guard g{qt_mutex};
298 if(self)
299 ossia::qt::run_async(self, std::forward<F>(f), Qt::AutoConnection);
300 }
301
303 void fire_open();
304 void fire_fail();
305 void fire_close();
306
308 void notify_closed()
309 {
310 if(alive && !closed.exchange(true))
311 socket.on_close();
312 }
313 };
314
315 struct receive_callback
316 {
317 std::shared_ptr<state> st;
318 QPointer<qml_unix_stream_outbound_socket> self;
319 // Points to onMessage or onBytes on the QObject; null when no callback is
320 // set, the read loop then only observing the remote closing. Dereferenced
321 // on the Qt thread only: a QJSValue must not be touched from asio.
322 QJSValue* target;
323
324 void operator()(const unsigned char* data, std::size_t sz) const
325 {
326 if(!st->alive || !target)
327 return;
328 auto buf = apply_decoding(st->enc, data, sz);
329 auto cb = target;
330 st->post_to_qt([self = self, buf, cb] {
331 if(!self.get())
332 return;
333 if(cb->isCallable())
334 {
335 auto engine = qjsEngine(self.get());
336 if(engine)
337 cb->call({engine->toScriptValue(buf)});
338 }
339 });
340 }
341
342 bool validate_stream(boost::system::error_code ec) const
343 {
344 if(!ec)
345 return true;
346 if(ec == boost::asio::error::operation_aborted)
347 return false;
348
349 // Any other error ends this stream -- eof on a graceful close,
350 // connection_reset on an RST -- and stops the read loop, so this is the
351 // only place where the close can be reported.
352 st->notify_closed();
353 return false;
354 }
355 };
356
357 qml_unix_stream_outbound_socket() { }
358
359 ~qml_unix_stream_outbound_socket()
360 {
361 if(m_state)
362 {
363 m_state->alive = false;
364 // No asio -> Qt call may start from here on: close() below still posts a
365 // shutdown, but its close notification is suppressed by `alive`.
366 m_state->detach();
367 close();
368 }
369 }
370
371 bool isOpen() const noexcept { return m_state != nullptr; }
372
373 void open(
374 const ossia::net::fd_configuration& conf, boost::asio::io_context& ctx,
375 ossia::net::framing f = ossia::net::framing::none,
376 const std::string& delim = {},
377 ossia::net::encoding e = ossia::net::encoding::none)
378 {
379 m_state = std::make_shared<state>(conf, ctx, f, delim, e);
380 m_state->self = this;
381
382 try
383 {
384 if(onOpen.isCallable())
385 m_state->socket.on_open.connect<&state::fire_open>(m_state.get());
386 if(onClose.isCallable())
387 m_state->socket.on_close.connect<&state::fire_close>(m_state.get());
388 if(onError.isCallable())
389 m_state->socket.on_fail.connect<&state::fire_fail>(m_state.get());
390 m_state->socket.connect();
391 }
392 catch(const std::exception& e)
393 {
394 if(onError.isCallable())
395 {
396 onError.call({QString::fromStdString(e.what())});
397 }
398 }
399 }
400
401 void write(QByteArray buffer)
402 {
403 if(!m_state)
404 return;
405 auto st = m_state;
406 if(st->enc != ossia::net::encoding::none)
407 buffer = apply_encoding(st->enc, buffer);
408 boost::asio::dispatch(st->socket.m_context, [st, buffer = std::move(buffer)] {
409 if(st->alive)
410 st->write_encoded(buffer.data(), buffer.size());
411 });
412 }
413 W_SLOT(write)
414
415 void close()
416 {
417 if(!m_state)
418 return;
419 auto st = m_state;
420 boost::asio::post(st->socket.m_context, [st] {
421 try
422 {
423 st->socket.m_socket.shutdown(boost::asio::ip::udp::socket::shutdown_both);
424 }
425 catch(...)
426 {
427 }
428 st->socket.m_socket.close();
429 // The destructor comes through here too: notify_closed() checks `alive`,
430 // so it cannot schedule a script call into an object that is gone.
431 st->notify_closed();
432 });
433 }
434 W_SLOT(close)
435
436 void on_open()
437 {
438 if(!m_state || !m_state->alive)
439 return;
440
441 auto st = m_state;
442 auto self = QPointer{this};
443 // The read loop is always armed: it is what observes the remote closing.
444 // With no callback the cheapest decoder does, as nothing is dispatched.
445 if(onMessage.isCallable())
446 {
447 ossia::visit(
448 [cb = receive_callback{st, self, &self.data()->onMessage}](
449 auto& decoder) mutable { decoder.receive(std::move(cb)); },
450 st->decoder);
451 }
452 else
453 {
454 QJSValue* target = onBytes.isCallable() ? &self.data()->onBytes : nullptr;
455 st->decoder.template emplace<0>(st->socket.m_socket);
456 ossia::get<0>(st->decoder).receive(receive_callback{st, self, target});
457 }
458
459 ossia::qt::run_async(
460 this, [=, this] { onOpen.call({qjsEngine(this)->newQObject(this)}); },
461 Qt::AutoConnection);
462 }
463 void on_fail()
464 {
465 if(!m_state || !m_state->alive)
466 return;
467 ossia::qt::run_async(this, [=, this] { onError.call(); }, Qt::AutoConnection);
468 }
469 void on_close()
470 {
471 if(!m_state || !m_state->alive)
472 return;
473 ossia::qt::run_async(this, [=, this] { onClose.call(); }, Qt::AutoConnection);
474 }
475
476 void osc(QByteArray address, QJSValueList values)
477 {
478 if(!m_state)
479 return;
480 QByteArray packet;
481 buffer_writer bw{packet};
482 using send_visitor = ossia::net::osc_value_send_visitor<
483 ossia::net::full_parameter_data, ossia::net::osc_1_0_policy, buffer_writer>;
484
486 const std::string addr = address.toStdString();
487
488 switch(values.size())
489 {
490 case 0: {
491 ossia::value{ossia::impulse{}}.apply(send_visitor{p, addr, bw});
492 break;
493 }
494 case 1: {
495 auto v = ossia::qt::value_from_js(values[0]);
496 v.apply(send_visitor{p, addr, bw});
497 break;
498 }
499 default: {
500 std::vector<ossia::value> vec;
501 vec.reserve(values.size());
502 for(const auto& v : values)
503 vec.push_back(ossia::qt::value_from_js(v));
504 ossia::value vvec(std::move(vec));
505 vvec.apply(send_visitor{p, addr, bw});
506 }
507 }
508 write(packet);
509 }
510 W_SLOT(osc)
511
512 QJSValue onOpen;
513 QJSValue onClose;
514 QJSValue onError;
515 QJSValue onMessage;
516 QJSValue onBytes; // raw bytes, ignores Framing (backward compatible)
517
518private:
519 std::shared_ptr<state> m_state;
520};
521
522inline void qml_unix_stream_outbound_socket::state::fire_open()
523{
524 std::lock_guard g{qt_mutex};
525 if(self)
526 self->on_open();
527}
528
529inline void qml_unix_stream_outbound_socket::state::fire_fail()
530{
531 std::lock_guard g{qt_mutex};
532 if(self)
533 self->on_fail();
534}
535
536inline void qml_unix_stream_outbound_socket::state::fire_close()
537{
538 std::lock_guard g{qt_mutex};
539 if(self)
540 self->on_close();
541}
542
543}
544#endif
The value class.
Definition value.hpp:173
Definition qml_device.cpp:43
Definition git_info.h:7
Full information about a parameter.
Definition parameter_data.hpp:61