Project
Loading...
Searching...
No Matches
StatusWebSocketHandler.cxx
Go to the documentation of this file.
1// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
11
13#include "DPLWebSocket.h"
14#include "DriverServerContext.h"
16#include <csignal>
17#include <unistd.h>
25#include "Framework/Signpost.h"
26#include <algorithm>
27#include <array>
28#include <cstdio>
29#include <fstream>
30#include <sstream>
31#include <string>
32#include <string_view>
33#include <unordered_map>
34#include <vector>
35
36namespace o2::framework
37{
38
39namespace
40{
41
42std::string jsonEscape(std::string_view s)
43{
44 std::string out;
45 out.reserve(s.size() + 4);
46 for (unsigned char c : s) {
47 switch (c) {
48 case '"':
49 out += "\\\"";
50 break;
51 case '\\':
52 out += "\\\\";
53 break;
54 case '\n':
55 out += "\\n";
56 break;
57 case '\r':
58 out += "\\r";
59 break;
60 case '\t':
61 out += "\\t";
62 break;
63 default:
64 if (c < 0x20) {
65 char buf[8];
66 snprintf(buf, sizeof(buf), "\\u%04x", c);
67 out += buf;
68 } else {
69 out += static_cast<char>(c);
70 }
71 }
72 }
73 return out;
74}
75
76char const* logLevelName(LogParsingHelpers::LogLevel level)
77{
78 switch (level) {
79 case LogParsingHelpers::LogLevel::Debug:
80 return "debug";
81 case LogParsingHelpers::LogLevel::Info:
82 return "info";
83 case LogParsingHelpers::LogLevel::Important:
84 return "important";
85 case LogParsingHelpers::LogLevel::Warning:
86 return "warning";
87 case LogParsingHelpers::LogLevel::Alarm:
88 return "alarm";
89 case LogParsingHelpers::LogLevel::Error:
90 return "error";
91 case LogParsingHelpers::LogLevel::Critical:
92 return "critical";
93 case LogParsingHelpers::LogLevel::Fatal:
94 return "fatal";
95 default:
96 return "unknown";
97 }
98}
99
100char const* streamingStateName(StreamingState s)
101{
102 switch (s) {
104 return "Streaming";
106 return "EndOfStreaming";
108 return "Idle";
109 default:
110 return "Unknown";
111 }
112}
113
114void appendMetricValue(std::string& out, DeviceMetricsInfo const& info, size_t mi)
115{
116 auto const& metric = info.metrics[mi];
117 if (metric.pos == 0) {
118 out += "null";
119 return;
120 }
121 size_t last = (metric.pos - 1) % metricStorageSize(metric.type);
122 switch (metric.type) {
123 case MetricType::Int:
124 out += std::to_string(info.intMetrics[metric.storeIdx][last]);
125 break;
126 case MetricType::Float: {
127 char buf[32];
128 snprintf(buf, sizeof(buf), "%g", static_cast<double>(info.floatMetrics[metric.storeIdx][last]));
129 out += buf;
130 break;
131 }
133 out += std::to_string(info.uint64Metrics[metric.storeIdx][last]);
134 break;
135 default:
136 out += "null";
137 }
138}
139
143std::string_view extractStringField(std::string_view json, std::string_view key)
144{
145 std::string needle;
146 needle += '"';
147 needle += key;
148 needle += "\":";
149 auto pos = json.find(needle);
150 if (pos == std::string_view::npos) {
151 return {};
152 }
153 pos += needle.size();
154 // skip optional whitespace between ':' and '"'
155 while (pos < json.size() && json[pos] == ' ') {
156 ++pos;
157 }
158 if (pos >= json.size() || json[pos] != '"') {
159 return {};
160 }
161 ++pos; // skip opening quote
162 auto end = json.find('"', pos);
163 if (end == std::string_view::npos) {
164 return {};
165 }
166 return json.substr(pos, end - pos);
167}
168
171std::string_view extractArrayField(std::string_view json, std::string_view key)
172{
173 std::string needle;
174 needle += '"';
175 needle += key;
176 needle += "\":";
177 auto pos = json.find(needle);
178 if (pos == std::string_view::npos) {
179 return {};
180 }
181 pos += needle.size();
182 // skip whitespace
183 while (pos < json.size() && json[pos] == ' ') {
184 ++pos;
185 }
186 if (pos >= json.size() || json[pos] != '[') {
187 return {};
188 }
189 auto start = pos;
190 size_t depth = 0;
191 while (pos < json.size()) {
192 if (json[pos] == '[') {
193 ++depth;
194 } else if (json[pos] == ']') {
195 --depth;
196 if (depth == 0) {
197 return json.substr(start, pos - start + 1);
198 }
199 }
200 ++pos;
201 }
202 return {};
203}
204
207template <typename F>
208void forEachStringInArray(std::string_view arr, F&& callback)
209{
210 // arr is like ["name1","name2"]
211 size_t pos = 0;
212 while (pos < arr.size()) {
213 auto q = arr.find('"', pos);
214 if (q == std::string_view::npos) {
215 break;
216 }
217 auto end = arr.find('"', q + 1);
218 if (end == std::string_view::npos) {
219 break;
220 }
221 callback(arr.substr(q + 1, end - q - 1));
222 pos = end + 1;
223 }
224}
225
226} // anonymous namespace
227
229 : mContext{context}, mHandler{handler}
230{
231}
232
234{
235 auto& handlers = mContext.statusHandlers;
236 handlers.erase(std::remove(handlers.begin(), handlers.end(), this), handlers.end());
237}
238
239void StatusWebSocketHandler::headers(std::map<std::string, std::string> const&)
240{
241 sendSnapshot();
242}
243
244void StatusWebSocketHandler::frame(char const* data, size_t s)
245{
246 std::string_view msg{data, s};
247 auto cmd = extractStringField(msg, "cmd");
248 if (cmd.empty()) {
249 return;
250 }
251 auto deviceName = extractStringField(msg, "device");
252
253 if (cmd == "list_metrics") {
254 handleListMetrics(deviceName);
255 } else if (cmd == "subscribe") {
256 handleSubscribe(deviceName, extractArrayField(msg, "metrics"));
257 } else if (cmd == "unsubscribe") {
258 handleUnsubscribe(deviceName, extractArrayField(msg, "metrics"));
259 } else if (cmd == "subscribe_logs") {
260 handleSubscribeLogs(deviceName);
261 } else if (cmd == "unsubscribe_logs") {
262 handleUnsubscribeLogs(deviceName);
263 } else if (cmd == "start_devices") {
264 handleStartDevices();
265 } else if (cmd == "enable_signpost") {
266 handleEnableSignpost(deviceName, extractArrayField(msg, "streams"));
267 } else if (cmd == "disable_signpost") {
268 handleDisableSignpost(deviceName, extractArrayField(msg, "streams"));
269 } else if (cmd == "smaps") {
270 handleSmaps(deviceName);
271 }
272}
273
274void StatusWebSocketHandler::sendText(std::string const& json)
275{
276 std::vector<uv_buf_t> outputs;
277 encode_websocket_frames(outputs, json.data(), json.size(), WebSocketOpCode::Text, 0);
278 mHandler->write(outputs);
279}
280
282{
283 auto const& specs = *mContext.specs;
284 auto const& infos = *mContext.infos;
285
286 // Size subscription tables to current device count; grow lazily as needed.
287 mSubscribedMetrics.resize(specs.size());
288 mLastLogSeq.resize(infos.size());
289 for (size_t di = 0; di < infos.size(); ++di) {
290 mLastLogSeq[di] = infos[di].logSeq;
291 }
292
293 std::string out;
294 out.reserve(512 + specs.size() * 128);
295 out += R"({"type":"snapshot","devices":[)";
296 for (size_t di = 0; di < specs.size(); ++di) {
297 if (di > 0) {
298 out += ',';
299 }
300 auto const& info = infos[di];
301 out += R"({"name":")";
302 out += jsonEscape(specs[di].name);
303 out += R"(","pid":)";
304 out += std::to_string(info.pid);
305 out += R"(,"active":)";
306 out += info.active ? "true" : "false";
307 out += R"(,"streamingState":")";
308 out += streamingStateName(info.streamingState);
309 out += R"(","deviceState":")";
310 out += jsonEscape(info.deviceState);
311 out += R"("})";
312 }
313 out += "]}";
314 sendText(out);
315}
316
318{
319 auto const& specs = *mContext.specs;
320 auto const& metrics = *mContext.metrics;
321
322 if (deviceIndex >= specs.size() || deviceIndex >= metrics.size()) {
323 return;
324 }
325
326 // Lazily grow the subscription table if new devices were added after snapshot.
327 if (mSubscribedMetrics.size() <= deviceIndex) {
328 mSubscribedMetrics.resize(deviceIndex + 1);
329 }
330
331 auto const& subscribed = mSubscribedMetrics[deviceIndex];
332 if (subscribed.empty()) {
333 return;
334 }
335
336 auto const& info = metrics[deviceIndex];
337 std::string metricsJson;
338 metricsJson += '{';
339 bool first = true;
340 for (size_t mi = 0; mi < info.metrics.size(); ++mi) {
341 if (!info.changed[mi]) {
342 continue;
343 }
344 auto const& metric = info.metrics[mi];
345 if (metric.type == MetricType::String ||
346 metric.type == MetricType::Enum ||
347 metric.type == MetricType::Unknown) {
348 continue;
349 }
350 auto const& label = info.metricLabels[mi];
351 std::string_view labelSV{label.label, label.size};
352 if (subscribed.find(std::string(labelSV)) == subscribed.end()) {
353 continue;
354 }
355 if (!first) {
356 metricsJson += ',';
357 }
358 first = false;
359 metricsJson += '"';
360 metricsJson += jsonEscape(labelSV);
361 metricsJson += "\":";
362 appendMetricValue(metricsJson, info, mi);
363 }
364 metricsJson += '}';
365
366 if (first) {
367 // Nothing subscribed changed in this cycle.
368 return;
369 }
370
371 std::string out;
372 out += R"({"type":"update","device":)";
373 out += std::to_string(deviceIndex);
374 out += R"(,"name":")";
375 out += jsonEscape(specs[deviceIndex].name);
376 out += R"(","metrics":)";
377 out += metricsJson;
378 out += '}';
379 sendText(out);
380}
381
382void StatusWebSocketHandler::handleListMetrics(std::string_view deviceName)
383{
384 size_t di = findDeviceIndex(deviceName);
385 if (di == SIZE_MAX) {
386 return;
387 }
388 auto const& metrics = *mContext.metrics;
389
390 std::string out;
391 out += R"({"type":"metrics_list","device":")";
392 out += jsonEscape(deviceName);
393 out += R"(","metrics":[)";
394 bool first = true;
395 if (di < metrics.size()) {
396 auto const& info = metrics[di];
397 for (size_t mi = 0; mi < info.metrics.size(); ++mi) {
398 auto const& metric = info.metrics[mi];
399 if (metric.type == MetricType::String ||
400 metric.type == MetricType::Enum ||
401 metric.type == MetricType::Unknown) {
402 continue;
403 }
404 if (!first) {
405 out += ',';
406 }
407 first = false;
408 auto const& label = info.metricLabels[mi];
409 out += '"';
410 out += jsonEscape({label.label, label.size});
411 out += '"';
412 }
413 }
414 out += "]}";
415 sendText(out);
416}
417
418void StatusWebSocketHandler::handleSubscribe(std::string_view deviceName, std::string_view metricsArr)
419{
420 size_t di = findDeviceIndex(deviceName);
421 if (di == SIZE_MAX || metricsArr.empty()) {
422 return;
423 }
424 if (mSubscribedMetrics.size() <= di) {
425 mSubscribedMetrics.resize(di + 1);
426 }
427 forEachStringInArray(metricsArr, [&](std::string_view name) {
428 mSubscribedMetrics[di].emplace(name);
429 });
430}
431
432void StatusWebSocketHandler::handleUnsubscribe(std::string_view deviceName, std::string_view metricsArr)
433{
434 size_t di = findDeviceIndex(deviceName);
435 if (di == SIZE_MAX || metricsArr.empty() || di >= mSubscribedMetrics.size()) {
436 return;
437 }
438 forEachStringInArray(metricsArr, [&](std::string_view name) {
439 mSubscribedMetrics[di].erase(std::string(name));
440 });
441}
442
443size_t StatusWebSocketHandler::findDeviceIndex(std::string_view name) const
444{
445 auto const& specs = *mContext.specs;
446 for (size_t di = 0; di < specs.size(); ++di) {
447 if (specs[di].name == name) {
448 return di;
449 }
450 }
451 return SIZE_MAX;
452}
453
454void StatusWebSocketHandler::handleSmaps(std::string_view deviceName)
455{
456 size_t di = findDeviceIndex(deviceName);
457 if (di == SIZE_MAX) {
458 return;
459 }
460 auto pid = (*mContext.infos)[di].pid;
461 std::string out = R"({"type":"smaps","device":")";
462 out += jsonEscape(deviceName);
463 out += R"(","pid":)";
464 out += std::to_string(pid);
465
466 std::ifstream smaps("/proc/" + std::to_string(pid) + "/smaps");
467 if (!smaps) {
468 out += R"(,"error":"cannot read /proc/)" + std::to_string(pid) + R"(/smaps"})";
469 sendText(out);
470 return;
471 }
472 constexpr std::array<std::string_view, 7> fields{"Rss:", "Pss:", "Shared_Clean:", "Shared_Dirty:", "Private_Clean:", "Private_Dirty:", "Swap:"};
473 std::unordered_map<std::string, std::array<uint64_t, fields.size()>> usage;
474 std::array<uint64_t, fields.size()>* current = nullptr;
475 std::string line;
476 while (std::getline(smaps, line)) {
477 std::istringstream tokens(line);
478 std::string first;
479 tokens >> first;
480 if (first.empty()) {
481 continue;
482 }
483 if (first.back() != ':') {
484 std::string perms, offset, dev, inode, name;
485 tokens >> perms >> offset >> dev >> inode;
486 std::getline(tokens >> std::ws, name);
487 current = &usage[name.empty() ? "[anon]" : name];
488 continue;
489 }
490 auto field = std::find(fields.begin(), fields.end(), first);
491 uint64_t kb = 0;
492 if (current && field != fields.end() && tokens >> kb) {
493 (*current)[field - fields.begin()] += kb;
494 }
495 }
496 out += R"(,"fields":["rss","pss","shared_clean","shared_dirty","private_clean","private_dirty","swap"],"mappings":{)";
497 bool first = true;
498 for (auto const& [name, kbs] : usage) {
499 out += first ? "\"" : ",\"";
500 first = false;
501 out += jsonEscape(name);
502 out += "\":[";
503 for (size_t fi = 0; fi < kbs.size(); ++fi) {
504 out += (fi ? "," : "") + std::to_string(kbs[fi]);
505 }
506 out += ']';
507 }
508 out += "}}";
509 sendText(out);
510}
511
512void StatusWebSocketHandler::handleStartDevices()
513{
514 for (auto const& info : *mContext.infos) {
515 if (info.active) {
516 kill(info.pid, SIGCONT);
517 }
518 }
519}
520
521void StatusWebSocketHandler::handleEnableSignpost(std::string_view deviceName, std::string_view streamsArr)
522{
523 if (streamsArr.empty()) {
524 return;
525 }
526 if (deviceName.empty()) {
527 // Driver process — toggle in-process via o2_walk_logs.
528 forEachStringInArray(streamsArr, [](std::string_view streamName) {
529 std::string target(streamName);
530 o2_walk_logs([](char const* name, void* l, void* context) -> bool {
531 auto* log = static_cast<_o2_log_t*>(l);
532 if (static_cast<std::string*>(context)->compare(name) == 0) {
533 _o2_log_set_stacktrace(log, log->defaultStacktrace);
534 return false;
535 }
536 return true;
537 }, &target);
538 });
539 } else {
540 size_t di = findDeviceIndex(deviceName);
541 if (di == SIZE_MAX || di >= mContext.controls->size() || !(*mContext.controls)[di].controller) {
542 return;
543 }
544 auto* controller = (*mContext.controls)[di].controller;
545 forEachStringInArray(streamsArr, [controller](std::string_view name) {
546 std::string cmd = "/signpost:enable ";
547 cmd += name;
548 controller->write(cmd.c_str(), cmd.size());
549 });
550 }
551}
552
553void StatusWebSocketHandler::handleDisableSignpost(std::string_view deviceName, std::string_view streamsArr)
554{
555 if (streamsArr.empty()) {
556 return;
557 }
558 if (deviceName.empty()) {
559 forEachStringInArray(streamsArr, [](std::string_view streamName) {
560 std::string target(streamName);
561 o2_walk_logs([](char const* name, void* l, void* context) -> bool {
562 auto* log = static_cast<_o2_log_t*>(l);
563 if (static_cast<std::string*>(context)->compare(name) == 0) {
565 return false;
566 }
567 return true;
568 }, &target);
569 });
570 } else {
571 size_t di = findDeviceIndex(deviceName);
572 if (di == SIZE_MAX || di >= mContext.controls->size() || !(*mContext.controls)[di].controller) {
573 return;
574 }
575 auto* controller = (*mContext.controls)[di].controller;
576 forEachStringInArray(streamsArr, [controller](std::string_view name) {
577 std::string cmd = "/signpost:disable ";
578 cmd += name;
579 controller->write(cmd.c_str(), cmd.size());
580 });
581 }
582}
583
584void StatusWebSocketHandler::handleSubscribeLogs(std::string_view deviceName)
585{
586 size_t di = findDeviceIndex(deviceName);
587 if (di == SIZE_MAX) {
588 return;
589 }
590 if (mLastLogSeq.size() <= di) {
591 mLastLogSeq.resize(di + 1, 0);
592 }
593 // Start the cursor at the current log position so we only push future lines.
594 mLastLogSeq[di] = (*mContext.infos)[di].logSeq;
595 mLogSubscriptions.insert(di);
596}
597
598void StatusWebSocketHandler::handleUnsubscribeLogs(std::string_view deviceName)
599{
600 size_t di = findDeviceIndex(deviceName);
601 if (di == SIZE_MAX) {
602 return;
603 }
604 mLogSubscriptions.erase(di);
605}
606
608{
609 if (mLogSubscriptions.find(deviceIndex) == mLogSubscriptions.end()) {
610 return;
611 }
612 auto const& infos = *mContext.infos;
613 auto const& specs = *mContext.specs;
614 if (deviceIndex >= infos.size() || deviceIndex >= specs.size()) {
615 return;
616 }
617 if (mLastLogSeq.size() <= deviceIndex) {
618 mLastLogSeq.resize(deviceIndex + 1, 0);
619 }
620
621 auto const& info = infos[deviceIndex];
622 size_t newLines = info.logSeq - mLastLogSeq[deviceIndex];
623 if (newLines == 0) {
624 return;
625 }
626 // Cap to buffer size to avoid re-reading overwritten entries.
627 if (newLines > info.history.size()) {
628 newLines = info.history.size();
629 }
630
631 size_t histSize = info.history.size();
632 // The oldest unread entry sits at (historyPos - newLines + histSize) % histSize.
633 size_t startPos = (info.historyPos + histSize - newLines) % histSize;
634
635 std::string_view devName = specs[deviceIndex].name;
636 for (size_t i = 0; i < newLines; ++i) {
637 size_t pos = (startPos + i) % histSize;
638 std::string out;
639 out += R"({"type":"log","device":")";
640 out += jsonEscape(devName);
641 out += R"(","level":")";
642 out += logLevelName(info.historyLevel[pos]);
643 out += R"(","line":")";
644 out += jsonEscape(info.history[pos]);
645 out += R"("})";
646 sendText(out);
647 }
648 mLastLogSeq[deviceIndex] = info.logSeq;
649}
650
651} // namespace o2::framework
random_device dev
std::vector< std::shared_ptr< arrow::Field > > fields
int32_t i
Timeframe settings Average distance in cm of events merged into Randomize distance around average distance of merged events Simulate correct bunch interactions instead of placing only the average number of events A value[n] sets TFlen for[n] collisions in Number of bunches per trainsort Number of bunch trains Instantaneous interaction rate Do not simulate border Total number of collisions to be placed in the interior of all time Do not select random but walk over array of events in stride steps h Event generator settings h configStandalone Use CPU for GPU Force usage of the specified GPU device no CPU fallback Continue processing after an error s
Timeframe settings Average distance in cm of events merged into Randomize distance around average distance of merged events Simulate correct bunch interactions instead of placing only the average number of events A value[n] sets TFlen for[n] collisions in Number of bunches per trainsort Number of bunch trains Instantaneous interaction rate Do not simulate border Total number of collisions to be placed in the interior of all time Do not select random but walk over array of events in stride steps h Event generator settings h configStandalone Use CPU for GPU Force usage of the specified GPU device no CPU fallback Continue processing after an error First event to Number of events to Number of iterations to Number of iterations to Number of initial iterations excluded from const char Directory with events to Events directory is and not inside events Show standalone event Run event generator maximum time bin of continuous for triggered for automatic continuous for automatic continuous triggered uint64_t
uint16_t pos
Definition RawData.h:3
uint16_t pid
Definition RawData.h:2
uint32_t c
Definition RawData.h:2
o2_log_handle_t * o2_walk_logs(bool(*callback)(char const *name, void *log, void *context), void *context=nullptr)
void _o2_log_set_stacktrace(_o2_log_t *log, int stacktrace)
StringRef key
GLuint GLuint end
Definition glcorearb.h:469
GLuint const GLchar * name
Definition glcorearb.h:781
GLint first
Definition glcorearb.h:399
GLsizei GLenum const void GLuint GLsizei GLfloat * metrics
Definition glcorearb.h:5500
GLenum target
Definition glcorearb.h:1641
GLintptr offset
Definition glcorearb.h:660
GLuint GLsizei const GLchar * label
Definition glcorearb.h:2519
GLint GLint GLsizei GLsizei GLsizei depth
Definition glcorearb.h:470
GLint level
Definition glcorearb.h:275
GLenum GLuint GLenum GLsizei const GLchar * buf
Definition glcorearb.h:2514
GLsizeiptr const void GLenum usage
Definition glcorearb.h:659
Defining ITS Vertex explicitly as messageable.
Definition Cartesian.h:288
constexpr size_t metricStorageSize()
void encode_websocket_frames(std::vector< uv_buf_t > &outputs, char const *src, size_t size, WebSocketOpCode opcode, uint32_t mask)
@ EndOfStreaming
End of streaming requested, but not notified.
@ Streaming
Data is being processed.
@ Idle
End of streaming notified.
int32_t const char int32_t line
std::string to_string(gsl::span< T, Size > span)
Definition common.h:52
nlohmann::json json
std::vector< DeviceSpec > * specs
std::vector< DeviceMetricsInfo > * metrics
std::vector< StatusWebSocketHandler * > statusHandlers
std::vector< DeviceInfo > * infos
std::vector< DeviceControl > * controls
LogLevel
Possible log levels for device log entries.
StatusWebSocketHandler(DriverServerContext &context, WSDPLHandler *handler)
void sendSnapshot()
Send a minimal JSON snapshot (device list + basic state, no metrics/logs).
void frame(char const *data, size_t s) override
Handles incoming commands from the MCP client.
void headers(std::map< std::string, std::string > const &headers) override
Sends the minimal snapshot on handshake completion.
void write(char const *, size_t)
Helper to write a message to the associated client.
void compare(std::string_view s1, std::string_view s2)
uint64_t const void const *restrict const msg
Definition x9.h:153