Logo ROOT  
Reference Guide
 
Loading...
Searching...
No Matches
RWebDisplayHandle.cxx
Go to the documentation of this file.
1// Author: Sergey Linev <s.linev@gsi.de>
2// Date: 2018-10-17
3// Warning: This is part of the ROOT 7 prototype! It will change without notice. It might trigger earthquakes. Feedback is welcome!
4
5/*************************************************************************
6 * Copyright (C) 1995-2019, Rene Brun and Fons Rademakers. *
7 * All rights reserved. *
8 * *
9 * For the licensing terms see $ROOTSYS/LICENSE. *
10 * For the list of contributors see $ROOTSYS/README/CREDITS. *
11 *************************************************************************/
12
14
15#include <ROOT/RLogger.hxx>
16
17#include "RConfigure.h"
18#include "TSystem.h"
19#include "TRandom3.h"
20#include "TString.h"
21#include "TObjArray.h"
22#include "THttpServer.h"
23#include "TEnv.h"
24#include "TError.h"
25#include "TROOT.h"
26#include "TBase64.h"
27#include "TBufferJSON.h"
29
30#include <fstream>
31#include <iostream>
32#include <filesystem>
33#include <memory>
34#include <regex>
35
36#ifdef _MSC_VER
37#include <process.h>
38#else
39#include <unistd.h>
40#include <cstdlib>
41#include <csignal>
42#include <spawn.h>
43#ifdef R__MACOSX
44#include <sys/wait.h>
45#include <crt_externs.h>
46#elif defined(__FreeBSD__)
47#include <sys/wait.h>
48#include <dlfcn.h>
49#else
50#include <wait.h>
51#endif
52#endif
53
54using namespace ROOT;
55using namespace std::string_literals;
56
57/** \class ROOT::RWebDisplayHandle
58\ingroup webdisplay
59
60Handle of created web-based display
61Depending from type of web display, holds handle of started browser process or other display-specific information
62to correctly stop and cleanup display.
63*/
64
65
66//////////////////////////////////////////////////////////////////////////////////////////////////
67/// Static holder of registered creators of web displays
68
69std::map<std::string, std::unique_ptr<RWebDisplayHandle::Creator>> &RWebDisplayHandle::GetMap()
70{
71 static std::map<std::string, std::unique_ptr<RWebDisplayHandle::Creator>> sMap;
72 return sMap;
73}
74
75//////////////////////////////////////////////////////////////////////////////////////////////////
76/// Search for specific browser creator
77/// If not found, try to add one
78/// \param name - creator name like ChromeCreator
79/// \param libname - shared library name where creator could be provided
80
81std::unique_ptr<RWebDisplayHandle::Creator> &RWebDisplayHandle::FindCreator(const std::string &name, const std::string &libname)
82{
83 auto &m = GetMap();
84 auto search = m.find(name);
85 if (search == m.end()) {
86
87 if (libname == "ChromeCreator") {
88 m.emplace(name, std::make_unique<ChromeCreator>(name == "edge"));
89 } else if (libname == "FirefoxCreator") {
90 m.emplace(name, std::make_unique<FirefoxCreator>());
91 } else if (libname == "SafariCreator") {
92 m.emplace(name, std::make_unique<SafariCreator>());
93 } else if (libname == "BrowserCreator") {
94 m.emplace(name, std::make_unique<BrowserCreator>(false));
95 } else if (!libname.empty()) {
96 gSystem->Load(libname.c_str());
97 }
98
99 search = m.find(name); // try again
100 }
101
102 if (search != m.end())
103 return search->second;
104
105 static std::unique_ptr<RWebDisplayHandle::Creator> dummy;
106 return dummy;
107}
108
109namespace ROOT {
110
111//////////////////////////////////////////////////////////////////////////////////////////////////
112/// Specialized handle to hold information about running browser process
113/// Used to correctly cleanup all processes and temporary directories
114
116
117#ifdef _MSC_VER
118 typedef int browser_process_id;
119#else
120 typedef pid_t browser_process_id;
121#endif
122 std::string fTmpDir; ///< temporary directory to delete at the end
123 std::string fTmpFile; ///< temporary file to remove
124 bool fHasPid{false};
126
127public:
128 RWebBrowserHandle(const std::string &url, const std::string &tmpdir, const std::string &tmpfile,
129 const std::string &dump)
130 : RWebDisplayHandle(url), fTmpDir(tmpdir), fTmpFile(tmpfile)
131 {
132 SetContent(dump);
133 }
134
135 RWebBrowserHandle(const std::string &url, const std::string &tmpdir, const std::string &tmpfile,
137 : RWebDisplayHandle(url), fTmpDir(tmpdir), fTmpFile(tmpfile), fHasPid(true), fPid(pid)
138 {
139 }
140
142 {
143#ifdef _MSC_VER
144 if (fHasPid)
145 gSystem->Exec(("taskkill /F /PID " + std::to_string(fPid) + " >NUL 2>NUL").c_str());
146 std::string rmdir = "rmdir /S /Q ";
147#else
148 if (fHasPid)
149 kill(fPid, SIGKILL);
150 std::string rmdir = "rm -rf ";
151#endif
152 if (!fTmpDir.empty())
153 gSystem->Exec((rmdir + fTmpDir).c_str());
155 }
156
157 void RemoveStartupFiles() override
158 {
159#ifdef _MSC_VER
160 std::string rmfile = "del /F ";
161#else
162 std::string rmfile = "rm -f ";
163#endif
164 if (!fTmpFile.empty()) {
165 gSystem->Exec((rmfile + fTmpFile).c_str());
166 fTmpFile.clear();
167 }
168 }
169};
170
171} // namespace ROOT
172
173//////////////////////////////////////////////////////////////////////////////////////////////////
174/// Class to handle starting of web-browsers like Chrome or Firefox
175
176RWebDisplayHandle::BrowserCreator::BrowserCreator(bool custom, const std::string &exec)
177{
178 if (custom) return;
179
180 if (!exec.empty()) {
181 if (exec.find("$url") == std::string::npos) {
182 fProg = exec;
183#ifdef _MSC_VER
184 fExec = exec + " $url";
185#else
186 fExec = exec + " $url &";
187#endif
188 } else {
189 fExec = exec;
190 auto pos = exec.find(" ");
191 if (pos != std::string::npos)
192 fProg = exec.substr(0, pos);
193 }
194 } else if (gSystem->InheritsFrom("TMacOSXSystem")) {
195 fExec = "open \'$url\'";
196 } else if (gSystem->InheritsFrom("TWinNTSystem")) {
197 fExec = "start $url";
198 } else {
199 fExec = "xdg-open \'$url\' &";
200 }
201}
202
203//////////////////////////////////////////////////////////////////////////////////////////////////
204/// Check if browser executable exists and can be used
205
206void RWebDisplayHandle::BrowserCreator::TestProg(const std::string &nexttry, bool check_std_paths)
207{
208 if (nexttry.empty() || !fProg.empty())
209 return;
210
211 if (!gSystem->AccessPathName(nexttry.c_str(), kExecutePermission)) {
212#ifdef R__MACOSX
213 fProg = std::regex_replace(nexttry, std::regex("%20"), " ");
214#else
215 fProg = nexttry;
216#endif
217 return;
218 }
219
220 if (!check_std_paths)
221 return;
222
223#ifdef _MSC_VER
224 std::string ProgramFiles = gSystem->Getenv("ProgramFiles");
225 auto pos = ProgramFiles.find(" (x86)");
226 if (pos != std::string::npos)
227 ProgramFiles.erase(pos, 6);
228 std::string ProgramFilesx86 = gSystem->Getenv("ProgramFiles(x86)");
229
230 if (!ProgramFiles.empty())
231 TestProg(ProgramFiles + nexttry, false);
232 if (!ProgramFilesx86.empty())
233 TestProg(ProgramFilesx86 + nexttry, false);
234#endif
235}
236
237//////////////////////////////////////////////////////////////////////////////////////////////////
238/// Create temporary file for web display
239/// Normally gSystem->TempFileName() method used to create file in default temporary directory
240/// For snap chromium use of default temp directory is not always possible therefore one switches to home directory
241/// But one checks if default temp directory modified and already points to /home folder
242
243FILE *RWebDisplayHandle::BrowserCreator::TemporaryFile(TString &name, int use_home_dir, const char *suffix)
244{
245 std::string dirname;
246 if (use_home_dir > 0) {
247 if (use_home_dir == 1) {
248 const char *tmp_dir = gSystem->TempDirectory();
249 if (tmp_dir && (strncmp(tmp_dir, "/home", 5) == 0))
250 use_home_dir = 0;
251 else if (!tmp_dir || (strncmp(tmp_dir, "/tmp", 4) == 0))
252 use_home_dir = 2;
253 }
254
255 if (use_home_dir > 1)
256 dirname = gSystem->GetHomeDirectory();
257 }
258 return gSystem->TempFileName(name, use_home_dir > 1 ? dirname.c_str() : nullptr, suffix);
259}
260
261static void DummyTimeOutHandler(int /* Sig */) {}
262
263
264//////////////////////////////////////////////////////////////////////////////////////////////////
265/// Display given URL in web browser
266/// \note See more details related to webdisplay on RWebWindowsManager::ShowWindow
267
268std::unique_ptr<RWebDisplayHandle>
270{
271 std::string url = args.GetFullUrl();
272 if (url.empty())
273 return nullptr;
274
276 std::cout << "New web window: " << url << std::endl;
277 return std::make_unique<RWebBrowserHandle>(url, "", "", "");
278 }
279
280 std::string exec;
281 if (args.IsBatchMode())
282 exec = fBatchExec;
283 else if (args.IsHeadless())
284 exec = fHeadlessExec;
285 else if (args.IsStandalone())
286 exec = fExec;
287 else
288 exec = "$prog $url &";
289
290 if (exec.empty())
291 return nullptr;
292
293 std::string swidth = std::to_string(args.GetWidth() > 0 ? args.GetWidth() : 800),
294 sheight = std::to_string(args.GetHeight() > 0 ? args.GetHeight() : 600),
295 sposx = std::to_string(args.GetX() >= 0 ? args.GetX() : 0),
296 sposy = std::to_string(args.GetY() >= 0 ? args.GetY() : 0);
297
298 ProcessGeometry(exec, args);
299
300 std::string extra = args.GetExtraArgs();
301 if (!extra.empty()) {
302 auto p = exec.find("$url");
303 if (p != std::string::npos)
304 exec.insert(p, extra + " ");
305 }
306
307 std::string rmdir = MakeProfile(exec, args.IsBatchMode() || args.IsHeadless());
308
309 std::string tmpfile;
310
311 // these are secret parameters, hide them in temp file
312 if (((url.find("token=") != std::string::npos) || (url.find("key=") != std::string::npos)) && !args.IsBatchMode() && !args.IsHeadless()) {
313 TString filebase = "root_start_";
314
315 auto f = TemporaryFile(filebase, IsSnapBrowser() ? 1 : 0, ".html");
316
317 bool ferr = false;
318
319 if (!f) {
320 ferr = true;
321 } else {
322 std::string content = std::regex_replace(
323 "<!DOCTYPE html>\n"
324 "<html lang=\"en\">\n"
325 "<head>\n"
326 " <meta charset=\"utf-8\">\n"
327 " <meta http-equiv=\"refresh\" content=\"0;url=$url\"/>\n"
328 " <title>Opening ROOT widget</title>\n"
329 "</head>\n"
330 "<body>\n"
331 "<p>\n"
332 " This page should redirect you to a ROOT widget. If it doesn't,\n"
333 " <a href=\"$url\">click here to go to ROOT</a>.\n"
334 "</p>\n"
335 "</body>\n"
336 "</html>\n", std::regex("\\$url"), url);
337
338 if (fwrite(content.c_str(), 1, content.length(), f) != content.length())
339 ferr = true;
340
341 if (fclose(f) != 0)
342 ferr = true;
343
344 tmpfile = filebase.Data();
345
346 url = "file://"s + tmpfile;
347 }
348
349 if (ferr) {
350 if (!tmpfile.empty())
351 gSystem->Unlink(tmpfile.c_str());
352 R__LOG_ERROR(WebGUILog()) << "Fail to create temporary HTML file to startup widget";
353 return nullptr;
354 }
355 }
356
357 exec = std::regex_replace(exec, std::regex("\\$rootetcdir"), TROOT::GetEtcDir().Data());
358 exec = std::regex_replace(exec, std::regex("\\$url"), url);
359 exec = std::regex_replace(exec, std::regex("\\$width"), swidth);
360 exec = std::regex_replace(exec, std::regex("\\$height"), sheight);
361 exec = std::regex_replace(exec, std::regex("\\$posx"), sposx);
362 exec = std::regex_replace(exec, std::regex("\\$posy"), sposy);
363
364 if (exec.compare(0,5,"fork:") == 0) {
365 if (fProg.empty()) {
366 if (!tmpfile.empty())
367 gSystem->Unlink(tmpfile.c_str());
368 R__LOG_ERROR(WebGUILog()) << "Fork instruction without executable";
369 return nullptr;
370 }
371
372 exec.erase(0, 5);
373
374 // in case of redirection process will wait until output is produced
375 std::string redirect = args.GetRedirectOutput();
376
377#ifndef _MSC_VER
378
379 std::unique_ptr<TObjArray> fargs(TString(exec.c_str()).Tokenize(" "));
380 if (!fargs || (fargs->GetLast()<=0)) {
381 if (!tmpfile.empty())
382 gSystem->Unlink(tmpfile.c_str());
383 R__LOG_ERROR(WebGUILog()) << "Fork instruction is empty";
384 return nullptr;
385 }
386
387 std::vector<char *> argv;
388 argv.push_back((char *) fProg.c_str());
389 for (Int_t n = 0; n <= fargs->GetLast(); ++n)
390 argv.push_back((char *)fargs->At(n)->GetName());
391 argv.push_back(nullptr);
392
393 R__LOG_DEBUG(0, WebGUILog()) << "Show web window in browser with posix_spawn:\n" << fProg << " " << exec;
394
395 posix_spawn_file_actions_t action;
396 posix_spawn_file_actions_init(&action);
397 if (redirect.empty())
398 posix_spawn_file_actions_addopen(&action, STDOUT_FILENO, "/dev/null", O_WRONLY|O_APPEND, 0);
399 else
400 posix_spawn_file_actions_addopen(&action, STDOUT_FILENO, redirect.c_str(), O_WRONLY|O_CREAT, 0600);
401 posix_spawn_file_actions_addopen(&action, STDERR_FILENO, "/dev/null", O_WRONLY|O_APPEND, 0);
402
403#ifdef R__MACOSX
404 char **envp = *_NSGetEnviron();
405#elif defined (__FreeBSD__)
406 //this is needed because the FreeBSD linker does not like to resolve these special symbols
407 //in shared libs with -Wl,--no-undefined
408 char** envp = (char**)dlsym(RTLD_DEFAULT, "environ");
409#else
410 char **envp = environ;
411#endif
412
413 pid_t pid;
414 int status = posix_spawn(&pid, argv[0], &action, nullptr, argv.data(), envp);
415
416 posix_spawn_file_actions_destroy(&action);
417
418 if (status != 0) {
419 if (!tmpfile.empty())
420 gSystem->Unlink(tmpfile.c_str());
421 R__LOG_ERROR(WebGUILog()) << "Fail to launch " << argv[0];
422 return nullptr;
423 }
424
425 if (!redirect.empty()) {
426 Int_t batch_timeout = gEnv->GetValue("WebGui.BatchTimeout", 30);
427 struct sigaction Act, Old;
428 int elapsed_time = 0;
429
430 if (batch_timeout) {
431 memset(&Act, 0, sizeof(Act));
432 Act.sa_handler = DummyTimeOutHandler;
433 sigemptyset(&Act.sa_mask);
434 sigaction(SIGALRM, &Act, &Old);
435 int alarm_timeout = batch_timeout > 3 ? 3 : batch_timeout;
436 alarm(alarm_timeout);
437 elapsed_time = alarm_timeout;
438 }
439
440 int job_done = 0;
441 std::string dump_content;
442
443 while (!job_done) {
444
445 // wait until output is produced
446 int wait_status = 0;
447
448 auto wait_res = waitpid(pid, &wait_status, WUNTRACED | WCONTINUED);
449
450 // try read dump anyway
451 dump_content = THttpServer::ReadFileContent(redirect.c_str());
452
453 if (dump_content.find("<div>###batch###job###done###</div>") != std::string::npos)
454 job_done = 1;
455
456 if (wait_res == -1) {
457 // failure when finish process
458 int alarm_timeout = batch_timeout - elapsed_time;
459 if ((errno == EINTR) && (alarm_timeout > 0) && !job_done) {
460 if (alarm_timeout > 2) alarm_timeout = 2;
461 elapsed_time += alarm_timeout;
462 alarm(alarm_timeout);
463 } else {
464 // end of timeout - do not try to wait any longer
465 job_done = 1;
466 }
467 } else if (!WIFEXITED(wait_status) && !WIFSIGNALED(wait_status)) {
468 // abnormal end of browser process
469 job_done = 1;
470 } else {
471 // this is normal finish, no need for process kill
472 job_done = 2;
473 }
474 }
475
476 if (job_done != 2) {
477 // kill browser process when no normal end was detected
478 kill(pid, SIGKILL);
479 }
480
481 if (batch_timeout) {
482 alarm(0); // disable alarm
483 sigaction(SIGALRM, &Old, nullptr);
484 }
485
486 if (gEnv->GetValue("WebGui.PreserveBatchFiles", -1) > 0)
487 ::Info("RWebDisplayHandle::Display", "Preserve dump file %s", redirect.c_str());
488 else
489 gSystem->Unlink(redirect.c_str());
490
491 return std::make_unique<RWebBrowserHandle>(url, rmdir, tmpfile, dump_content);
492 }
493
494 // add processid and rm dir
495
496 return std::make_unique<RWebBrowserHandle>(url, rmdir, tmpfile, pid);
497
498#else
499
500 if (fProg.empty()) {
501 if (!tmpfile.empty())
502 gSystem->Unlink(tmpfile.c_str());
503 R__LOG_ERROR(WebGUILog()) << "No Web browser found";
504 return nullptr;
505 }
506
507 // use UnixPathName to simplify handling of backslashes
508 exec = "wmic process call create '"s + gSystem->UnixPathName(fProg.c_str()) + " " + exec + "' | find \"ProcessId\" "s;
509 std::string process_id = gSystem->GetFromPipe(exec.c_str()).Data();
510 std::stringstream ss(process_id);
511 std::string tmp;
512 char c;
513 int pid = 0;
514 ss >> tmp >> c >> pid;
515
516 if (pid <= 0) {
517 if (!tmpfile.empty())
518 gSystem->Unlink(tmpfile.c_str());
519 R__LOG_ERROR(WebGUILog()) << "Fail to launch " << fProg;
520 return nullptr;
521 }
522
523 // add processid and rm dir
524 return std::make_unique<RWebBrowserHandle>(url, rmdir, tmpfile, pid);
525#endif
526 }
527
528#ifdef _MSC_VER
529
530 if (exec.rfind("&") == exec.length() - 1) {
531
532 // if last symbol is &, use _spawn to detach execution
533 exec.resize(exec.length() - 1);
534
535 std::vector<char *> argv;
536 std::string firstarg = fProg;
537 auto slashpos = firstarg.find_last_of("/\\");
538 if (slashpos != std::string::npos)
539 firstarg.erase(0, slashpos + 1);
540 argv.push_back((char *)firstarg.c_str());
541
542 std::unique_ptr<TObjArray> fargs(TString(exec.c_str()).Tokenize(" "));
543 for (Int_t n = 1; n <= fargs->GetLast(); ++n)
544 argv.push_back((char *)fargs->At(n)->GetName());
545 argv.push_back(nullptr);
546
547 R__LOG_DEBUG(0, WebGUILog()) << "Showing web window in " << fProg << " with:\n" << exec;
548
549 _spawnv(_P_NOWAIT, gSystem->UnixPathName(fProg.c_str()), argv.data());
550
551 return std::make_unique<RWebBrowserHandle>(url, rmdir, tmpfile, ""s);
552 }
553
554 std::string prog = "\""s + gSystem->UnixPathName(fProg.c_str()) + "\""s;
555
556#else
557
558#ifdef R__MACOSX
559 std::string prog = std::regex_replace(fProg, std::regex(" "), "\\ ");
560#else
561 std::string prog = fProg;
562#endif
563
564#endif
565
566 exec = std::regex_replace(exec, std::regex("\\$prog"), prog);
567
568 std::string redirect = args.GetRedirectOutput(), dump_content;
569
570 if (!redirect.empty()) {
571 if (exec.find("$dumpfile") != std::string::npos) {
572 exec = std::regex_replace(exec, std::regex("\\$dumpfile"), redirect);
573 } else {
574 auto p = exec.length();
575 if (exec.rfind("&") == p-1) --p;
576 exec.insert(p, " >"s + redirect + " "s);
577 }
578 }
579
580 R__LOG_DEBUG(0, WebGUILog()) << "Showing web window in browser with:\n" << exec;
581
582 gSystem->Exec(exec.c_str());
583
584 // read content of redirected output
585 if (!redirect.empty()) {
586 dump_content = THttpServer::ReadFileContent(redirect.c_str());
587
588 if (gEnv->GetValue("WebGui.PreserveBatchFiles", -1) > 0)
589 ::Info("RWebDisplayHandle::Display", "Preserve dump file %s", redirect.c_str());
590 else
591 gSystem->Unlink(redirect.c_str());
592 }
593
594 return std::make_unique<RWebBrowserHandle>(url, rmdir, tmpfile, dump_content);
595}
596
597//////////////////////////////////////////////////////////////////////////////////////////////////
598/// Constructor
599
601{
602 fExec = gEnv->GetValue("WebGui.SafariInteractive", "open -a Safari $url");
603}
604
605//////////////////////////////////////////////////////////////////////////////////////////////////
606/// Returns true if it can be used
607
609{
610#ifdef R__MACOSX
611 return true;
612#else
613 return false;
614#endif
615}
616
617//////////////////////////////////////////////////////////////////////////////////////////////////
618/// Constructor
619
621{
622 fEdge = _edge;
623
624 fEnvPrefix = fEdge ? "WebGui.Edge" : "WebGui.Chrome";
625
626 TestProg(gEnv->GetValue(fEnvPrefix.c_str(), ""));
627
628 if (!fProg.empty() && !fEdge)
629 fChromeVersion = gEnv->GetValue("WebGui.ChromeVersion", -1);
630
631#ifdef _MSC_VER
632 if (fEdge)
633 TestProg("\\Microsoft\\Edge\\Application\\msedge.exe", true);
634 else
635 TestProg("\\Google\\Chrome\\Application\\chrome.exe", true);
636#endif
637#ifdef R__MACOSX
638 TestProg("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome");
639#endif
640#ifdef R__LINUX
641 TestProg("/snap/bin/chromium"); // test snap before to detect it properly
642 TestProg("/usr/bin/chromium");
643 TestProg("/usr/bin/chromium-browser");
644 TestProg("/usr/bin/chrome-browser");
645 TestProg("/usr/bin/google-chrome-stable");
646 TestProg("/usr/bin/google-chrome");
647#endif
648
649// --no-sandbox is required to run chrome with super-user, but only in headless mode
650// --headless=new was used when both old and new were available, but old was removed from chrome 132, see https://developer.chrome.com/blog/removing-headless-old-from-chrome
651
652#ifdef _MSC_VER
653 // here --headless=old was used to let normally end of Edge process when --dump-dom is used
654 // while on Windows chrome and edge version not tested, just suppose that newest chrome is used
655 fBatchExec = gEnv->GetValue((fEnvPrefix + "Batch").c_str(), "$prog --headless --no-sandbox $geometry --dump-dom $url");
656 // in interactive headless mode fork used to let stop browser via process id
657 fHeadlessExec = gEnv->GetValue((fEnvPrefix + "Headless").c_str(), "fork:--headless --no-sandbox --disable-gpu $geometry \"$url\"");
658 fExec = gEnv->GetValue((fEnvPrefix + "Interactive").c_str(), "$prog $geometry --new-window --app=$url &"); // & in windows mean usage of spawn
659#else
660#ifdef R__MACOSX
661 bool use_normal = true; // mac does not like new flag
662#else
663 bool use_normal = (fChromeVersion < 119) || (fChromeVersion > 131);
664#endif
665 if (use_normal) {
666 // old or newest browser with standard headless mode
667 fBatchExec = gEnv->GetValue((fEnvPrefix + "Batch").c_str(), "fork:--headless --no-sandbox --disable-extensions --disable-audio-output $geometry --dump-dom $url");
668 fHeadlessExec = gEnv->GetValue((fEnvPrefix + "Headless").c_str(), "fork:--headless --no-sandbox --disable-extensions --disable-audio-output $geometry $url");
669 } else {
670 // newer version with headless=new mode
671 fBatchExec = gEnv->GetValue((fEnvPrefix + "Batch").c_str(), "fork:--headless=new --no-sandbox --disable-extensions --disable-audio-output $geometry --dump-dom $url");
672 fHeadlessExec = gEnv->GetValue((fEnvPrefix + "Headless").c_str(), "fork:--headless=new --no-sandbox --disable-extensions --disable-audio-output $geometry $url");
673 }
674 fExec = gEnv->GetValue((fEnvPrefix + "Interactive").c_str(), "$prog $geometry --new-window --app=\'$url\' >/dev/null 2>/dev/null &");
675#endif
676}
677
678
679//////////////////////////////////////////////////////////////////////////////////////////////////
680/// Replace $geometry placeholder with geometry settings
681/// Also RWebDisplayArgs::GetExtraArgs() are appended
682
684{
685 std::string geometry;
686 if ((args.GetWidth() > 0) && (args.GetHeight() > 0))
687 geometry = "--window-size="s + std::to_string(args.GetWidth())
688 + (args.IsHeadless() ? "x"s : ","s)
689 + std::to_string(args.GetHeight());
690
691 if (((args.GetX() >= 0) || (args.GetY() >= 0)) && !args.IsHeadless()) {
692 if (!geometry.empty()) geometry.append(" ");
693 geometry.append("--window-position="s + std::to_string(args.GetX() >= 0 ? args.GetX() : 0) + ","s +
694 std::to_string(args.GetY() >= 0 ? args.GetY() : 0));
695 }
696
697 exec = std::regex_replace(exec, std::regex("\\$geometry"), geometry);
698}
699
700
701//////////////////////////////////////////////////////////////////////////////////////////////////
702/// Handle profile argument
703
704std::string RWebDisplayHandle::ChromeCreator::MakeProfile(std::string &exec, bool)
705{
706 std::string rmdir, profile_arg;
707
708 if (exec.find("$profile") == std::string::npos)
709 return rmdir;
710
711 const char *chrome_profile = gEnv->GetValue((fEnvPrefix + "Profile").c_str(), "");
712 if (chrome_profile && *chrome_profile) {
713 profile_arg = chrome_profile;
714 } else {
715 TRandom3 rnd;
716 rnd.SetSeed(0);
717 profile_arg = gSystem->TempDirectory();
718 if ((profile_arg.compare(0, 4, "/tmp") == 0) && IsSnapBrowser())
719 profile_arg = gSystem->GetHomeDirectory();
720
721#ifdef _MSC_VER
722 char slash = '\\';
723#else
724 char slash = '/';
725#endif
726 if (!profile_arg.empty() && (profile_arg[profile_arg.length()-1] != slash))
727 profile_arg += slash;
728 profile_arg += "root_chrome_profile_"s + std::to_string(rnd.Integer(0x100000));
729
730 rmdir = profile_arg;
731 }
732
733 exec = std::regex_replace(exec, std::regex("\\$profile"), profile_arg);
734
735 return rmdir;
736}
737
738
739//////////////////////////////////////////////////////////////////////////////////////////////////
740/// Constructor
741
743{
744 TestProg(gEnv->GetValue("WebGui.Firefox", ""));
745
746#ifdef _MSC_VER
747 TestProg("\\Mozilla Firefox\\firefox.exe", true);
748#endif
749#ifdef R__MACOSX
750 TestProg("/Applications/Firefox.app/Contents/MacOS/firefox");
751#endif
752#ifdef R__LINUX
753 TestProg("/snap/bin/firefox");
754 TestProg("/usr/bin/firefox");
755 TestProg("/usr/bin/firefox-bin");
756#endif
757
758#ifdef _MSC_VER
759 // there is a problem when specifying the window size with wmic on windows:
760 // It gives: Invalid format. Hint: <paramlist> = <param> [, <paramlist>].
761 fBatchExec = gEnv->GetValue("WebGui.FirefoxBatch", "$prog -headless -no-remote $profile $url");
762 fHeadlessExec = gEnv->GetValue("WebGui.FirefoxHeadless", "fork:-headless -no-remote $profile \"$url\"");
763 fExec = gEnv->GetValue("WebGui.FirefoxInteractive", "$prog -no-remote $profile $geometry $url &");
764#else
765 fBatchExec = gEnv->GetValue("WebGui.FirefoxBatch", "fork:--headless -no-remote -new-instance $profile $url");
766 fHeadlessExec = gEnv->GetValue("WebGui.FirefoxHeadless", "fork:--headless -no-remote $profile --private-window $url");
767 fExec = gEnv->GetValue("WebGui.FirefoxInteractive", "$rootetcdir/runfirefox.sh __nodump__ $cleanup_profile $prog -no-remote $profile $geometry -url \'$url\' &");
768#endif
769}
770
771//////////////////////////////////////////////////////////////////////////////////////////////////
772/// Process window geometry for Firefox
773
775{
776 std::string geometry;
777 if ((args.GetWidth() > 0) && (args.GetHeight() > 0) && !args.IsHeadless())
778 geometry = "-width="s + std::to_string(args.GetWidth()) + " -height=" + std::to_string(args.GetHeight());
779
780 exec = std::regex_replace(exec, std::regex("\\$geometry"), geometry);
781}
782
783//////////////////////////////////////////////////////////////////////////////////////////////////
784/// Create Firefox profile to run independent browser window
785
786std::string RWebDisplayHandle::FirefoxCreator::MakeProfile(std::string &exec, bool batch_mode)
787{
788 std::string rmdir, profile_arg;
789
790 if (exec.find("$profile") == std::string::npos)
791 return rmdir;
792
793 const char *ff_profile = gEnv->GetValue("WebGui.FirefoxProfile", "");
794 const char *ff_profilepath = gEnv->GetValue("WebGui.FirefoxProfilePath", "");
795 Int_t ff_randomprofile = RWebWindowWSHandler::GetBoolEnv("WebGui.FirefoxRandomProfile", 1);
796 if (ff_profile && *ff_profile) {
797 profile_arg = "-P "s + ff_profile;
798 } else if (ff_profilepath && *ff_profilepath) {
799 profile_arg = "-profile "s + ff_profilepath;
800 } else if (ff_randomprofile > 0) {
801 TRandom3 rnd;
802 rnd.SetSeed(0);
803 std::string profile_dir = gSystem->TempDirectory();
804 if ((profile_dir.compare(0, 4, "/tmp") == 0) && IsSnapBrowser())
805 profile_dir = gSystem->GetHomeDirectory();
806
807#ifdef _MSC_VER
808 char slash = '\\';
809#else
810 char slash = '/';
811#endif
812 if (!profile_dir.empty() && (profile_dir[profile_dir.length()-1] != slash))
813 profile_dir += slash;
814 profile_dir += "root_ff_profile_"s + std::to_string(rnd.Integer(0x100000));
815
816 profile_arg = "-profile "s + profile_dir;
817
818 if (gSystem->mkdir(profile_dir.c_str()) == 0) {
819 rmdir = profile_dir;
820
821 std::ofstream user_js(profile_dir + "/user.js", std::ios::trunc);
822 // workaround for current Firefox, without such settings it fail to close window and terminate it from batch
823 // also disable question about upload of data
824 user_js << "user_pref(\"datareporting.policy.dataSubmissionPolicyBypassNotification\", true);" << std::endl;
825 user_js << "user_pref(\"datareporting.policy.dataSubmissionPolicyAcceptedVersion\", 2);" << std::endl;
826 user_js << "user_pref(\"datareporting.policy.dataSubmissionPolicyNotifiedTime\", \"1635760572813\");" << std::endl;
827
828 // try to avoid any kind of dialogs on the start
829 user_js << "user_pref(\"app.update.auto\", false);" << std::endl;
830 user_js << "user_pref(\"browser.shell.checkDefaultBrowser\", false);" << std::endl;
831 user_js << "user_pref(\"browser.aboutwelcome.enabled\", false);" << std::endl;
832 user_js << "user_pref(\"browser.tabs.disableBackgroundLinkLoading\", true);" << std::endl;
833
834 // try to ensure that window closes with last tab
835 user_js << "user_pref(\"browser.tabs.closeWindowWithLastTab\", true);" << std::endl;
836 user_js << "user_pref(\"dom.allow_scripts_to_close_windows\", true);" << std::endl;
837 user_js << "user_pref(\"browser.sessionstore.resume_from_crash\", false);" << std::endl;
838
839 if (batch_mode) {
840 // allow to dump messages to std output
841 user_js << "user_pref(\"browser.dom.window.dump.enabled\", true);" << std::endl;
842 } else {
843 // to suppress annoying privacy tab
844 user_js << "user_pref(\"datareporting.policy.firstRunURL\", \"\");" << std::endl;
845 // to use custom userChrome.css files
846 user_js << "user_pref(\"toolkit.legacyUserProfileCustomizations.stylesheets\", true);" << std::endl;
847 // do not put tabs in title
848 user_js << "user_pref(\"browser.tabs.inTitlebar\", 0);" << std::endl;
849
850#ifdef R__LINUX
851 // fix WebGL creation problem on some Linux platforms
852 user_js << "user_pref(\"webgl.out-of-process\", false);" << std::endl;
853#endif
854
855 std::ofstream times_json(profile_dir + "/times.json", std::ios::trunc);
856 times_json << "{" << std::endl;
857 times_json << " \"created\": 1699968480952," << std::endl;
858 times_json << " \"firstUse\": null" << std::endl;
859 times_json << "}" << std::endl;
860 if (gSystem->mkdir((profile_dir + "/chrome").c_str()) == 0) {
861 std::ofstream style(profile_dir + "/chrome/userChrome.css", std::ios::trunc);
862 // do not show tabs
863 style << "#TabsToolbar { visibility: collapse; }" << std::endl;
864 // do not show URL
865 style << "#nav-bar, #urlbar-container, #searchbar { visibility: collapse !important; }" << std::endl;
866 }
867 }
868
869 } else {
870 R__LOG_ERROR(WebGUILog()) << "Cannot create Firefox profile directory " << profile_dir;
871 }
872 }
873
874 exec = std::regex_replace(exec, std::regex("\\$profile"), profile_arg);
875
876 if (exec.find("$cleanup_profile") != std::string::npos) {
877 if (rmdir.empty()) rmdir = "__dummy__";
878 exec = std::regex_replace(exec, std::regex("\\$cleanup_profile"), rmdir);
879 rmdir.clear(); // no need to delete directory - it will be removed by script
880 }
881
882 return rmdir;
883}
884
885///////////////////////////////////////////////////////////////////////////////////////////////////
886/// Check if http server required for display
887/// \param args - defines where and how to display web window
888
890{
893 return false;
894
895 if (!args.IsHeadless() && (args.GetBrowserKind() == RWebDisplayArgs::kOn)) {
896
897#ifdef WITH_QT6WEB
898 auto &qt6 = FindCreator("qt6", "libROOTQt6WebDisplay");
899 if (qt6 && qt6->IsActive())
900 return false;
901#endif
902#ifdef WITH_CEFWEB
903 auto &cef = FindCreator("cef", "libROOTCefDisplay");
904 if (cef && cef->IsActive())
905 return false;
906#endif
907 }
908
909 return true;
910}
911
912
913///////////////////////////////////////////////////////////////////////////////////////////////////
914/// Create web display
915/// \param args - defines where and how to display web window
916/// Returns RWebDisplayHandle, which holds information of running browser application
917/// Can be used fully independent from RWebWindow classes just to show any web page
918
919std::unique_ptr<RWebDisplayHandle> RWebDisplayHandle::Display(const RWebDisplayArgs &args)
920{
921 std::unique_ptr<RWebDisplayHandle> handle;
922
924 return handle;
925
926 auto try_creator = [&](std::unique_ptr<Creator> &creator) {
927 if (!creator || !creator->IsActive())
928 return false;
929 handle = creator->Display(args);
930 return handle ? true : false;
931 };
932
933 bool handleAsLocal = (args.GetBrowserKind() == RWebDisplayArgs::kLocal) ||
934 (!args.IsHeadless() && (args.GetBrowserKind() == RWebDisplayArgs::kOn)),
935 has_qt6web = false, has_cefweb = false;
936
937#ifdef WITH_QT6WEB
938 has_qt6web = true;
939#endif
940
941#ifdef WITH_CEFWEB
942 has_cefweb = true;
943#endif
944
945 if ((handleAsLocal && has_qt6web) || (args.GetBrowserKind() == RWebDisplayArgs::kQt6)) {
946 if (try_creator(FindCreator("qt6", "libROOTQt6WebDisplay")))
947 return handle;
948 }
949
950 if ((handleAsLocal && has_cefweb) || (args.GetBrowserKind() == RWebDisplayArgs::kCEF)) {
951 if (try_creator(FindCreator("cef", "libROOTCefDisplay")))
952 return handle;
953 }
954
955 if (args.IsLocalDisplay()) {
956 R__LOG_ERROR(WebGUILog()) << "Neither Qt5/6 nor CEF libraries were found to provide local display";
957 return handle;
958 }
959
960 bool handleAsNative =
962
963 if (handleAsNative || (args.GetBrowserKind() == RWebDisplayArgs::kChrome)) {
964 if (try_creator(FindCreator("chrome", "ChromeCreator")))
965 return handle;
966 }
967
968 if (handleAsNative || (args.GetBrowserKind() == RWebDisplayArgs::kFirefox)) {
969 if (try_creator(FindCreator("firefox", "FirefoxCreator")))
970 return handle;
971 }
972
973#ifdef _MSC_VER
974 // Edge browser cannot be run headless without registry change, therefore do not try it by default
975 if ((handleAsNative && !args.IsHeadless() && !args.IsBatchMode()) || (args.GetBrowserKind() == RWebDisplayArgs::kEdge)) {
976 if (try_creator(FindCreator("edge", "ChromeCreator")))
977 return handle;
978 }
979#endif
980
983 // R__LOG_ERROR(WebGUILog()) << "Neither Chrome nor Firefox browser cannot be started to provide display";
984 return handle;
985 }
986
988 if (try_creator(FindCreator("safari", "SafariCreator")))
989 return handle;
990 }
991
993 std::unique_ptr<Creator> creator = std::make_unique<BrowserCreator>(false, args.GetCustomExec());
994 try_creator(creator);
995 } else {
996 try_creator(FindCreator("browser", "BrowserCreator"));
997 }
998
999 return handle;
1000}
1001
1002///////////////////////////////////////////////////////////////////////////////////////////////////
1003/// Display provided url in configured web browser
1004/// \param url - specified URL address like https://root.cern
1005/// Browser can specified when starting `root --web=firefox`
1006/// Returns true when browser started
1007/// It is convenience method, equivalent to:
1008/// ~~~
1009/// RWebDisplayArgs args;
1010/// args.SetUrl(url);
1011/// args.SetStandalone(false);
1012/// auto handle = RWebDisplayHandle::Display(args);
1013/// ~~~
1014
1015bool RWebDisplayHandle::DisplayUrl(const std::string &url)
1016{
1017 RWebDisplayArgs args;
1018 args.SetUrl(url);
1019 args.SetStandalone(false);
1020
1021 auto handle = Display(args);
1022
1023 return !!handle;
1024}
1025
1026///////////////////////////////////////////////////////////////////////////////////////////////////
1027/// Checks if configured browser can be used for image production
1028
1030{
1034 bool detected = false;
1035
1036 auto &h1 = FindCreator("chrome", "ChromeCreator");
1037 if (h1 && h1->IsActive()) {
1039 detected = true;
1040 }
1041
1042 if (!detected) {
1043 auto &h2 = FindCreator("firefox", "FirefoxCreator");
1044 if (h2 && h2->IsActive()) {
1046 detected = true;
1047 }
1048 }
1049
1050 return detected;
1051 }
1052
1054 auto &h1 = FindCreator("chrome", "ChromeCreator");
1055 return h1 && h1->IsActive();
1056 }
1057
1059 auto &h2 = FindCreator("firefox", "FirefoxCreator");
1060 return h2 && h2->IsActive();
1061 }
1062
1063#ifdef _MSC_VER
1064 if (args.GetBrowserKind() == RWebDisplayArgs::kEdge) {
1065 auto &h3 = FindCreator("edge", "ChromeCreator");
1066 return h3 && h3->IsActive();
1067 }
1068#endif
1069
1070 return true;
1071}
1072
1073///////////////////////////////////////////////////////////////////////////////////////////////////
1074/// Returns true if image production for specified browser kind is supported
1075/// If browser not specified - use currently configured browser or try to test existing web browsers
1076
1077bool RWebDisplayHandle::CanProduceImages(const std::string &browser)
1078{
1079 RWebDisplayArgs args(browser);
1080
1081 return CheckIfCanProduceImages(args);
1082}
1083
1084///////////////////////////////////////////////////////////////////////////////////////////////////
1085/// Detect image format
1086/// There is special handling of ".screenshot.pdf" and ".screenshot.png" extensions
1087/// Creation of such files relies on headless browser functionality and fully supported only by Chrome browser
1088
1089std::string RWebDisplayHandle::GetImageFormat(const std::string &fname)
1090{
1091 std::string _fname = fname;
1092 std::transform(_fname.begin(), _fname.end(), _fname.begin(), ::tolower);
1093 auto EndsWith = [&_fname](const std::string &suffix) {
1094 return (_fname.length() > suffix.length()) ? (0 == _fname.compare(_fname.length() - suffix.length(), suffix.length(), suffix)) : false;
1095 };
1096
1097 if (EndsWith(".screenshot.pdf"))
1098 return "s.pdf"s;
1099 if (EndsWith(".pdf"))
1100 return "pdf"s;
1101 if (EndsWith(".json"))
1102 return "json"s;
1103 if (EndsWith(".svg"))
1104 return "svg"s;
1105 if (EndsWith(".screenshot.png"))
1106 return "s.png"s;
1107 if (EndsWith(".png"))
1108 return "png"s;
1109 if (EndsWith(".html") || EndsWith(".htm"))
1110 return "html"s;
1111 if (EndsWith(".jpg") || EndsWith(".jpeg"))
1112 return "jpeg"s;
1113 if (EndsWith(".webp"))
1114 return "webp"s;
1115
1116 return ""s;
1117}
1118
1119
1120///////////////////////////////////////////////////////////////////////////////////////////////////
1121/// Produce image file using JSON data as source
1122/// Invokes JSROOT drawing functionality in headless browser - Google Chrome or Mozilla Firefox
1123
1124bool RWebDisplayHandle::ProduceImage(const std::string &fname, const std::string &json, int width, int height, const char *batch_file)
1125{
1126 return ProduceImages(fname, {json}, {width}, {height}, batch_file);
1127}
1128
1129
1130///////////////////////////////////////////////////////////////////////////////////////////////////
1131/// Produce vector of file names for specified file pattern
1132/// Depending from supported file formats
1133
1134std::vector<std::string> RWebDisplayHandle::ProduceImagesNames(const std::string &fname, unsigned nfiles)
1135{
1136 auto fmt = GetImageFormat(fname);
1137
1138 std::vector<std::string> fnames;
1139
1140 if ((fmt == "s.pdf") || (fmt == "s.png")) {
1141 fnames.emplace_back(fname);
1142 } else {
1143 std::string farg = fname;
1144
1145 bool has_quialifier = farg.find("%") != std::string::npos;
1146
1147 if (!has_quialifier && (nfiles > 1) && (fmt != "pdf") && (fmt != "html")) {
1148 farg.insert(farg.rfind("."), "%d");
1149 has_quialifier = true;
1150 }
1151
1152 for (unsigned n = 0; n < nfiles; n++) {
1153 if(has_quialifier) {
1154 auto expand_name = TString::Format(farg.c_str(), (int) n);
1155 fnames.emplace_back(expand_name.Data());
1156 } else if (n > 0)
1157 fnames.emplace_back(""); // empty name is multiPdf or multiHtml
1158 else
1159 fnames.emplace_back(fname);
1160 }
1161 }
1162
1163 return fnames;
1164}
1165
1166
1167///////////////////////////////////////////////////////////////////////////////////////////////////
1168/// Produce image file(s) using JSON data as source
1169/// Invokes JSROOT drawing functionality in headless browser - Google Chrome or Mozilla Firefox
1170
1171bool RWebDisplayHandle::ProduceImages(const std::string &fname, const std::vector<std::string> &jsons, const std::vector<int> &widths, const std::vector<int> &heights, const char *batch_file)
1172{
1173 return ProduceImages(ProduceImagesNames(fname, jsons.size()), jsons, widths, heights, batch_file);
1174}
1175
1176///////////////////////////////////////////////////////////////////////////////////////////////////
1177/// Produce image file(s) using JSON data as source
1178/// Invokes JSROOT drawing functionality in headless browser - Google Chrome or Mozilla Firefox
1179
1180bool RWebDisplayHandle::ProduceImages(const std::vector<std::string> &fnames, const std::vector<std::string> &jsons, const std::vector<int> &widths, const std::vector<int> &heights, const char *batch_file)
1181{
1182 if (fnames.empty() || jsons.empty())
1183 return false;
1184
1185 std::vector<std::string> fmts;
1186 unsigned num_non_empty_fmts = 0, num_non_empty_files = 0;
1187 for (auto& fname : fnames) {
1188 if (!fname.empty())
1189 num_non_empty_files++;
1190 std::string fmt = GetImageFormat(fname);
1191 if (!fmt.empty())
1192 num_non_empty_fmts++;
1193 fmts.emplace_back(fmt);
1194 }
1195
1196 bool is_any_image = false;
1197
1198 const char *jsrootsys = gSystem->Getenv("JSROOTSYS");
1199
1200 for (unsigned n = 0; (n < fmts.size()) && (n < jsons.size()); n++) {
1201 if (fmts[n] == "json") {
1202 std::ofstream ofs(fnames[n]);
1203 ofs << jsons[n];
1204 fmts[n].clear();
1205 ::Info("ProduceImages", "JSON file %s size %d bytes has been created", fnames[n].c_str(), (int) jsons[n].length());
1206
1207 } else if (fmts[n] == "html") {
1208 bool is_multi_html = (num_non_empty_fmts == 1) && (num_non_empty_files == 1) && (jsons.size() > 1) && (n == 0);
1209
1210 std::string filejsrootsys;
1211 if (jsrootsys)
1212 filejsrootsys = jsrootsys;
1213 if (filejsrootsys.empty() || ((filejsrootsys.find("http://") != 0) && (filejsrootsys.find("https://") != 0)))
1214 filejsrootsys = "https://root.cern/js/latest";
1215
1216 std::ofstream ofs(fnames[n]);
1217
1218 ofs << "<!DOCTYPE html>\n"
1219 "<html lang=\"en\">\n"
1220 "<head>\n"
1221 " <meta charset=\"utf-8\">\n"
1222 " <title>Dsiplay of ROOT " << (is_multi_html ? "objects" : "object") << "</title>\n"
1223 " <link rel=\"shortcut icon\" href=\"" << filejsrootsys << "/img/RootIcon.ico\"/>\n"
1224 " <script type=\"importmap\">\n"
1225 " { \"imports\": { \"jsroot\": \"" << filejsrootsys << "/modules/main.mjs\" } }\n"
1226 " </script>\n"
1227 " <style>\n";
1228 if (is_multi_html) {
1229 ofs << " .root-container {\n"
1230 " display: flex;\n"
1231 " flex-direction: column;\n"
1232 " align-items: center;\n"
1233 " gap: 20px;\n"
1234 " width: 100%;\n"
1235 " }\n";
1236 } else {
1237 ofs << " body {\n"
1238 " margin: 0;\n"
1239 " padding: 0;\n"
1240 " display: flex;\n"
1241 " justify-content: center;\n"
1242 " align-items: center;\n"
1243 " min-height: 100vh;\n"
1244 " background-color: #f0f0f0;\n"
1245 " }\n";
1246 }
1247 ofs << " .root-drawing {\n"
1248 " background-color: white;\n"
1249 " box-shadow: 0 4px 10px rgba(0,0,0,0.1);\n"
1250 " }\n"
1251 " </style>\n"
1252 "</head>\n"
1253 "<body>\n";
1254 if (is_multi_html) {
1255 ofs << " <div class=\"root-container\">\n";
1256 for (unsigned k = 0; k < jsons.size(); ++k)
1257 ofs << " <div id=\"drawing" << k << "\" class=\"root-drawing\""
1258 " style=\"width: " << widths[k] << "px;"
1259 " height: " << heights[k] << "px;\"></div>\n";
1260 ofs << " </div>\n";
1261 } else {
1262 ofs << " <div id=\"drawing\" class=\"root-drawing\""
1263 " style=\"width: " << widths[n] << "px;"
1264 " min-height: " << heights[n] << "px;\"></div>\n";
1265 }
1266 ofs << " <script type=\"module\">\n"
1267 " import { parse, draw } from \"jsroot\";\n";
1268 if (is_multi_html) {
1269 for (unsigned k = 0; k < jsons.size(); ++k) {
1270 ofs << " const obj" << k << " = parse(" << jsons[k] << ");\n"
1271 " draw(\"drawing" << k << "\", obj" << k << ");\n";
1272 }
1273 } else {
1274 ofs << " const obj = parse(" << jsons[n] << ");\n"
1275 " draw(\"drawing\", obj);\n";
1276 }
1277 ofs << " </script>\n"
1278 "</body>\n"
1279 "</html>\n";
1280
1281 ::Info("ProduceImages", "HTML file %s size %d bytes has been created", fnames[n].c_str(), (int) ofs.tellp());
1282
1283 fmts[n].clear();
1284 if (is_multi_html)
1285 break;
1286 } else if (!fmts[n].empty())
1287 is_any_image = true;
1288 }
1289
1290 if (!is_any_image)
1291 return true;
1292
1293 std::string fdebug;
1294 if (fnames.size() == 1)
1295 fdebug = fnames[0];
1296 else
1298
1299 TString jsrootsysdflt;
1300 if (!jsrootsys) {
1301 jsrootsysdflt = TROOT::GetDataDir() + "/js";
1302 if (gSystem->ExpandPathName(jsrootsysdflt)) {
1303 R__LOG_ERROR(WebGUILog()) << "Fail to locate JSROOT " << jsrootsysdflt;
1304 return false;
1305 }
1306 jsrootsys = jsrootsysdflt.Data();
1307 }
1308
1309 RWebDisplayArgs args; // set default browser kind, only Chrome/Firefox/Edge or CEF/Qt6 can be used here
1310 if (!CheckIfCanProduceImages(args)) {
1311 R__LOG_ERROR(WebGUILog()) << "Fail to detect supported browsers for image production";
1312 return false;
1313 }
1314
1315 auto isChrome = (args.GetBrowserKind() == RWebDisplayArgs::kChrome),
1316 isChromeBased = isChrome || (args.GetBrowserKind() == RWebDisplayArgs::kEdge),
1317 isFirefox = args.GetBrowserKind() == RWebDisplayArgs::kFirefox;
1318
1319 std::vector<std::string> draw_kinds;
1320 bool use_browser_draw = false, can_optimize_json = false;
1321 int use_home_dir = 0;
1322 TString jsonkind;
1323
1324 // Some Chrome installation do not allow run html code from files, created in /tmp directory
1325 // When during session such failures happened, force usage of home directory from the beginning
1326 static int chrome_tmp_workaround = 0;
1327
1328 if (isChrome) {
1329 use_home_dir = chrome_tmp_workaround;
1330 auto &h1 = FindCreator("chrome", "ChromeCreator");
1331 if (h1 && h1->IsActive() && h1->IsSnapBrowser() && (use_home_dir == 0))
1332 use_home_dir = 1;
1333 }
1334
1335 if (fmts[0] == "s.png") {
1336 if (!isChromeBased && !isFirefox) {
1337 R__LOG_ERROR(WebGUILog()) << "Direct png image creation supported only by Chrome and Firefox browsers";
1338 return false;
1339 }
1340 use_browser_draw = true;
1341 jsonkind = "1111"; // special mark in canv_batch.htm
1342 } else if (fmts[0] == "s.pdf") {
1343 if (!isChromeBased) {
1344 R__LOG_ERROR(WebGUILog()) << "Direct creation of PDF files supported only by Chrome-based browser";
1345 return false;
1346 }
1347 use_browser_draw = true;
1348 jsonkind = "2222"; // special mark in canv_batch.htm
1349 } else {
1350 draw_kinds = fmts;
1351 jsonkind = TBufferJSON::ToJSON(&draw_kinds, TBufferJSON::kNoSpaces);
1352 can_optimize_json = true;
1353 }
1354
1355 if (!batch_file || !*batch_file)
1356 batch_file = "/js/files/canv_batch.htm";
1357
1358 TString origin = TROOT::GetDataDir() + batch_file;
1359 if (gSystem->ExpandPathName(origin)) {
1360 R__LOG_ERROR(WebGUILog()) << "Fail to find " << origin;
1361 return false;
1362 }
1363
1364 auto filecont = THttpServer::ReadFileContent(origin.Data());
1365 if (filecont.empty()) {
1366 R__LOG_ERROR(WebGUILog()) << "Fail to read content of " << origin;
1367 return false;
1368 }
1369
1370 int max_width = 0, max_height = 0, page_margin = 10;
1371 for (auto &w : widths)
1372 if (w > max_width)
1373 max_width = w;
1374 for (auto &h : heights)
1375 if (h > max_height)
1376 max_height = h;
1377
1378 auto jsonw = TBufferJSON::ToJSON(&widths, TBufferJSON::kNoSpaces);
1379 auto jsonh = TBufferJSON::ToJSON(&heights, TBufferJSON::kNoSpaces);
1380
1381 std::string mains, prev;
1382 for (auto &json : jsons) {
1383 mains.append(mains.empty() ? "[" : ", ");
1384 if (can_optimize_json && (json == prev)) {
1385 mains.append("'same'");
1386 } else {
1387 mains.append(json);
1388 prev = json;
1389 }
1390 }
1391 mains.append("]");
1392
1393 if (strstr(jsrootsys, "http://") || strstr(jsrootsys, "https://") || strstr(jsrootsys, "file://"))
1394 filecont = std::regex_replace(filecont, std::regex("\\$jsrootsys"), jsrootsys);
1395 else {
1396 static std::string jsroot_include = "<script id=\"jsroot\" src=\"$jsrootsys/build/jsroot.js\"></script>";
1397 auto p = filecont.find(jsroot_include);
1398 if (p != std::string::npos) {
1399 auto jsroot_build = THttpServer::ReadFileContent(std::string(jsrootsys) + "/build/jsroot.js");
1400 if (!jsroot_build.empty()) {
1401 // insert actual jsroot file location
1402 jsroot_build = std::regex_replace(jsroot_build, std::regex("'\\$jsrootsys'"), std::string("'file://") + jsrootsys + "/'");
1403 filecont.erase(p, jsroot_include.length());
1404 filecont.insert(p, "<script id=\"jsroot\">" + jsroot_build + "</script>");
1405 }
1406 }
1407
1408 filecont = std::regex_replace(filecont, std::regex("\\$jsrootsys"), "file://"s + jsrootsys);
1409 }
1410
1411 filecont = std::regex_replace(filecont, std::regex("\\$page_margin"), std::to_string(page_margin) + "px");
1412 filecont = std::regex_replace(filecont, std::regex("\\$page_width"), std::to_string(max_width + 2*page_margin) + "px");
1413 filecont = std::regex_replace(filecont, std::regex("\\$page_height"), std::to_string(max_height + 2*page_margin) + "px");
1414
1415 filecont = std::regex_replace(filecont, std::regex("\\$draw_kind"), jsonkind.Data());
1416 filecont = std::regex_replace(filecont, std::regex("\\$draw_widths"), jsonw.Data());
1417 filecont = std::regex_replace(filecont, std::regex("\\$draw_heights"), jsonh.Data());
1418 filecont = std::regex_replace(filecont, std::regex("\\$draw_objects"), mains);
1419
1420 TString dump_name, html_name;
1421
1422 if (!use_browser_draw && (isChromeBased || isFirefox)) {
1423 dump_name = "canvasdump";
1424 FILE *df = BrowserCreator::TemporaryFile(dump_name, use_home_dir);
1425 if (!df) {
1426 R__LOG_ERROR(WebGUILog()) << "Fail to create temporary file for dump-dom";
1427 return false;
1428 }
1429 fputs("placeholder", df);
1430 fclose(df);
1431 }
1432
1433try_again:
1434
1436 args.SetUrl(""s);
1437 args.SetPageContent(filecont);
1438
1439 html_name.Clear();
1440
1441 R__LOG_DEBUG(0, WebGUILog()) << "Using file content_len " << filecont.length() << " to produce batch images ";
1442
1443 } else {
1444 html_name = "canvasbody";
1445 FILE *hf = BrowserCreator::TemporaryFile(html_name, use_home_dir, ".html");
1446 if (!hf) {
1447 R__LOG_ERROR(WebGUILog()) << "Fail to create temporary file for batch job";
1448 return false;
1449 }
1450 fputs(filecont.c_str(), hf);
1451 fclose(hf);
1452
1453 args.SetUrl("file://"s + gSystem->UnixPathName(html_name.Data()));
1454 args.SetPageContent(""s);
1455
1456 R__LOG_DEBUG(0, WebGUILog()) << "Using " << html_name << " content_len " << filecont.length() << " to produce batch images " << fdebug;
1457 }
1458
1459 TString wait_file_name, tgtfilename;
1460
1461 args.SetStandalone(true);
1462 args.SetHeadless(true);
1463 args.SetBatchMode(true);
1464 args.SetSize(widths[0], heights[0]);
1465
1466 if (use_browser_draw) {
1467
1468 tgtfilename = fnames[0].c_str();
1469 if (!gSystem->IsAbsoluteFileName(tgtfilename.Data()))
1470 gSystem->PrependPathName(gSystem->WorkingDirectory(), tgtfilename);
1471
1472 wait_file_name = tgtfilename;
1473
1474 if (fmts[0] == "s.pdf")
1475 args.SetExtraArgs("--print-to-pdf-no-header --print-to-pdf="s + gSystem->UnixPathName(tgtfilename.Data()));
1476 else if (isFirefox) {
1477 args.SetExtraArgs("--screenshot"); // firefox does not let specify output image file
1478 wait_file_name = "screenshot.png";
1479 } else
1480 args.SetExtraArgs("--screenshot="s + gSystem->UnixPathName(tgtfilename.Data()));
1481
1482 // remove target image file - we use it as detection when chrome is ready
1483 gSystem->Unlink(tgtfilename.Data());
1484
1485 } else if (isFirefox) {
1486 // firefox will use window.dump to output produced result
1487 args.SetRedirectOutput(dump_name.Data());
1488 gSystem->Unlink(dump_name.Data());
1489 } else if (isChromeBased) {
1490 // chrome should have --dump-dom args configures
1491 args.SetRedirectOutput(dump_name.Data());
1492 gSystem->Unlink(dump_name.Data());
1493 }
1494
1495 auto handle = RWebDisplayHandle::Display(args);
1496
1497 // ensure file is created by browser draw
1498 if (use_browser_draw && handle) {
1499 Int_t batch_timeout = gEnv->GetValue("WebGui.BatchTimeout", 30) * 10;
1500 while (gSystem->AccessPathName(wait_file_name.Data()) && (--batch_timeout > 0)) {
1501 gSystem->ProcessEvents();
1502 gSystem->Sleep(100);
1503 }
1504 }
1505
1506 // delete temporary HTML file
1507 if (html_name.Length() > 0) {
1508 if (gEnv->GetValue("WebGui.PreserveBatchFiles", -1) > 0)
1509 ::Info("ProduceImages", "Preserve batch file %s", html_name.Data());
1510 else
1511 gSystem->Unlink(html_name.Data());
1512 }
1513
1514 if (!handle) {
1515 R__LOG_DEBUG(0, WebGUILog()) << "Cannot start " << args.GetBrowserName() << " to produce image " << fdebug;
1516 return false;
1517 }
1518
1519 if (use_browser_draw) {
1520
1521 if (gSystem->AccessPathName(wait_file_name.Data())) {
1522 R__LOG_ERROR(WebGUILog()) << "Fail to produce image " << fdebug;
1523 return false;
1524 }
1525
1526 if (fmts[0] == "s.pdf")
1527 ::Info("ProduceImages", "PDF file %s with %d pages has been created", fnames[0].c_str(), (int) jsons.size());
1528 else {
1529 if (isFirefox)
1530 gSystem->Rename("screenshot.png", fnames[0].c_str());
1531 ::Info("ProduceImages", "PNG file %s with %d pages has been created", fnames[0].c_str(), (int) jsons.size());
1532 }
1533 } else {
1534 auto dumpcont = handle->GetContent();
1535
1536 if ((dumpcont.length() > 20) && (dumpcont.length() < 60) && (use_home_dir < 2) && isChrome) {
1537 // chrome creates dummy html file with mostly no content
1538 // problem running chrome from /tmp directory, lets try work from home directory
1539 R__LOG_INFO(WebGUILog()) << "Use home directory for running chrome in batch, set TMPDIR for preferable temp directory";
1540 chrome_tmp_workaround = use_home_dir = 2;
1541 goto try_again;
1542 }
1543
1544 if (dumpcont.length() < 100) {
1545 R__LOG_ERROR(WebGUILog()) << "Fail to dump HTML code into " << (dump_name.IsNull() ? "CEF" : dump_name.Data());
1546 return false;
1547 }
1548
1549 std::string::size_type p = 0;
1550
1551 for (unsigned n = 0; n < fmts.size(); n++) {
1552 if (fmts[n].empty())
1553 continue;
1554 if (fmts[n] == "svg") {
1555 auto p1 = dumpcont.find("<div><svg", p);
1556 auto p2 = dumpcont.find("</svg></div>", p1 + 8);
1557 p = p2 + 12;
1558 std::ofstream ofs(fnames[n]);
1559 if ((p1 != std::string::npos) && (p2 != std::string::npos) && (p1 < p2)) {
1560 if (p2 - p1 > 10) {
1561 ofs << dumpcont.substr(p1 + 5, p2 - p1 + 1);
1562 ::Info("ProduceImages", "Image file %s size %d bytes has been created", fnames[n].c_str(), (int) (p2 - p1 + 1));
1563 } else {
1564 ::Error("ProduceImages", "Failure producing %s", fnames[n].c_str());
1565 }
1566 }
1567 } else {
1568 auto p0 = dumpcont.find("<img src=\"", p);
1569 auto p1 = dumpcont.find(";base64,", p0 + 8);
1570 auto p2 = dumpcont.find("\">", p1 + 8);
1571 p = p2 + 2;
1572
1573 if ((p0 != std::string::npos) && (p1 != std::string::npos) && (p2 != std::string::npos) && (p1 < p2)) {
1574 auto base64 = dumpcont.substr(p1+8, p2-p1-8);
1575 if ((base64 == "failure") || (base64.length() < 10)) {
1576 ::Error("ProduceImages", "Failure producing %s", fnames[n].c_str());
1577 } else {
1578 auto binary = TBase64::Decode(base64.c_str());
1579 std::ofstream ofs(fnames[n], std::ios::binary);
1580 ofs.write(binary.Data(), binary.Length());
1581 ::Info("ProduceImages", "Image file %s size %d bytes has been created", fnames[n].c_str(), (int) binary.Length());
1582 }
1583 } else {
1584 ::Error("ProduceImages", "Failure producing %s", fnames[n].c_str());
1585 return false;
1586 }
1587 }
1588 }
1589 }
1590
1591 R__LOG_DEBUG(0, WebGUILog()) << "Create " << (fnames.size() > 1 ? "files " : "file ") << fdebug;
1592
1593 return true;
1594}
1595
true
Register systematic variations for multiple existing columns using auto-generated tags.
#define R__LOG_ERROR(...)
Definition RLogger.hxx:356
#define R__LOG_DEBUG(DEBUGLEVEL,...)
Definition RLogger.hxx:359
#define R__LOG_INFO(...)
Definition RLogger.hxx:358
#define f(i)
Definition RSha256.hxx:104
#define c(i)
Definition RSha256.hxx:101
#define h(i)
Definition RSha256.hxx:106
static void DummyTimeOutHandler(int)
int Int_t
Signed integer 4 bytes (int)
Definition RtypesCore.h:59
R__EXTERN TEnv * gEnv
Definition TEnv.h:126
void Info(const char *location, const char *msgfmt,...)
Use this function for informational messages.
Definition TError.cxx:241
void Error(const char *location, const char *msgfmt,...)
Use this function in case an error occurred.
Definition TError.cxx:208
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void w
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t Float_t Float_t Int_t Int_t UInt_t UInt_t Rectangle_t Int_t Int_t Window_t TString Int_t GCValues_t GetPrimarySelectionOwner GetDisplay GetScreen GetColormap GetNativeEvent const char const char dpyName wid window const char font_name cursor keysym reg const char only_if_exist regb h Point_t winding char text const char depth char const char Int_t count const char ColorStruct_t color const char Pixmap_t Pixmap_t PictureAttributes_t attr const char char ret_data h unsigned char height h length
Option_t Option_t width
Option_t Option_t style
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t height
char name[80]
Definition TGX11.cxx:148
@ kExecutePermission
Definition TSystem.h:53
R__EXTERN TSystem * gSystem
Definition TSystem.h:582
RWebBrowserHandle(const std::string &url, const std::string &tmpdir, const std::string &tmpfile, browser_process_id pid)
std::string fTmpDir
temporary directory to delete at the end
void RemoveStartupFiles() override
remove file which was used to startup widget - if possible
RWebBrowserHandle(const std::string &url, const std::string &tmpdir, const std::string &tmpfile, const std::string &dump)
std::string fTmpFile
temporary file to remove
Holds different arguments for starting browser with RWebDisplayHandle::Display() method.
std::string GetBrowserName() const
Returns configured browser name.
EBrowserKind GetBrowserKind() const
returns configured browser kind, see EBrowserKind for supported values
const std::string & GetRedirectOutput() const
get file name to which web browser output should be redirected
void SetStandalone(bool on=true)
Set standalone mode for running browser, default on When disabled, normal browser window (or just tab...
void SetBatchMode(bool on=true)
set batch mode
RWebDisplayArgs & SetSize(int w, int h)
set preferable web window width and height
RWebDisplayArgs & SetUrl(const std::string &url)
set window url
int GetWidth() const
returns preferable web window width
RWebDisplayArgs & SetPageContent(const std::string &cont)
set window url
int GetY() const
set preferable web window y position
std::string GetFullUrl() const
returns window url with append options
bool IsStandalone() const
Return true if browser should runs in standalone mode.
int GetHeight() const
returns preferable web window height
RWebDisplayArgs & SetBrowserKind(const std::string &kind)
Set browser kind as string argument.
std::string GetCustomExec() const
returns custom executable to start web browser
void SetExtraArgs(const std::string &args)
set extra command line arguments for starting web browser command
bool IsBatchMode() const
returns batch mode
bool IsHeadless() const
returns headless mode
@ kOn
web display enable, first try use embed displays like Qt or CEF, then native browsers and at the end ...
@ kFirefox
Mozilla Firefox browser.
@ kNative
either Chrome or Firefox - both support major functionality
@ kLocal
either CEF or Qt5 - both runs on local display without real http server
@ kServer
indicates that ROOT runs as server and just printouts window URL, browser should be started by the us...
@ kOff
disable web display, do not start any browser
@ kCEF
Chromium Embedded Framework - local display with CEF libs.
@ kSafari
Safari browser.
@ kQt6
Qt6 QWebEngine libraries - Chromium code packed in qt6.
@ kCustom
custom web browser, execution string should be provided
@ kChrome
Google Chrome browser.
@ kEdge
Microsoft Edge browser (Windows only)
void SetRedirectOutput(const std::string &fname="")
specify file name to which web browser output should be redirected
void SetHeadless(bool on=true)
set headless mode
const std::string & GetExtraArgs() const
get extra command line arguments for starting web browser command
int GetX() const
set preferable web window x position
bool IsLocalDisplay() const
returns true if local display like CEF or Qt5 QWebEngine should be used
std::string fBatchExec
batch execute line
std::string fHeadlessExec
headless execute line
static FILE * TemporaryFile(TString &name, int use_home_dir=0, const char *suffix=nullptr)
Create temporary file for web display Normally gSystem->TempFileName() method used to create file in ...
std::unique_ptr< RWebDisplayHandle > Display(const RWebDisplayArgs &args) override
Display given URL in web browser.
virtual void ProcessGeometry(std::string &, const RWebDisplayArgs &)
std::string fExec
standard execute line
void TestProg(const std::string &nexttry, bool check_std_paths=false)
Check if browser executable exists and can be used.
BrowserCreator(bool custom=true, const std::string &exec="")
Class to handle starting of web-browsers like Chrome or Firefox.
virtual std::string MakeProfile(std::string &, bool)
ChromeCreator(bool is_edge=false)
Constructor.
void ProcessGeometry(std::string &, const RWebDisplayArgs &) override
Replace $geometry placeholder with geometry settings Also RWebDisplayArgs::GetExtraArgs() are appende...
std::string MakeProfile(std::string &exec, bool) override
Handle profile argument.
std::string MakeProfile(std::string &exec, bool batch) override
Create Firefox profile to run independent browser window.
void ProcessGeometry(std::string &, const RWebDisplayArgs &) override
Process window geometry for Firefox.
bool IsActive() const override
Returns true if it can be used.
static std::map< std::string, std::unique_ptr< Creator > > & GetMap()
Static holder of registered creators of web displays.
static bool CheckIfCanProduceImages(RWebDisplayArgs &args)
Checks if configured browser can be used for image production.
static bool ProduceImages(const std::string &fname, const std::vector< std::string > &jsons, const std::vector< int > &widths, const std::vector< int > &heights, const char *batch_file=nullptr)
Produce image file(s) using JSON data as source Invokes JSROOT drawing functionality in headless brow...
static std::vector< std::string > ProduceImagesNames(const std::string &fname, unsigned nfiles=1)
Produce vector of file names for specified file pattern Depending from supported file formats.
static std::string GetImageFormat(const std::string &fname)
Detect image format There is special handling of ".screenshot.pdf" and ".screenshot....
void SetContent(const std::string &cont)
set content
RWebDisplayHandle(const std::string &url)
constructor
static bool ProduceImage(const std::string &fname, const std::string &json, int width=800, int height=600, const char *batch_file=nullptr)
Produce image file using JSON data as source Invokes JSROOT drawing functionality in headless browser...
static bool CanProduceImages(const std::string &browser="")
Returns true if image production for specified browser kind is supported If browser not specified - u...
static bool NeedHttpServer(const RWebDisplayArgs &args)
Check if http server required for display.
static bool DisplayUrl(const std::string &url)
Display provided url in configured web browser.
static std::unique_ptr< RWebDisplayHandle > Display(const RWebDisplayArgs &args)
Create web display.
static std::unique_ptr< Creator > & FindCreator(const std::string &name, const std::string &libname="")
Search for specific browser creator If not found, try to add one.
static int GetBoolEnv(const std::string &name, int dfl=-1)
Parse boolean gEnv variable which should be "yes" or "no".
static TString Decode(const char *data)
Decode a base64 string date into a generic TString.
Definition TBase64.cxx:130
static TString ToJSON(const T *obj, Int_t compact=0, const char *member_name=nullptr)
Definition TBufferJSON.h:77
@ kNoSpaces
no new lines plus remove all spaces around "," and ":" symbols
Definition TBufferJSON.h:39
static char * ReadFileContent(const char *filename, Int_t &len)
Reads content of file from the disk.
static const TString & GetEtcDir()
Get the sysconfig directory in the installation. Static utility function.
Definition TROOT.cxx:3381
static const TString & GetDataDir()
Get the data directory in the installation. Static utility function.
Definition TROOT.cxx:3391
Random number generator class based on M.
Definition TRandom3.h:27
void SetSeed(ULong_t seed=0) override
Set the random generator sequence.
Definition TRandom3.cxx:229
virtual UInt_t Integer(UInt_t imax)
Returns a random integer uniformly distributed on the interval [ 0, imax-1 ].
Definition TRandom.cxx:360
Basic string class.
Definition TString.h:138
Ssiz_t Length() const
Definition TString.h:427
void Clear()
Clear string without changing its capacity.
Definition TString.cxx:1241
const char * Data() const
Definition TString.h:386
TObjArray * Tokenize(const TString &delim) const
This function is used to isolate sequential tokens in a TString.
Definition TString.cxx:2344
Bool_t IsNull() const
Definition TString.h:424
static TString Format(const char *fmt,...)
Static method which formats a string using a printf style format descriptor and return a TString.
Definition TString.cxx:2459
const Int_t n
Definition legend1.C:16
TH1F * h1
Definition legend1.C:5
bool EndsWith(std::string_view string, std::string_view suffix)
ROOT::RLogChannel & WebGUILog()
Log channel for WebGUI diagnostics.
TCanvas * slash()
Definition slash.C:1
TMarker m
Definition textangle.C:8