Logo ROOT   6.30.04
Reference Guide
 All Namespaces Files Pages
RWebWindow.cxx
Go to the documentation of this file.
1 /// \file RWebWindow.cxx
2 /// \ingroup WebGui ROOT7
3 /// \author Sergey Linev <s.linev@gsi.de>
4 /// \date 2017-10-16
5 /// \warning This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback
6 /// is welcome!
7 
8 /*************************************************************************
9  * Copyright (C) 1995-2019, Rene Brun and Fons Rademakers. *
10  * All rights reserved. *
11  * *
12  * For the licensing terms see $ROOTSYS/LICENSE. *
13  * For the list of contributors see $ROOTSYS/README/CREDITS. *
14  *************************************************************************/
15 
16 #include <ROOT/RWebWindow.hxx>
17 
19 #include <ROOT/RLogger.hxx>
20 
21 #include "RWebWindowWSHandler.hxx"
22 #include "THttpCallArg.h"
23 #include "TUrl.h"
24 #include "TROOT.h"
25 
26 #include <cstring>
27 #include <cstdlib>
28 #include <utility>
29 #include <assert.h>
30 #include <algorithm>
31 #include <fstream>
32 
33 using namespace std::string_literals;
34 
35 //////////////////////////////////////////////////////////////////////////////////////////
36 /// Destructor for WebConn
37 /// Notify special HTTP request which blocks headless browser from exit
38 
39 ROOT::Experimental::RWebWindow::WebConn::~WebConn()
40 {
41  if (fHold) {
42  fHold->SetTextContent("console.log('execute holder script'); if (window) setTimeout (window.close, 1000); if (window) window.close();");
43  fHold->NotifyCondition();
44  fHold.reset();
45  }
46 }
47 
48 
49 /** \class ROOT::Experimental::RWebWindow
50 \ingroup webdisplay
51 
52 Represents web window, which can be shown in web browser or any other supported environment
53 
54 Window can be configured to run either in the normal or in the batch (headless) mode.
55 In second case no any graphical elements will be created. For the normal window one can configure geometry
56 (width and height), which are applied when window shown.
57 
58 Each window can be shown several times (if allowed) in different places - either as the
59 CEF (chromium embedded) window or in the standard web browser. When started, window will open and show
60 HTML page, configured with RWebWindow::SetDefaultPage() method.
61 
62 Typically (but not necessarily) clients open web socket connection to the window and one can exchange data,
63 using RWebWindow::Send() method and call-back function assigned via RWebWindow::SetDataCallBack().
64 
65 */
66 
67 
68 //////////////////////////////////////////////////////////////////////////////////////////
69 /// RWebWindow constructor
70 /// Should be defined here because of std::unique_ptr<RWebWindowWSHandler>
71 
72 ROOT::Experimental::RWebWindow::RWebWindow() = default;
73 
74 //////////////////////////////////////////////////////////////////////////////////////////
75 /// RWebWindow destructor
76 /// Closes all connections and remove window from manager
77 
78 ROOT::Experimental::RWebWindow::~RWebWindow()
79 {
80  if (fMaster)
81  fMaster->RemoveEmbedWindow(fMasterConnId, fMasterChannel);
82 
83  if (fWSHandler)
84  fWSHandler->SetDisabled();
85 
86  if (fMgr) {
87 
88  // make copy of all connections
89  auto lst = GetConnections();
90 
91  {
92  // clear connections vector under mutex
93  std::lock_guard<std::mutex> grd(fConnMutex);
94  fConn.clear();
95  fPendingConn.clear();
96  }
97 
98  for (auto &conn : lst) {
99  conn->fActive = false;
100  for (auto &elem: conn->fEmbed)
101  elem.second->fMaster.reset();
102  }
103 
104  fMgr->Unregister(*this);
105  }
106 }
107 
108 //////////////////////////////////////////////////////////////////////////////////////////
109 /// Configure window to show some of existing JSROOT panels
110 /// It uses "file:rootui5sys/panel/panel.html" as default HTML page
111 /// At the moment only FitPanel is existing
112 
113 void ROOT::Experimental::RWebWindow::SetPanelName(const std::string &name)
114 {
115  {
116  std::lock_guard<std::mutex> grd(fConnMutex);
117  if (!fConn.empty()) {
118  R__ERROR_HERE("webgui") << "Cannot configure panel when connection exists";
119  return;
120  }
121  }
122 
123  fPanelName = name;
124  SetDefaultPage("file:rootui5sys/panel/panel.html");
125 }
126 
127 //////////////////////////////////////////////////////////////////////////////////////////
128 /// Assigns manager reference, window id and creates websocket handler, used for communication with the clients
129 
130 std::shared_ptr<ROOT::Experimental::RWebWindowWSHandler>
131 ROOT::Experimental::RWebWindow::CreateWSHandler(std::shared_ptr<RWebWindowsManager> mgr, unsigned id, double tmout)
132 {
133  fMgr = mgr;
134  fId = id;
135  fOperationTmout = tmout;
136 
137  fSendMT = fMgr->IsUseSenderThreads();
138  fWSHandler = std::make_shared<RWebWindowWSHandler>(*this, Form("win%u", GetId()));
139 
140  return fWSHandler;
141 }
142 
143 //////////////////////////////////////////////////////////////////////////////////////////
144 /// Return URL string to access web window
145 /// If remote flag is specified, real HTTP server will be started automatically
146 
147 std::string ROOT::Experimental::RWebWindow::GetUrl(bool remote)
148 {
149  return fMgr->GetUrl(*this, remote);
150 }
151 
152 //////////////////////////////////////////////////////////////////////////////////////////
153 /// Return THttpServer instance serving requests to the window
154 
155 THttpServer *ROOT::Experimental::RWebWindow::GetServer()
156 {
157  return fMgr->GetServer();
158 }
159 
160 //////////////////////////////////////////////////////////////////////////////////////////
161 /// Show window in specified location
162 /// See ROOT::Experimental::RWebWindowsManager::Show() docu for more info
163 /// returns (future) connection id (or 0 when fails)
164 
165 unsigned ROOT::Experimental::RWebWindow::Show(const RWebDisplayArgs &args)
166 {
167  return fMgr->ShowWindow(*this, false, args);
168 }
169 
170 //////////////////////////////////////////////////////////////////////////////////////////
171 /// Create batch job for specified window
172 /// Normally only single batch job is used, but many can be created
173 /// See ROOT::Experimental::RWebWindowsManager::Show() docu for more info
174 /// returns (future) connection id (or 0 when fails)
175 
176 unsigned ROOT::Experimental::RWebWindow::MakeBatch(bool create_new, const RWebDisplayArgs &args)
177 {
178  unsigned connid = 0;
179  if (!create_new)
180  connid = FindBatch();
181  if (!connid)
182  connid = fMgr->ShowWindow(*this, true, args);
183  return connid;
184 }
185 
186 //////////////////////////////////////////////////////////////////////////////////////////
187 /// Returns connection id of batch job
188 /// Connection to that job may not be initialized yet
189 /// If connection does not exists, returns 0
190 
191 unsigned ROOT::Experimental::RWebWindow::FindBatch()
192 {
193  std::lock_guard<std::mutex> grd(fConnMutex);
194 
195  for (auto &entry : fPendingConn) {
196  if (entry->fBatchMode)
197  return entry->fConnId;
198  }
199 
200  for (auto &conn : fConn) {
201  if (conn->fBatchMode)
202  return conn->fConnId;
203  }
204 
205  return 0;
206 }
207 
208 //////////////////////////////////////////////////////////////////////////////////////////
209 /// Returns first connection id where window is displayed
210 /// It could be that connection(s) not yet fully established - but also not timed out
211 /// Batch jobs will be ignored here
212 /// Returns 0 if connection not exists
213 
214 unsigned ROOT::Experimental::RWebWindow::GetDisplayConnection() const
215 {
216  std::lock_guard<std::mutex> grd(fConnMutex);
217 
218  for (auto &entry : fPendingConn) {
219  if (!entry->fBatchMode)
220  return entry->fConnId;
221  }
222 
223  for (auto &conn : fConn) {
224  if (!conn->fBatchMode)
225  return conn->fConnId;
226  }
227 
228  return 0;
229 }
230 
231 //////////////////////////////////////////////////////////////////////////////////////////
232 /// Find connection with given websocket id
233 /// Connection mutex should be locked before method calling
234 
235 std::shared_ptr<ROOT::Experimental::RWebWindow::WebConn> ROOT::Experimental::RWebWindow::FindOrCreateConnection(unsigned wsid, bool make_new, const char *query)
236 {
237  std::lock_guard<std::mutex> grd(fConnMutex);
238 
239  for (auto &conn : fConn) {
240  if (conn->fWSId == wsid)
241  return conn;
242  }
243 
244  // put code to create new connection here to stay under same locked mutex
245  if (make_new) {
246  // check if key was registered already
247 
248  std::shared_ptr<WebConn> key;
249  std::string keyvalue;
250 
251  if (query) {
252  TUrl url;
253  url.SetOptions(query);
254  if (url.HasOption("key"))
255  keyvalue = url.GetValueFromOptions("key");
256  }
257 
258  if (!keyvalue.empty())
259  for (size_t n = 0; n < fPendingConn.size(); ++n)
260  if (fPendingConn[n]->fKey == keyvalue) {
261  key = std::move(fPendingConn[n]);
262  fPendingConn.erase(fPendingConn.begin() + n);
263  break;
264  }
265 
266  if (key) {
267  key->fWSId = wsid;
268  key->fActive = true;
269  key->ResetStamps(); // TODO: probably, can be moved outside locked area
270  fConn.emplace_back(key);
271  } else {
272  fConn.emplace_back(std::make_shared<WebConn>(++fConnCnt, wsid));
273  }
274  }
275 
276  return nullptr;
277 }
278 
279 //////////////////////////////////////////////////////////////////////////////////////////
280 /// Remove connection with given websocket id
281 
282 std::shared_ptr<ROOT::Experimental::RWebWindow::WebConn> ROOT::Experimental::RWebWindow::RemoveConnection(unsigned wsid)
283 {
284 
285  std::shared_ptr<WebConn> res;
286 
287  {
288  std::lock_guard<std::mutex> grd(fConnMutex);
289 
290  for (size_t n = 0; n < fConn.size(); ++n)
291  if (fConn[n]->fWSId == wsid) {
292  res = std::move(fConn[n]);
293  fConn.erase(fConn.begin() + n);
294  res->fActive = false;
295  break;
296  }
297  }
298 
299  if (res)
300  for (auto &elem: res->fEmbed)
301  elem.second->fMaster.reset();
302 
303  return res;
304 }
305 
306 //////////////////////////////////////////////////////////////////////////////////////////
307 /// Process special http request, used to hold headless browser running
308 /// Such requests should not be replied for the long time
309 /// Be aware that function called directly from THttpServer thread, which is not same thread as window
310 
311 bool ROOT::Experimental::RWebWindow::ProcessBatchHolder(std::shared_ptr<THttpCallArg> &arg)
312 {
313  std::string query = arg->GetQuery();
314 
315  if (query.compare(0, 4, "key=") != 0)
316  return false;
317 
318  std::string key = query.substr(4);
319 
320  std::shared_ptr<THttpCallArg> prev;
321 
322  bool found_key = false;
323 
324  // use connection mutex to access hold request
325  {
326  std::lock_guard<std::mutex> grd(fConnMutex);
327  for (auto &entry : fPendingConn) {
328  if (entry->fKey == key) {
329  assert(!found_key); // indicate error if many same keys appears
330  found_key = true;
331  prev = std::move(entry->fHold);
332  entry->fHold = arg;
333  }
334  }
335 
336 
337  for (auto &conn : fConn) {
338  if (conn->fKey == key) {
339  assert(!found_key); // indicate error if many same keys appears
340  prev = std::move(conn->fHold);
341  conn->fHold = arg;
342  found_key = true;
343  }
344  }
345  }
346 
347  if (prev) {
348  prev->SetTextContent("console.log('execute holder script'); if (window) window.close();");
349  prev->NotifyCondition();
350  }
351 
352  return found_key;
353 }
354 
355 //////////////////////////////////////////////////////////////////////////////////////////
356 /// Provide data to user callback
357 /// User callback must be executed in the window thread
358 
359 void ROOT::Experimental::RWebWindow::ProvideQueueEntry(unsigned connid, EQueueEntryKind kind, std::string &&arg)
360 {
361  {
362  std::lock_guard<std::mutex> grd(fInputQueueMutex);
363  fInputQueue.emplace(connid, kind, std::move(arg));
364  }
365 
366  InvokeCallbacks();
367 }
368 
369 //////////////////////////////////////////////////////////////////////////////////////////
370 /// Invoke callbacks with existing data
371 /// Must be called from appropriate thread
372 
373 void ROOT::Experimental::RWebWindow::InvokeCallbacks(bool force)
374 {
375  if (fCallbacksThrdIdSet && (fCallbacksThrdId != std::this_thread::get_id()) && !force)
376  return;
377 
378  while (true) {
379  unsigned connid;
380  EQueueEntryKind kind;
381  std::string arg;
382 
383  {
384  std::lock_guard<std::mutex> grd(fInputQueueMutex);
385  if (fInputQueue.size() == 0)
386  return;
387  auto &entry = fInputQueue.front();
388  connid = entry.fConnId;
389  kind = entry.fKind;
390  arg = std::move(entry.fData);
391  fInputQueue.pop();
392  }
393 
394  switch (kind) {
395  case kind_None: break;
396  case kind_Connect:
397  if (fConnCallback)
398  fConnCallback(connid);
399  break;
400  case kind_Data:
401  if (fDataCallback)
402  fDataCallback(connid, arg);
403  break;
404  case kind_Disconnect:
405  if (fDisconnCallback)
406  fDisconnCallback(connid);
407  break;
408  }
409  }
410 }
411 
412 //////////////////////////////////////////////////////////////////////////////////////////
413 /// Add display handle and associated key
414 /// Key is random number generated when starting new window
415 /// When client is connected, key should be supplied to correctly identify it
416 
417 unsigned ROOT::Experimental::RWebWindow::AddDisplayHandle(bool batch_mode, const std::string &key, std::unique_ptr<RWebDisplayHandle> &handle)
418 {
419  std::lock_guard<std::mutex> grd(fConnMutex);
420 
421  ++fConnCnt;
422 
423  auto conn = std::make_shared<WebConn>(fConnCnt, batch_mode, key);
424 
425  std::swap(conn->fDisplayHandle, handle);
426 
427  fPendingConn.emplace_back(conn);
428 
429  return fConnCnt;
430 }
431 
432 //////////////////////////////////////////////////////////////////////////////////////////
433 /// Returns true if provided key value already exists (in processes map or in existing connections)
434 
435 bool ROOT::Experimental::RWebWindow::HasKey(const std::string &key) const
436 {
437  std::lock_guard<std::mutex> grd(fConnMutex);
438 
439  for (auto &entry : fPendingConn) {
440  if (entry->fKey == key)
441  return true;
442  }
443 
444  for (auto &conn : fConn) {
445  if (conn->fKey == key)
446  return true;
447  }
448 
449  return false;
450 }
451 
452 //////////////////////////////////////////////////////////////////////////////////////////
453 /// Check if started process(es) establish connection. After timeout such processed will be killed
454 /// Method invoked from http server thread, therefore appropriate mutex must be used on all relevant data
455 
456 void ROOT::Experimental::RWebWindow::CheckPendingConnections()
457 {
458  if (!fMgr) return;
459 
460  timestamp_t stamp = std::chrono::system_clock::now();
461 
462  float tmout = fMgr->GetLaunchTmout();
463 
464  ConnectionsList_t selected;
465 
466  {
467  std::lock_guard<std::mutex> grd(fConnMutex);
468 
469  auto pred = [&](std::shared_ptr<WebConn> &e) {
470  std::chrono::duration<double> diff = stamp - e->fSendStamp;
471 
472  if (diff.count() > tmout) {
473  R__DEBUG_HERE("webgui") << "Halt process after " << diff.count() << " sec";
474  selected.emplace_back(e);
475  return true;
476  }
477 
478  return false;
479  };
480 
481  fPendingConn.erase(std::remove_if(fPendingConn.begin(), fPendingConn.end(), pred), fPendingConn.end());
482  }
483 
484 }
485 
486 
487 //////////////////////////////////////////////////////////////////////////////////////////
488 /// Check if there are connection which are inactive for longer time
489 /// For instance, batch browser will be stopped if no activity for 30 sec is there
490 
491 void ROOT::Experimental::RWebWindow::CheckInactiveConnections()
492 {
493  timestamp_t stamp = std::chrono::system_clock::now();
494 
495  double batch_tmout = 20.;
496 
497  std::vector<std::shared_ptr<WebConn>> clr;
498 
499  {
500  std::lock_guard<std::mutex> grd(fConnMutex);
501 
502  auto pred = [&](std::shared_ptr<WebConn> &conn) {
503  std::chrono::duration<double> diff = stamp - conn->fSendStamp;
504  // introduce large timeout
505  if ((diff.count() > batch_tmout) && conn->fBatchMode) {
506  conn->fActive = false;
507  clr.emplace_back(conn);
508  return true;
509  }
510  return false;
511  };
512 
513  fConn.erase(std::remove_if(fConn.begin(), fConn.end(), pred), fConn.end());
514  }
515 
516  for (auto &entry : clr)
517  ProvideQueueEntry(entry->fConnId, kind_Disconnect, ""s);
518 
519 }
520 
521 //////////////////////////////////////////////////////////////////////////////////////////
522 /// Processing of websockets call-backs, invoked from RWebWindowWSHandler
523 /// Method invoked from http server thread, therefore appropriate mutex must be used on all relevant data
524 
525 bool ROOT::Experimental::RWebWindow::ProcessWS(THttpCallArg &arg)
526 {
527  if (arg.GetWSId() == 0)
528  return true;
529 
530  if (arg.IsMethod("WS_CONNECT")) {
531 
532  std::lock_guard<std::mutex> grd(fConnMutex);
533 
534  // refuse connection when number of connections exceed limit
535  if (fConnLimit && (fConn.size() >= fConnLimit))
536  return false;
537 
538  return true;
539  }
540 
541  if (arg.IsMethod("WS_READY")) {
542 
543  auto conn = FindOrCreateConnection(arg.GetWSId(), true, arg.GetQuery());
544 
545  if (conn) {
546  R__ERROR_HERE("webgui") << "WSHandle with given websocket id " << arg.GetWSId() << " already exists";
547  return false;
548  }
549 
550  return true;
551  }
552 
553  if (arg.IsMethod("WS_CLOSE")) {
554  // connection is closed, one can remove handle, associated window will be closed
555 
556  auto conn = RemoveConnection(arg.GetWSId());
557 
558  if (conn)
559  ProvideQueueEntry(conn->fConnId, kind_Disconnect, ""s);
560 
561  return true;
562  }
563 
564  if (!arg.IsMethod("WS_DATA")) {
565  R__ERROR_HERE("webgui") << "only WS_DATA request expected!";
566  return false;
567  }
568 
569  auto conn = FindConnection(arg.GetWSId());
570 
571  if (!conn) {
572  R__ERROR_HERE("webgui") << "Get websocket data without valid connection - ignore!!!";
573  return false;
574  }
575 
576  if (arg.GetPostDataLength() <= 0)
577  return true;
578 
579  // here processing of received data should be performed
580  // this is task for the implemented windows
581 
582  const char *buf = (const char *)arg.GetPostData();
583  char *str_end = nullptr;
584 
585  unsigned long ackn_oper = std::strtoul(buf, &str_end, 10);
586  if (!str_end || *str_end != ':') {
587  R__ERROR_HERE("webgui") << "missing number of acknowledged operations";
588  return false;
589  }
590 
591  unsigned long can_send = std::strtoul(str_end + 1, &str_end, 10);
592  if (!str_end || *str_end != ':') {
593  R__ERROR_HERE("webgui") << "missing can_send counter";
594  return false;
595  }
596 
597  unsigned long nchannel = std::strtoul(str_end + 1, &str_end, 10);
598  if (!str_end || *str_end != ':') {
599  R__ERROR_HERE("webgui") << "missing channel number";
600  return false;
601  }
602 
603  Long_t processed_len = (str_end + 1 - buf);
604 
605  if (processed_len > arg.GetPostDataLength()) {
606  R__ERROR_HERE("webgui") << "corrupted buffer";
607  return false;
608  }
609 
610  std::string cdata(str_end + 1, arg.GetPostDataLength() - processed_len);
611 
612  timestamp_t stamp = std::chrono::system_clock::now();
613 
614  {
615  std::lock_guard<std::mutex> grd(conn->fMutex);
616 
617  conn->fSendCredits += ackn_oper;
618  conn->fRecvCount++;
619  conn->fClientCredits = (int)can_send;
620  conn->fRecvStamp = stamp;
621  }
622 
623  if (fProtocolCnt >= 0)
624  if (!fProtocolConnId || (conn->fConnId == fProtocolConnId)) {
625  fProtocolConnId = conn->fConnId; // remember connection
626 
627  // record send event only for normal channel or very first message via ch0
628  if ((nchannel != 0) || (cdata.find("READY=") == 0)) {
629  if (fProtocol.length() > 2)
630  fProtocol.insert(fProtocol.length() - 1, ",");
631  fProtocol.insert(fProtocol.length() - 1, "\"send\"");
632 
633  std::ofstream pfs(fProtocolFileName);
634  pfs.write(fProtocol.c_str(), fProtocol.length());
635  pfs.close();
636  }
637  }
638 
639  if (nchannel == 0) {
640  // special system channel
641  if ((cdata.find("READY=") == 0) && !conn->fReady) {
642  std::string key = cdata.substr(6);
643 
644  if (key.empty() && IsNativeOnlyConn()) {
645  RemoveConnection(conn->fWSId);
646  return false;
647  }
648 
649  if (!key.empty() && !conn->fKey.empty() && (conn->fKey != key)) {
650  R__ERROR_HERE("webgui") << "Key mismatch after established connection " << key << " != " << conn->fKey;
651  RemoveConnection(conn->fWSId);
652  return false;
653  }
654 
655  if (!fPanelName.empty()) {
656  // initialization not yet finished, appropriate panel should be started
657  Send(conn->fConnId, "SHOWPANEL:"s + fPanelName);
658  conn->fReady = 5;
659  } else {
660  ProvideQueueEntry(conn->fConnId, kind_Connect, ""s);
661  conn->fReady = 10;
662  }
663  } else if (cdata.compare(0,8,"CLOSECH=") == 0) {
664  int channel = std::stoi(cdata.substr(8));
665  auto iter = conn->fEmbed.find(channel);
666  if (iter != conn->fEmbed.end()) {
667  iter->second->ProvideQueueEntry(conn->fConnId, kind_Disconnect, ""s);
668  conn->fEmbed.erase(iter);
669  }
670  }
671  } else if (fPanelName.length() && (conn->fReady < 10)) {
672  if (cdata == "PANEL_READY") {
673  R__DEBUG_HERE("webgui") << "Get panel ready " << fPanelName;
674  ProvideQueueEntry(conn->fConnId, kind_Connect, ""s);
675  conn->fReady = 10;
676  } else {
677  ProvideQueueEntry(conn->fConnId, kind_Disconnect, ""s);
678  RemoveConnection(conn->fWSId);
679  }
680  } else if (nchannel == 1) {
681  ProvideQueueEntry(conn->fConnId, kind_Data, std::move(cdata));
682  } else if (nchannel > 1) {
683  // process embed window
684  auto embed_window = conn->fEmbed[nchannel];
685  if (embed_window)
686  embed_window->ProvideQueueEntry(conn->fConnId, kind_Data, std::move(cdata));
687  }
688 
689  CheckDataToSend();
690 
691  return true;
692 }
693 
694 void ROOT::Experimental::RWebWindow::CompleteWSSend(unsigned wsid)
695 {
696  auto conn = FindConnection(wsid);
697 
698  if (!conn)
699  return;
700 
701  {
702  std::lock_guard<std::mutex> grd(conn->fMutex);
703  conn->fDoingSend = false;
704  }
705 
706  CheckDataToSend(conn);
707 }
708 
709 //////////////////////////////////////////////////////////////////////////////////////////
710 /// Prepare text part of send data
711 /// Should be called under locked connection mutex
712 
713 std::string ROOT::Experimental::RWebWindow::_MakeSendHeader(std::shared_ptr<WebConn> &conn, bool txt, const std::string &data, int chid)
714 {
715  std::string buf;
716 
717  if (!conn->fWSId || !fWSHandler) {
718  R__ERROR_HERE("webgui") << "try to send text data when connection not established";
719  return buf;
720  }
721 
722  if (conn->fSendCredits <= 0) {
723  R__ERROR_HERE("webgui") << "No credits to send text data via connection";
724  return buf;
725  }
726 
727  if (conn->fDoingSend) {
728  R__ERROR_HERE("webgui") << "Previous send operation not completed yet";
729  return buf;
730  }
731 
732  if (txt)
733  buf.reserve(data.length() + 100);
734 
735  buf.append(std::to_string(conn->fRecvCount));
736  buf.append(":");
737  buf.append(std::to_string(conn->fSendCredits));
738  buf.append(":");
739  conn->fRecvCount = 0; // we confirm how many packages was received
740  conn->fSendCredits--;
741 
742  buf.append(std::to_string(chid));
743  buf.append(":");
744 
745  if (txt) {
746  buf.append(data);
747  } else if (data.length()==0) {
748  buf.append("$$nullbinary$$");
749  } else {
750  buf.append("$$binary$$");
751  }
752 
753  return buf;
754 }
755 
756 //////////////////////////////////////////////////////////////////////////////////////////
757 /// Checks if one should send data for specified connection
758 /// Returns true when send operation was performed
759 
760 bool ROOT::Experimental::RWebWindow::CheckDataToSend(std::shared_ptr<WebConn> &conn)
761 {
762  std::string hdr, data;
763 
764  {
765  std::lock_guard<std::mutex> grd(conn->fMutex);
766 
767  if (!conn->fActive || (conn->fSendCredits <= 0) || conn->fDoingSend) return false;
768 
769  if (!conn->fQueue.empty()) {
770  QueueItem &item = conn->fQueue.front();
771  hdr = _MakeSendHeader(conn, item.fText, item.fData, item.fChID);
772  if (!hdr.empty() && !item.fText)
773  data = std::move(item.fData);
774  conn->fQueue.pop();
775  } else if ((conn->fClientCredits < 3) && (conn->fRecvCount > 1)) {
776  // give more credits to the client
777  hdr = _MakeSendHeader(conn, true, "KEEPALIVE", 0);
778  }
779 
780  if (hdr.empty()) return false;
781 
782  conn->fDoingSend = true;
783  }
784 
785  int res = 0;
786 
787  if (data.empty()) {
788  res = fWSHandler->SendCharStarWS(conn->fWSId, hdr.c_str());
789  } else {
790  res = fWSHandler->SendHeaderWS(conn->fWSId, hdr.c_str(), data.data(), data.length());
791  }
792 
793  // submit operation, will be processed
794  if (res >=0) return true;
795 
796 
797  // failure, clear sending flag
798  std::lock_guard<std::mutex> grd(conn->fMutex);
799  conn->fDoingSend = false;
800  return false;
801 }
802 
803 
804 //////////////////////////////////////////////////////////////////////////////////////////
805 /// Checks if new data can be send (internal use only)
806 /// If necessary, provide credits to the client
807 
808 void ROOT::Experimental::RWebWindow::CheckDataToSend(bool only_once)
809 {
810  // make copy of all connections to be independent later, only active connections are checked
811  auto arr = GetConnections(0, true);
812 
813  do {
814  bool isany = false;
815 
816  for (auto &conn : arr)
817  if (CheckDataToSend(conn))
818  isany = true;
819 
820  if (!isany) break;
821 
822  } while (!only_once);
823 }
824 
825 ///////////////////////////////////////////////////////////////////////////////////
826 /// Special method to process all internal activity when window runs in separate thread
827 
828 void ROOT::Experimental::RWebWindow::Sync()
829 {
830  InvokeCallbacks();
831 
832  CheckDataToSend();
833 
834  CheckPendingConnections();
835 
836  CheckInactiveConnections();
837 }
838 
839 ///////////////////////////////////////////////////////////////////////////////////
840 /// Returns window address which is used in URL
841 
842 std::string ROOT::Experimental::RWebWindow::GetAddr() const
843 {
844  return fWSHandler->GetName();
845 }
846 
847 ///////////////////////////////////////////////////////////////////////////////////
848 /// Returns relative URL address for the specified window
849 /// Address can be required if one needs to access data from one window into another window
850 /// Used for instance when inserting panel into canvas
851 
852 std::string ROOT::Experimental::RWebWindow::GetRelativeAddr(const std::shared_ptr<RWebWindow> &win) const
853 {
854  if (fMgr != win->fMgr) {
855  R__ERROR_HERE("WebDisplay") << "Same web window manager should be used";
856  return "";
857  }
858 
859  std::string res("../");
860  res.append(win->GetAddr());
861  res.append("/");
862  return res;
863 }
864 
865 /////////////////////////////////////////////////////////////////////////
866 /// Set client version, used as prefix in scripts URL
867 /// When changed, web browser will reload all related JS files while full URL will be different
868 /// Default is empty value - no extra string in URL
869 /// Version should be string like "1.2" or "ver1.subv2" and not contain any special symbols
870 
871 void ROOT::Experimental::RWebWindow::SetClientVersion(const std::string &vers)
872 {
873  std::lock_guard<std::mutex> grd(fConnMutex);
874  fClientVersion = vers;
875 }
876 
877 /////////////////////////////////////////////////////////////////////////
878 /// Returns current client version
879 
880 std::string ROOT::Experimental::RWebWindow::GetClientVersion() const
881 {
882  std::lock_guard<std::mutex> grd(fConnMutex);
883  return fClientVersion;
884 }
885 
886 /////////////////////////////////////////////////////////////////////////
887 /// Set arbitrary JSON code, which is accessible via conn.GetUserArgs() method
888 /// This JSON code injected into main HTML document into JSROOT.ConnectWebWindow()
889 /// Must be called before RWebWindow::Show() method is called
890 
891 void ROOT::Experimental::RWebWindow::SetUserArgs(const std::string &args)
892 {
893  std::lock_guard<std::mutex> grd(fConnMutex);
894  fUserArgs = args;
895 }
896 
897 /////////////////////////////////////////////////////////////////////////
898 /// Returns configured user arguments for web window
899 /// See \ref SetUserArgs method for more details
900 
901 std::string ROOT::Experimental::RWebWindow::GetUserArgs() const
902 {
903  std::lock_guard<std::mutex> grd(fConnMutex);
904  return fUserArgs;
905 }
906 
907 ///////////////////////////////////////////////////////////////////////////////////
908 /// Returns current number of active clients connections
909 
910 int ROOT::Experimental::RWebWindow::NumConnections(bool with_pending) const
911 {
912  std::lock_guard<std::mutex> grd(fConnMutex);
913  auto sz = fConn.size();
914  if (with_pending)
915  sz += fPendingConn.size();
916  return sz;
917 }
918 
919 ///////////////////////////////////////////////////////////////////////////////////
920 /// Configures recording of communication data in protocol file
921 /// Provided filename will be used to store JSON array with names of written files - text or binary
922 /// If data was send from client, "send" entry will be placed. JSON file will look like:
923 /// ["send","msg0.txt","send","msg1.txt","msg2.txt"]
924 /// If empty file name is provided, data recording will be disabled
925 /// Recorded data can be used in JSROOT directly to test client code without running C++ server
926 
927 void ROOT::Experimental::RWebWindow::RecordData(const std::string &fname, const std::string &fprefix)
928 {
929  fProtocolFileName = fname;
930  fProtocolCnt = fProtocolFileName.empty() ? -1 : 0;
931  fProtocolConnId = fProtocolFileName.empty() ? 0 : GetConnectionId(0);
932  fProtocolPrefix = fprefix;
933  fProtocol = "[]"; // empty array
934 }
935 
936 ///////////////////////////////////////////////////////////////////////////////////
937 /// Returns connection for specified connection number
938 /// Only active connections are returned - where clients confirms connection
939 /// Total number of connections can be retrieved with NumConnections() method
940 
941 unsigned ROOT::Experimental::RWebWindow::GetConnectionId(int num) const
942 {
943  std::lock_guard<std::mutex> grd(fConnMutex);
944  return ((num >= 0) && (num < (int)fConn.size()) && fConn[num]->fActive) ? fConn[num]->fConnId : 0;
945 }
946 
947 ///////////////////////////////////////////////////////////////////////////////////
948 /// returns true if specified connection id exists
949 /// connid is connection (0 - any)
950 /// if only_active==false, also inactive connections check or connections which should appear
951 
952 bool ROOT::Experimental::RWebWindow::HasConnection(unsigned connid, bool only_active) const
953 {
954  std::lock_guard<std::mutex> grd(fConnMutex);
955 
956  for (auto &conn : fConn) {
957  if (connid && (conn->fConnId != connid))
958  continue;
959  if (conn->fActive || !only_active)
960  return true;
961  }
962 
963  if (!only_active)
964  for (auto &conn : fPendingConn) {
965  if (!connid || (conn->fConnId == connid))
966  return true;
967  }
968 
969  return false;
970 }
971 
972 ///////////////////////////////////////////////////////////////////////////////////
973 /// Closes all connection to clients
974 /// Normally leads to closing of all correspondent browser windows
975 /// Some browsers (like firefox) do not allow by default to close window
976 
977 void ROOT::Experimental::RWebWindow::CloseConnections()
978 {
979  SubmitData(0, true, "CLOSE", 0);
980 }
981 
982 ///////////////////////////////////////////////////////////////////////////////////
983 /// Close specified connection
984 /// Connection id usually appears in the correspondent call-backs
985 
986 void ROOT::Experimental::RWebWindow::CloseConnection(unsigned connid)
987 {
988  if (connid)
989  SubmitData(connid, true, "CLOSE", 0);
990 }
991 
992 ///////////////////////////////////////////////////////////////////////////////////
993 /// returns connection (or all active connections)
994 
995 ROOT::Experimental::RWebWindow::ConnectionsList_t ROOT::Experimental::RWebWindow::GetConnections(unsigned connid, bool only_active) const
996 {
997  ConnectionsList_t arr;
998 
999  {
1000  std::lock_guard<std::mutex> grd(fConnMutex);
1001 
1002  for (auto &conn : fConn) {
1003  if ((conn->fActive || !only_active) && (!connid || (conn->fConnId == connid)))
1004  arr.push_back(conn);
1005  }
1006 
1007  if (!only_active)
1008  for (auto &conn : fPendingConn)
1009  if (!connid || (conn->fConnId == connid))
1010  arr.push_back(conn);
1011  }
1012 
1013  return arr;
1014 }
1015 
1016 ///////////////////////////////////////////////////////////////////////////////////
1017 /// returns true if sending via specified connection can be performed
1018 /// if direct==true, checks if direct sending (without queuing) is possible
1019 /// if connid==0, all existing connections are checked
1020 
1021 bool ROOT::Experimental::RWebWindow::CanSend(unsigned connid, bool direct) const
1022 {
1023  auto arr = GetConnections(connid, direct); // for direct sending connection has to be active
1024 
1025  auto maxqlen = GetMaxQueueLength();
1026 
1027  for (auto &conn : arr) {
1028 
1029  std::lock_guard<std::mutex> grd(conn->fMutex);
1030 
1031  if (direct && (!conn->fQueue.empty() || (conn->fSendCredits == 0) || conn->fDoingSend))
1032  return false;
1033 
1034  if (conn->fQueue.size() >= maxqlen)
1035  return false;
1036  }
1037 
1038  return true;
1039 }
1040 
1041 ///////////////////////////////////////////////////////////////////////////////////
1042 /// returns send queue length for specified connection
1043 /// if connid==0, maximal value for all connections is returned
1044 /// If wrong connection is specified, -1 is return
1045 
1046 int ROOT::Experimental::RWebWindow::GetSendQueueLength(unsigned connid) const
1047 {
1048  int maxq = -1;
1049 
1050  for (auto &conn : GetConnections(connid)) {
1051  std::lock_guard<std::mutex> grd(conn->fMutex);
1052  int len = conn->fQueue.size();
1053  if (len > maxq) maxq = len;
1054  }
1055 
1056  return maxq;
1057 }
1058 
1059 
1060 ///////////////////////////////////////////////////////////////////////////////////
1061 /// Internal method to send data
1062 /// Allows to specify channel. chid==1 is normal communication, chid==0 for internal with higher priority
1063 /// If connid==0, data will be send to all connections
1064 
1065 void ROOT::Experimental::RWebWindow::SubmitData(unsigned connid, bool txt, std::string &&data, int chid)
1066 {
1067  if (fMaster)
1068  return fMaster->SubmitData(fMasterConnId, txt, std::move(data), fMasterChannel);
1069 
1070  auto arr = GetConnections(connid);
1071  auto cnt = arr.size();
1072  auto maxqlen = GetMaxQueueLength();
1073 
1074  timestamp_t stamp = std::chrono::system_clock::now();
1075 
1076  for (auto &conn : arr) {
1077 
1078  if (fProtocolCnt >= 0)
1079  if (!fProtocolConnId || (conn->fConnId == fProtocolConnId)) {
1080  fProtocolConnId = conn->fConnId; // remember connection
1081  std::string fname = fProtocolPrefix;
1082  fname.append("msg");
1083  fname.append(std::to_string(fProtocolCnt++));
1084  fname.append(txt ? ".txt" : ".bin");
1085 
1086  std::ofstream ofs(fname);
1087  ofs.write(data.c_str(), data.length());
1088  ofs.close();
1089 
1090  if (fProtocol.length() > 2)
1091  fProtocol.insert(fProtocol.length() - 1, ",");
1092  fProtocol.insert(fProtocol.length() - 1, "\""s + fname + "\""s);
1093 
1094  std::ofstream pfs(fProtocolFileName);
1095  pfs.write(fProtocol.c_str(), fProtocol.length());
1096  pfs.close();
1097  }
1098 
1099  conn->fSendStamp = stamp;
1100 
1101  std::lock_guard<std::mutex> grd(conn->fMutex);
1102 
1103  if (conn->fQueue.size() < maxqlen) {
1104  if (--cnt)
1105  conn->fQueue.emplace(chid, txt, std::string(data)); // make copy
1106  else
1107  conn->fQueue.emplace(chid, txt, std::move(data)); // move content
1108  } else {
1109  R__ERROR_HERE("webgui") << "Maximum queue length achieved";
1110  }
1111  }
1112 
1113  CheckDataToSend();
1114 }
1115 
1116 ///////////////////////////////////////////////////////////////////////////////////
1117 /// Sends data to specified connection
1118 /// If connid==0, data will be send to all connections
1119 
1120 void ROOT::Experimental::RWebWindow::Send(unsigned connid, const std::string &data)
1121 {
1122  SubmitData(connid, true, std::string(data), 1);
1123 }
1124 
1125 ///////////////////////////////////////////////////////////////////////////////////
1126 /// Send binary data to specified connection
1127 /// If connid==0, data will be sent to all connections
1128 
1129 void ROOT::Experimental::RWebWindow::SendBinary(unsigned connid, std::string &&data)
1130 {
1131  SubmitData(connid, false, std::move(data), 1);
1132 }
1133 
1134 ///////////////////////////////////////////////////////////////////////////////////
1135 /// Send binary data to specified connection
1136 /// If connid==0, data will be sent to all connections
1137 
1138 void ROOT::Experimental::RWebWindow::SendBinary(unsigned connid, const void *data, std::size_t len)
1139 {
1140  std::string buf;
1141  buf.resize(len);
1142  std::copy((const char *)data, (const char *)data + len, buf.begin());
1143  SubmitData(connid, false, std::move(buf), 1);
1144 }
1145 
1146 ///////////////////////////////////////////////////////////////////////////////////
1147 /// Assign thread id which has to be used for callbacks
1148 
1149 void ROOT::Experimental::RWebWindow::AssignCallbackThreadId()
1150 {
1151  fCallbacksThrdIdSet = true;
1152  fCallbacksThrdId = std::this_thread::get_id();
1153  if (!RWebWindowsManager::IsMainThrd()) {
1154  fProcessMT = true;
1155  } else if (fMgr->IsUseHttpThread()) {
1156  // special thread is used by the manager, but main thread used for the canvas - not supported
1157  R__ERROR_HERE("webgui") << "create web window from main thread when THttpServer created with special thread - not supported";
1158  }
1159 }
1160 
1161 /////////////////////////////////////////////////////////////////////////////////
1162 /// Set call-back function for data, received from the clients via websocket
1163 ///
1164 /// Function should have signature like void func(unsigned connid, const std::string &data)
1165 /// First argument identifies connection (unique for each window), second argument is received data
1166 ///
1167 /// At the moment when callback is assigned, RWebWindow working thread is detected.
1168 /// If called not from main application thread, RWebWindow::Run() function must be regularly called from that thread.
1169 ///
1170 /// Most simple way to assign call-back - use of c++11 lambdas like:
1171 /// ~~~ {.cpp}
1172 /// auto win = RWebWindow::Create();
1173 /// win->SetDefaultPage("file:./page.htm");
1174 /// win->SetDataCallBack(
1175 /// [](unsigned connid, const std::string &data) {
1176 /// printf("Conn:%u data:%s\n", connid, data.c_str());
1177 /// }
1178 /// );
1179 /// win->Show();
1180 /// ~~~
1181 
1182 void ROOT::Experimental::RWebWindow::SetDataCallBack(WebWindowDataCallback_t func)
1183 {
1184  AssignCallbackThreadId();
1185  fDataCallback = func;
1186 }
1187 
1188 /////////////////////////////////////////////////////////////////////////////////
1189 /// Set call-back function for new connection
1190 
1191 void ROOT::Experimental::RWebWindow::SetConnectCallBack(WebWindowConnectCallback_t func)
1192 {
1193  AssignCallbackThreadId();
1194  fConnCallback = func;
1195 }
1196 
1197 /////////////////////////////////////////////////////////////////////////////////
1198 /// Set call-back function for disconnecting
1199 
1200 void ROOT::Experimental::RWebWindow::SetDisconnectCallBack(WebWindowConnectCallback_t func)
1201 {
1202  AssignCallbackThreadId();
1203  fDisconnCallback = func;
1204 }
1205 
1206 /////////////////////////////////////////////////////////////////////////////////
1207 /// Set call-backs function for connect, data and disconnect events
1208 
1209 void ROOT::Experimental::RWebWindow::SetCallBacks(WebWindowConnectCallback_t conn, WebWindowDataCallback_t data, WebWindowConnectCallback_t disconn)
1210 {
1211  AssignCallbackThreadId();
1212  fConnCallback = conn;
1213  fDataCallback = data;
1214  fDisconnCallback = disconn;
1215 }
1216 
1217 /////////////////////////////////////////////////////////////////////////////////
1218 /// Waits until provided check function or lambdas returns non-zero value
1219 /// Check function has following signature: int func(double spent_tm)
1220 /// Waiting will be continued, if function returns zero.
1221 /// Parameter spent_tm is time in seconds, which already spent inside the function
1222 /// First non-zero value breaks loop and result is returned.
1223 /// Runs application mainloop and short sleeps in-between
1224 
1225 int ROOT::Experimental::RWebWindow::WaitFor(WebWindowWaitFunc_t check)
1226 {
1227  return fMgr->WaitFor(*this, check);
1228 }
1229 
1230 /////////////////////////////////////////////////////////////////////////////////
1231 /// Waits until provided check function or lambdas returns non-zero value
1232 /// Check function has following signature: int func(double spent_tm)
1233 /// Waiting will be continued, if function returns zero.
1234 /// Parameter spent_tm in lambda is time in seconds, which already spent inside the function
1235 /// First non-zero value breaks waiting loop and result is returned (or 0 if time is expired).
1236 /// Runs application mainloop and short sleeps in-between
1237 /// WebGui.OperationTmout rootrc parameter defines waiting time in seconds
1238 
1239 int ROOT::Experimental::RWebWindow::WaitForTimed(WebWindowWaitFunc_t check)
1240 {
1241  return fMgr->WaitFor(*this, check, true, GetOperationTmout());
1242 }
1243 
1244 /////////////////////////////////////////////////////////////////////////////////
1245 /// Waits until provided check function or lambdas returns non-zero value
1246 /// Check function has following signature: int func(double spent_tm)
1247 /// Waiting will be continued, if function returns zero.
1248 /// Parameter spent_tm in lambda is time in seconds, which already spent inside the function
1249 /// First non-zero value breaks waiting loop and result is returned (or 0 if time is expired).
1250 /// Runs application mainloop and short sleeps in-between
1251 /// duration (in seconds) defines waiting time
1252 
1253 int ROOT::Experimental::RWebWindow::WaitForTimed(WebWindowWaitFunc_t check, double duration)
1254 {
1255  return fMgr->WaitFor(*this, check, true, duration);
1256 }
1257 
1258 
1259 /////////////////////////////////////////////////////////////////////////////////
1260 /// Run window functionality for specified time
1261 /// If no action can be performed - just sleep specified time
1262 
1263 void ROOT::Experimental::RWebWindow::Run(double tm)
1264 {
1265  if (!fCallbacksThrdIdSet || (fCallbacksThrdId != std::this_thread::get_id())) {
1266  R__WARNING_HERE("webgui") << "Change thread id where RWebWindow is executed";
1267  fCallbacksThrdIdSet = true;
1268  fCallbacksThrdId = std::this_thread::get_id();
1269  }
1270 
1271  if (tm <= 0) {
1272  Sync();
1273  } else {
1274  WaitForTimed([](double) { return 0; }, tm);
1275  }
1276 }
1277 
1278 
1279 /////////////////////////////////////////////////////////////////////////////////
1280 /// Add embed window
1281 
1282 unsigned ROOT::Experimental::RWebWindow::AddEmbedWindow(std::shared_ptr<RWebWindow> window, int channel)
1283 {
1284  if (channel < 2)
1285  return 0;
1286 
1287  auto arr = GetConnections(0, true);
1288  if (arr.size() == 0)
1289  return 0;
1290 
1291  // check if channel already occupied
1292  if (arr[0]->fEmbed.find(channel) != arr[0]->fEmbed.end())
1293  return 0;
1294 
1295  arr[0]->fEmbed[channel] = window;
1296 
1297  return arr[0]->fConnId;
1298 }
1299 
1300 /////////////////////////////////////////////////////////////////////////////////
1301 /// Remove RWebWindow associated with the channel
1302 
1303 void ROOT::Experimental::RWebWindow::RemoveEmbedWindow(unsigned connid, int channel)
1304 {
1305  auto arr = GetConnections(connid);
1306 
1307  for (auto &conn : arr) {
1308  auto iter = conn->fEmbed.find(channel);
1309  if (iter != conn->fEmbed.end())
1310  conn->fEmbed.erase(iter);
1311  }
1312 }
1313 
1314 
1315 /////////////////////////////////////////////////////////////////////////////////
1316 /// Create new RWebWindow
1317 /// Using default RWebWindowsManager
1318 
1319 std::shared_ptr<ROOT::Experimental::RWebWindow> ROOT::Experimental::RWebWindow::Create()
1320 {
1321  return ROOT::Experimental::RWebWindowsManager::Instance()->CreateWindow();
1322 }
1323 
1324 /////////////////////////////////////////////////////////////////////////////////
1325 /// Terminate ROOT session
1326 /// Tries to correctly close THttpServer, associated with RWebWindowsManager
1327 /// After that exit from process
1328 
1329 void ROOT::Experimental::RWebWindow::TerminateROOT()
1330 {
1331  fMgr->Terminate();
1332 }
1333 
1334 /////////////////////////////////////////////////////////////////////////////////
1335 /// Static method to show web window
1336 /// Has to be used instead of RWebWindow::Show() when window potentially can be embed into other windows
1337 /// Soon RWebWindow::Show() method will be done protected
1338 
1339 unsigned ROOT::Experimental::RWebWindow::ShowWindow(std::shared_ptr<RWebWindow> window, const RWebDisplayArgs &args)
1340 {
1341  if (!window)
1342  return 0;
1343 
1344  if (args.GetBrowserKind() == RWebDisplayArgs::kEmbedded) {
1345  unsigned connid = args.fMaster ? args.fMaster->AddEmbedWindow(window, args.fMasterChannel) : 0;
1346 
1347  if (connid > 0) {
1348  window->fMaster = args.fMaster;
1349  window->fMasterConnId = connid;
1350  window->fMasterChannel = args.fMasterChannel;
1351 
1352  // inform client that connection is established and window initialized
1353  args.fMaster->SubmitData(connid, true, "EMBED_DONE"s, args.fMasterChannel);
1354 
1355  // provide call back for window itself that connection is ready
1356  window->ProvideQueueEntry(connid, kind_Connect, ""s);
1357  }
1358 
1359  return connid;
1360  }
1361 
1362  return window->Show(args);
1363 }
1364