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