Logo ROOT   6.30.04
Reference Guide
 All Namespaces Files Pages
RWebWindowsManager.cxx
Go to the documentation of this file.
1 /// \file RWebWindowsManager.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 
17 
18 #include <ROOT/RLogger.hxx>
19 #include <ROOT/RWebDisplayArgs.hxx>
21 
22 #include "RWebWindowWSHandler.hxx"
23 
24 #include "THttpServer.h"
25 
26 #include "TSystem.h"
27 #include "TRandom.h"
28 #include "TString.h"
29 #include "TApplication.h"
30 #include "TTimer.h"
31 #include "TObjArray.h"
32 #include "TROOT.h"
33 #include "TEnv.h"
34 
35 #include <thread>
36 #include <chrono>
37 
38 
39 /** \class ROOT::Experimental::RWebWindowsManager
40 \ingroup webdisplay
41 
42 Central instance to create and show web-based windows like Canvas or FitPanel.
43 
44 Manager responsible to creating THttpServer instance, which is used for RWebWindow's
45 communication with clients.
46 
47 Method RWebWindowsManager::Show() used to show window in specified location.
48 */
49 
50 //////////////////////////////////////////////////////////////////////////////////////////
51 /// Returns default window manager
52 /// Used to display all standard ROOT elements like TCanvas or TFitPanel
53 
54 std::shared_ptr<ROOT::Experimental::RWebWindowsManager> &ROOT::Experimental::RWebWindowsManager::Instance()
55 {
56  static std::shared_ptr<RWebWindowsManager> sInstance = std::make_shared<ROOT::Experimental::RWebWindowsManager>();
57  return sInstance;
58 }
59 
60 //////////////////////////////////////////////////////////////////
61 /// This thread id used to identify main application thread, where ROOT event processing runs
62 /// To inject code in that thread, one should use TTimer (like THttpServer does)
63 /// In other threads special run methods have to be invoked like RWebWindow::Run()
64 ///
65 /// TODO: probably detection of main thread should be delivered by central ROOT instances like gApplication or gROOT
66 /// Main thread can only make sense if special processing runs there and one can inject own functionality there
67 
68 static std::thread::id gWebWinMainThrd = std::this_thread::get_id();
69 
70 //////////////////////////////////////////////////////////////////////////////////////////
71 /// Returns true when called from main process
72 /// Main process recognized at the moment when library is loaded
73 
74 bool ROOT::Experimental::RWebWindowsManager::IsMainThrd()
75 {
76  return std::this_thread::get_id() == gWebWinMainThrd;
77 }
78 
79 //////////////////////////////////////////////////////////////////////////////////////////
80 /// window manager constructor
81 /// Required here for correct usage of unique_ptr<THttpServer>
82 
83 ROOT::Experimental::RWebWindowsManager::RWebWindowsManager() = default;
84 
85 //////////////////////////////////////////////////////////////////////////////////////////
86 /// window manager destructor
87 /// Required here for correct usage of unique_ptr<THttpServer>
88 
89 ROOT::Experimental::RWebWindowsManager::~RWebWindowsManager()
90 {
91  if (gApplication && fServer && !fServer->IsTerminated()) {
92  gApplication->Disconnect("Terminate(Int_t)", fServer.get(), "SetTerminate()");
93  fServer->SetTerminate();
94  }
95 }
96 
97 //////////////////////////////////////////////////////////////////////////////////////////
98 /// Creates http server, if required - with real http engine (civetweb)
99 /// One could configure concrete HTTP port, which should be used for the server,
100 /// provide following entry in rootrc file:
101 ///
102 /// WebGui.HttpPort: 8088
103 ///
104 /// or specify range of http ports, which can be used:
105 ///
106 /// WebGui.HttpPortMin: 8800
107 /// WebGui.HttpPortMax: 9800
108 ///
109 /// By default range [8800..9800] is used
110 ///
111 /// One also can bind HTTP server socket to loopback address,
112 /// In that case only connection from localhost will be available:
113 ///
114 /// WebGui.HttpLoopback: yes
115 ///
116 /// Or one could specify hostname which should be used for binding of server socket
117 ///
118 /// WebGui.HttpBind: hostname | ipaddress
119 ///
120 /// To use secured protocol, following parameter should be specified
121 ///
122 /// WebGui.UseHttps: yes
123 /// WebGui.ServerCert: sertificate_filename.pem
124 ///
125 /// One also can configure usage of special thread of processing of http server requests
126 ///
127 /// WebGui.HttpThrd: no
128 ///
129 /// Extra threads can be used to send data to different clients via websocket (default no)
130 ///
131 /// WebGui.SenderThrds: no
132 ///
133 /// If required, one could change websocket timeouts (default is 10000 ms)
134 ///
135 /// WebGui.HttpWSTmout: 10000
136 ///
137 /// Following parameter controls browser max-age caching parameter for files (default 3600)
138 ///
139 /// WebGui.HttpMaxAge: 3600
140 
141 bool ROOT::Experimental::RWebWindowsManager::CreateServer(bool with_http)
142 {
143  // explicitly protect server creation
144  std::lock_guard<std::recursive_mutex> grd(fMutex);
145 
146  if (!fServer) {
147 
148  fServer = std::make_unique<THttpServer>("basic_sniffer");
149 
150  const char *serv_thrd = gEnv->GetValue("WebGui.HttpThrd", "");
151  if (serv_thrd && strstr(serv_thrd, "yes"))
152  fUseHttpThrd = true;
153  else if (serv_thrd && strstr(serv_thrd, "no"))
154  fUseHttpThrd = false;
155 
156  const char *send_thrds = gEnv->GetValue("WebGui.SenderThrds", "");
157  if (send_thrds && *send_thrds) {
158  if (strstr(send_thrds, "yes"))
159  fUseSenderThreads = true;
160  else if (strstr(send_thrds, "no"))
161  fUseSenderThreads = false;
162  else
163  R__ERROR_HERE("WebDisplay") << "WebGui.SenderThrds has to be yes or no";
164  }
165 
166  if (IsUseHttpThread())
167  fServer->CreateServerThread();
168 
169  if (gApplication)
170  gApplication->Connect("Terminate(Int_t)", "THttpServer", fServer.get(), "SetTerminate()");
171 
172 
173  // this is location where all ROOT UI5 sources are collected
174  // normally it is $ROOTSYS/ui5 or <prefix>/ui5 location
175  TString ui5dir = gSystem->Getenv("ROOTUI5SYS");
176  if (ui5dir.Length() == 0)
177  ui5dir = gEnv->GetValue("WebGui.RootUi5Path","");
178 
179  if (ui5dir.Length() == 0)
180  ui5dir.Form("%s/ui5", TROOT::GetDataDir().Data());
181 
182  if (gSystem->ExpandPathName(ui5dir)) {
183  R__ERROR_HERE("WebDisplay") << "Path to ROOT ui5 sources " << ui5dir << " not found, set ROOTUI5SYS correctly";
184  ui5dir = ".";
185  }
186 
187  fServer->AddLocation("rootui5sys/", ui5dir.Data());
188  }
189 
190  if (!with_http || !fAddr.empty())
191  return true;
192 
193  int http_port = gEnv->GetValue("WebGui.HttpPort", 0);
194  int http_min = gEnv->GetValue("WebGui.HttpPortMin", 8800);
195  int http_max = gEnv->GetValue("WebGui.HttpPortMax", 9800);
196  int http_wstmout = gEnv->GetValue("WebGui.HttpWSTmout", 10000);
197  int http_maxage = gEnv->GetValue("WebGui.HttpMaxAge", -1);
198  fLaunchTmout = gEnv->GetValue("WebGui.LaunchTmout", 30.);
199  const char *http_loopback = gEnv->GetValue("WebGui.HttpLoopback", "no");
200  const char *http_bind = gEnv->GetValue("WebGui.HttpBind", "");
201  const char *http_ssl = gEnv->GetValue("WebGui.UseHttps", "no");
202  const char *ssl_cert = gEnv->GetValue("WebGui.ServerCert", "rootserver.pem");
203 
204  bool assign_loopback = http_loopback && strstr(http_loopback, "yes");
205  bool use_secure = http_ssl && strstr(http_ssl, "yes");
206  int ntry = 100;
207 
208  if (http_port < 0) {
209  R__ERROR_HERE("WebDisplay") << "Not allowed to create real HTTP server, check WebGui.HttpPort variable";
210  return false;
211  }
212 
213  if (!http_port)
214  gRandom->SetSeed(0);
215 
216  if (http_max - http_min < ntry)
217  ntry = http_max - http_min;
218 
219  while (ntry-- >= 0) {
220  if (!http_port) {
221  if ((http_min <= 0) || (http_max <= http_min)) {
222  R__ERROR_HERE("WebDisplay") << "Wrong HTTP range configuration, check WebGui.HttpPortMin/Max variables";
223  return false;
224  }
225 
226  http_port = (int)(http_min + (http_max - http_min) * gRandom->Rndm(1));
227  }
228 
229  TString engine, url(use_secure ? "https://" : "http://");
230  engine.Form("%s:%d?websocket_timeout=%d", (use_secure ? "https" : "http"), http_port, http_wstmout);
231  if (assign_loopback) {
232  engine.Append("&loopback");
233  url.Append("localhost");
234  } else if (http_bind && (strlen(http_bind) > 0)) {
235  engine.Append("&bind=");
236  engine.Append(http_bind);
237  url.Append(http_bind);
238  } else {
239  url.Append("localhost");
240  }
241 
242  if (http_maxage >= 0)
243  engine.Append(TString::Format("&max_age=%d", http_maxage));
244 
245  if (use_secure) {
246  engine.Append("&ssl_cert=");
247  engine.Append(ssl_cert);
248  }
249 
250  if (fServer->CreateEngine(engine)) {
251  fAddr = url.Data();
252  fAddr.append(":");
253  fAddr.append(std::to_string(http_port));
254  return true;
255  }
256 
257  http_port = 0;
258  }
259 
260  return false;
261 }
262 
263 //////////////////////////////////////////////////////////////////////////////////////////
264 /// Creates new window
265 /// To show window, RWebWindow::Show() have to be called
266 
267 std::shared_ptr<ROOT::Experimental::RWebWindow> ROOT::Experimental::RWebWindowsManager::CreateWindow()
268 {
269 
270  // we book manager mutex for a longer operation, locked again in server creation
271  std::lock_guard<std::recursive_mutex> grd(fMutex);
272 
273  if (!CreateServer()) {
274  R__ERROR_HERE("WebDisplay") << "Cannot create server when creating window";
275  return nullptr;
276  }
277 
278  std::shared_ptr<ROOT::Experimental::RWebWindow> win = std::make_shared<ROOT::Experimental::RWebWindow>();
279 
280  if (!win) {
281  R__ERROR_HERE("WebDisplay") << "Fail to create RWebWindow instance";
282  return nullptr;
283  }
284 
285  double dflt_tmout = gEnv->GetValue("WebGui.OperationTmout", 50.);
286 
287  auto wshandler = win->CreateWSHandler(Instance(), ++fIdCnt, dflt_tmout);
288 
289  if (gEnv->GetValue("WebGui.RecordData", 0) > 0) {
290  std::string fname, prefix;
291  if (fIdCnt > 1) {
292  prefix = std::string("f") + std::to_string(fIdCnt) + "_";
293  fname = std::string("protcol") + std::to_string(fIdCnt) + ".json";
294  } else {
295  fname = "protocol.json";
296  }
297  win->RecordData(fname, prefix);
298  }
299 
300  fServer->RegisterWS(wshandler);
301 
302  return win;
303 }
304 
305 //////////////////////////////////////////////////////////////////////////////////////////
306 /// Release all references to specified window
307 /// Called from RWebWindow destructor
308 
309 void ROOT::Experimental::RWebWindowsManager::Unregister(ROOT::Experimental::RWebWindow &win)
310 {
311  if (win.fWSHandler)
312  fServer->UnregisterWS(win.fWSHandler);
313 }
314 
315 //////////////////////////////////////////////////////////////////////////
316 /// Provide URL address to access specified window from inside or from remote
317 
318 std::string ROOT::Experimental::RWebWindowsManager::GetUrl(const ROOT::Experimental::RWebWindow &win, bool remote)
319 {
320  if (!fServer) {
321  R__ERROR_HERE("WebDisplay") << "Server instance not exists when requesting window URL";
322  return "";
323  }
324 
325  std::string addr = "/";
326 
327  addr.append(win.fWSHandler->GetName());
328 
329  addr.append("/");
330 
331  if (remote) {
332  if (!CreateServer(true)) {
333  R__ERROR_HERE("WebDisplay") << "Fail to start real HTTP server when requesting URL";
334  return "";
335  }
336 
337  addr = fAddr + addr;
338  }
339 
340  return addr;
341 }
342 
343 ///////////////////////////////////////////////////////////////////////////////////////////////////
344 /// Show web window in specified location.
345 ///
346 /// \param batch_mode indicates that browser will run in headless mode
347 /// \param user_args specifies where and how display web window
348 ///
349 /// As display args one can use string like "firefox" or "chrome" - these are two main supported web browsers.
350 /// See RWebDisplayArgs::SetBrowserKind() for all available options. Default value for the browser can be configured
351 /// when starting root with --web argument like: "root --web=chrome"
352 ///
353 /// If allowed, same window can be displayed several times (like for TCanvas)
354 ///
355 /// Following parameters can be configured in rootrc file:
356 ///
357 /// WebGui.Chrome: full path to Google Chrome executable
358 /// WebGui.ChromeBatch: command to start chrome in batch
359 /// WebGui.ChromeInteractive: command to start chrome in interactive mode
360 /// WebGui.Firefox: full path to Mozialla Firefox executable
361 /// WebGui.FirefoxBatch: command to start Firefox in batch mode
362 /// WebGui.FirefoxInteractive: command to start Firefox in interactive mode
363 /// WebGui.FirefoxProfile: name of Firefox profile to use
364 /// WebGui.FirefoxProfilePath: file path to Firefox profile
365 /// WebGui.FirefoxRandomProfile: usage of random Firefox profile -1 never, 0 - only for batch mode (dflt), 1 - always
366 /// WebGui.LaunchTmout: time required to start process in seconds (default 30 s)
367 /// WebGui.OperationTmout: time required to perform WebWindow operation like execute command or update drawings
368 /// WebGui.RecordData: if specified enables data recording for each web window 0 - off, 1 - on
369 /// WebGui.JsonComp: compression factor for JSON conversion, if not specified - each widget uses own default values
370 /// WebGui.ForceHttp: 0 - off (default), 1 - always create real http server to run web window
371 /// WebGui.Console: -1 - output only console.error(), 0 - add console.warn(), 1 - add console.log() output
372 /// WebGui.openui5src: alternative location for openui5 like https://openui5.hana.ondemand.com/
373 /// WebGui.openui5libs: list of pre-loaded ui5 libs like sap.m, sap.ui.layout, sap.ui.unified
374 /// WebGui.openui5theme: openui5 theme like sap_belize (default) or sap_fiori_3
375 ///
376 /// HTTP-server related parameters documented in RWebWindowsManager::CreateServer() method
377 
378 unsigned ROOT::Experimental::RWebWindowsManager::ShowWindow(RWebWindow &win, bool batch_mode, const RWebDisplayArgs &user_args)
379 {
380  // silently ignore regular Show() calls in batch mode
381  if (!batch_mode && gROOT->IsWebDisplayBatch())
382  return 0;
383 
384  // for embedded window no any browser need to be started
385  if (user_args.GetBrowserKind() == RWebDisplayArgs::kEmbedded)
386  return 0;
387 
388  // we book manager mutex for a longer operation,
389  std::lock_guard<std::recursive_mutex> grd(fMutex);
390 
391  if (!fServer) {
392  R__ERROR_HERE("WebDisplay") << "Server instance not exists to show window";
393  return 0;
394  }
395 
396  std::string key;
397  int ntry = 100000;
398 
399  do {
400  key = std::to_string(gRandom->Integer(0x100000));
401  } while ((--ntry > 0) && win.HasKey(key));
402  if (ntry == 0) {
403  R__ERROR_HERE("WebDisplay") << "Fail to create unique key for the window";
404  return 0;
405  }
406 
407  RWebDisplayArgs args(user_args);
408 
409  if (batch_mode && !args.IsSupportHeadless()) {
410  R__ERROR_HERE("WebDisplay") << "Cannot use batch mode with " << args.GetBrowserName();
411  return 0;
412  }
413 
414  args.SetHeadless(batch_mode);
415  if (args.GetWidth() <= 0) args.SetWidth(win.GetWidth());
416  if (args.GetHeight() <= 0) args.SetHeight(win.GetHeight());
417 
418  bool normal_http = !args.IsLocalDisplay();
419  if (!normal_http && (gEnv->GetValue("WebGui.ForceHttp",0) == 1))
420  normal_http = true;
421 
422  std::string url = GetUrl(win, normal_http);
423  if (url.empty()) {
424  R__ERROR_HERE("WebDisplay") << "Cannot create URL for the window";
425  return 0;
426  }
427 
428  args.SetUrl(url);
429 
430  args.AppendUrlOpt(std::string("key=") + key);
431  if (batch_mode) args.AppendUrlOpt("batch_mode");
432 
433  if (!normal_http)
434  args.SetHttpServer(GetServer());
435 
436  auto handle = RWebDisplayHandle::Display(args);
437 
438  if (!handle) {
439  R__ERROR_HERE("WebDisplay") << "Cannot display window in " << args.GetBrowserName();
440  return 0;
441  }
442 
443  return win.AddDisplayHandle(batch_mode, key, handle);
444 }
445 
446 //////////////////////////////////////////////////////////////////////////
447 /// Waits until provided check function or lambdas returns non-zero value
448 /// Regularly calls WebWindow::Sync() method to let run event loop
449 /// If call from the main thread, runs system events processing
450 /// Check function has following signature: int func(double spent_tm)
451 /// Parameter spent_tm is time in seconds, which already spent inside function
452 /// Waiting will be continued, if function returns zero.
453 /// First non-zero value breaks waiting loop and result is returned (or 0 if time is expired).
454 /// If parameter timed is true, timelimit (in seconds) defines how long to wait
455 
456 int ROOT::Experimental::RWebWindowsManager::WaitFor(RWebWindow &win, WebWindowWaitFunc_t check, bool timed, double timelimit)
457 {
458  int res = 0;
459  int cnt = 0;
460  double spent = 0;
461 
462  auto start = std::chrono::high_resolution_clock::now();
463 
464  win.Sync(); // in any case call sync once to ensure
465 
466  while ((res = check(spent)) == 0) {
467 
468  if (IsMainThrd())
469  gSystem->ProcessEvents();
470 
471  win.Sync();
472 
473  std::this_thread::sleep_for(std::chrono::milliseconds(1));
474 
475  std::chrono::duration<double, std::milli> elapsed = std::chrono::high_resolution_clock::now() - start;
476 
477  spent = elapsed.count() * 1e-3; // use ms precision
478 
479  if (timed && (spent > timelimit))
480  return -3;
481 
482  cnt++;
483  }
484 
485  return res;
486 }
487 
488 //////////////////////////////////////////////////////////////////////////
489 /// Terminate http server and ROOT application
490 
491 void ROOT::Experimental::RWebWindowsManager::Terminate()
492 {
493  if (fServer)
494  fServer->SetTerminate();
495 
496  if (gApplication)
497  gApplication->Terminate();
498 }