OSSIA
Open Scenario System for Interactive Application
Loading...
Searching...
No Matches
qml_ws_inbound_socket.hpp
1#pragma once
2#include <ossia/network/context.hpp>
3#include <ossia/network/sockets/configuration.hpp>
4#include <ossia/network/sockets/websocket_server.hpp>
5
6#include <ossia-qt/protocols/utils.hpp>
7
8#include <QJSValue>
9#include <QObject>
10#include <QQmlEngine>
11
12#include <nano_observer.hpp>
13
14#include <verdigris>
15
16#include <vector>
17
18namespace ossia::qt
19{
25{
26 boost::asio::io_context& context;
27 std::shared_ptr<ossia::net::websocket_server> server;
28 std::atomic_bool alive{true};
29
30 explicit websocket_server_state(const ossia::net::network_context_ptr& ctx)
31 : context{ctx->context}
32 , server{std::make_shared<ossia::net::websocket_server>(ctx)}
33 {
34 }
35};
36
37class qml_websocket_connection
38 : public QObject
39 , public Nano::Observer
40{
41 W_OBJECT(qml_websocket_connection)
42public:
43 using handler = ossia::net::websocket_server::connection_handler;
44
45 qml_websocket_connection(std::shared_ptr<websocket_server_state> st, handler hdl)
46 : m_state{std::move(st)}
47 , m_hdl{std::move(hdl)}
48 {
49 }
50
51 bool isOpen() const noexcept { return m_state && !m_hdl.expired(); }
52
53 const handler& handle() const noexcept { return m_hdl; }
54
55 void write(QString message)
56 {
57 if(!m_state)
58 return;
59 auto st = m_state;
60 boost::asio::dispatch(st->context, [st, hdl = m_hdl, msg = message.toStdString()] {
61 if(!st->alive)
62 return;
63 try
64 {
65 st->server->send_message(hdl, msg);
66 }
67 catch(...)
68 {
69 }
70 });
71 }
72 W_SLOT(write)
73
74 void writeBinary(QByteArray buffer)
75 {
76 if(!m_state)
77 return;
78 auto st = m_state;
79 boost::asio::dispatch(
80 st->context,
81 [st, hdl = m_hdl, msg = std::string(buffer.data(), buffer.size())] {
82 if(!st->alive)
83 return;
84 try
85 {
86 st->server->send_binary_message(hdl, msg);
87 }
88 catch(...)
89 {
90 }
91 });
92 }
93 W_SLOT(writeBinary)
94
95 void close()
96 {
97 if(!m_state)
98 return;
99 auto st = m_state;
100 boost::asio::dispatch(st->context, [st, hdl = m_hdl] {
101 if(!st->alive)
102 return;
103 try
104 {
105 st->server->close(hdl);
106 }
107 catch(...)
108 {
109 }
110 });
111 }
112 W_SLOT(close)
113
114
115 void on_message(
116 websocketpp::frame::opcode::value opcode, const std::string& payload) const
117 {
118 if(opcode == websocketpp::frame::opcode::text && onTextMessage.isCallable())
119 onTextMessage.call({QString::fromUtf8(payload.data(), payload.size())});
120 else if(opcode == websocketpp::frame::opcode::binary && onBinaryMessage.isCallable())
121 {
122 if(auto engine = qjsEngine(this))
123 onBinaryMessage.call(
124 {engine->toScriptValue(QByteArray(payload.data(), payload.size()))});
125 }
126
127 if(onBytes.isCallable())
128 {
129 if(auto engine = qjsEngine(this))
130 onBytes.call({engine->toScriptValue(QByteArray(payload.data(), payload.size()))});
131 }
132 }
133
134 QJSValue onBytes;
135 W_PROPERTY(QJSValue, onBytes W_MEMBER onBytes);
136 QJSValue onTextMessage;
137 W_PROPERTY(QJSValue, onTextMessage W_MEMBER onTextMessage);
138 QJSValue onBinaryMessage;
139 W_PROPERTY(QJSValue, onBinaryMessage W_MEMBER onBinaryMessage);
140 QJSValue onClose;
141 W_PROPERTY(QJSValue, onClose W_MEMBER onClose);
142
143private:
144 std::shared_ptr<websocket_server_state> m_state;
145 handler m_hdl;
146};
147
148class qml_websocket_inbound_socket
149 : public QObject
150 , public Nano::Observer
151{
152 W_OBJECT(qml_websocket_inbound_socket)
153public:
154 using state = websocket_server_state;
155 using handler = ossia::net::websocket_server::connection_handler;
156
157 qml_websocket_inbound_socket() { }
158
159 ~qml_websocket_inbound_socket() { shutdown(); }
160
161 bool isOpen() const noexcept { return m_state && m_state->alive; }
162
163 void open(
164 const ossia::net::inbound_socket_configuration& conf,
165 const ossia::net::network_context_ptr& ctx)
166 {
167 auto st = std::make_shared<state>(ctx);
168 auto self = QPointer{this};
169
170 st->server->set_open_handler([st, self](handler hdl) {
171 if(!st->alive)
172 return;
173 if(auto* ptr = self.get())
174 ossia::qt::run_async(
175 ptr,
176 [self, st, hdl] {
177 if(auto* ptr = self.get())
178 ptr->on_connection(st, hdl);
179 },
180 Qt::AutoConnection);
181 });
182
183 st->server->set_close_handler([st, self](handler hdl) {
184 if(!st->alive)
185 return;
186 if(auto* ptr = self.get())
187 ossia::qt::run_async(
188 ptr,
189 [self, hdl] {
190 if(auto* ptr = self.get())
191 ptr->on_disconnection(hdl);
192 },
193 Qt::AutoConnection);
194 });
195
196 st->server->set_message_handler(
197 [st, self](
198 handler hdl, websocketpp::frame::opcode::value opcode,
199 const std::string& payload) -> ossia::net::server_reply {
200 if(!st->alive)
201 return {};
202 if(auto* ptr = self.get())
203 ossia::qt::run_async(
204 ptr,
205 [self, hdl, opcode, msg = payload] {
206 if(auto* ptr = self.get())
207 {
208 if(auto* conn = ptr->find_connection(hdl))
209 conn->on_message(opcode, msg);
210 }
211 },
212 Qt::AutoConnection);
213 return {};
214 });
215
216 m_state = std::move(st);
217 m_state->server->listen(conf.port);
218
219 if(onOpen.isCallable())
220 onOpen.call({qjsEngine(this)->newQObject(this)});
221 }
222
223 void close()
224 {
225 if(!m_state)
226 return;
227 shutdown();
228 if(onClose.isCallable())
229 onClose.call();
230 }
231 W_SLOT(close)
232
233 QJSValue onOpen;
234 QJSValue onClose;
235 QJSValue onError;
236 QJSValue onConnection;
237
238private:
241 void shutdown()
242 {
243 if(!m_state)
244 return;
245 auto st = std::move(m_state);
246 st->alive = false;
247
248 std::vector<handler> handles;
249 handles.reserve(m_connections.size());
250 for(auto* conn : m_connections)
251 {
252 handles.push_back(conn->handle());
253 // A script may reach here from inside one of this connection's own
254 // callbacks (server.close() called from onTextMessage): deleting it
255 // synchronously would destroy the object whose method is on the stack.
256 conn->deleteLater();
257 }
258 m_connections.clear();
259
260 // Closing the clients and the listener walks websocketpp state that the
261 // asio thread is concurrently using to re-arm async_accept, so it has to
262 // run on the context like every other operation on the server.
263 boost::asio::dispatch(st->context, [st, handles = std::move(handles)] {
264 for(const auto& hdl : handles)
265 {
266 try
267 {
268 st->server->close(hdl);
269 }
270 catch(...)
271 {
272 }
273 }
274
275 try
276 {
277 st->server->stop();
278 }
279 catch(...)
280 {
281 }
282 });
283 }
284
285 void on_connection(const std::shared_ptr<state>& st, const handler& hdl)
286 {
287 if(!m_state || m_state != st)
288 return;
289
290 auto conn = new qml_websocket_connection{st, hdl};
291 // Parent to the server so that Qt uses CppOwnership (prevents QML GC)
292 conn->setParent(this);
293 m_connections.push_back(conn);
294
295 if(onConnection.isCallable())
296 onConnection.call({qjsEngine(this)->newQObject(conn)});
297 }
298
299 void on_disconnection(const handler& hdl)
300 {
301 for(auto it = m_connections.begin(); it != m_connections.end(); ++it)
302 {
303 auto* conn = *it;
304 if(!same(conn->handle(), hdl))
305 continue;
306
307 m_connections.erase(it);
308 if(conn->onClose.isCallable())
309 conn->onClose.call();
310 conn->deleteLater();
311 return;
312 }
313 }
314
315 qml_websocket_connection* find_connection(const handler& hdl) const noexcept
316 {
317 for(auto* conn : m_connections)
318 if(same(conn->handle(), hdl))
319 return conn;
320 return nullptr;
321 }
322
323 static bool same(const handler& lhs, const handler& rhs) noexcept
324 {
325 return !lhs.owner_before(rhs) && !rhs.owner_before(lhs);
326 }
327
328 std::shared_ptr<state> m_state;
329 std::vector<qml_websocket_connection*> m_connections;
330};
331}
Definition qml_device.cpp:43
If using the library, you should create this class at some point.
Definition context.hpp:27
context()
Most common case.
Definition context.cpp:89
Definition qml_ws_inbound_socket.hpp:25