| #!/usr/bin/env python3 |
| # Copyright 2017 The Emscripten Authors. All rights reserved. |
| # Emscripten is available under two separate licenses, the MIT license and the |
| # University of Illinois/NCSA Open Source License. Both these licenses can be |
| # found in the LICENSE file. |
| |
| """emrun: Implements machinery that allows running a .html page as if it was a |
| standard executable file. |
| |
| Usage: emrun <options> filename.html <args to program> |
| |
| See emrun --help for more information |
| """ |
| |
| # N.B. Do not introduce external dependencies to this file. It is often used |
| # standalone outside Emscripten directory tree. |
| import argparse |
| import atexit |
| import cgi |
| import json |
| import os |
| import platform |
| import re |
| import shlex |
| import shutil |
| import socket |
| import stat |
| import struct |
| import subprocess |
| import sys |
| import tempfile |
| import threading |
| import time |
| from operator import itemgetter |
| |
| if sys.version_info.major == 2: |
| import SocketServer as socketserver |
| from BaseHTTPServer import HTTPServer |
| from SimpleHTTPServer import SimpleHTTPRequestHandler |
| from urllib import unquote |
| from urlparse import urlsplit |
| |
| def print_to_handle(handle, line): |
| print >> handle, line # noqa: F633 |
| else: |
| import socketserver |
| from http.server import HTTPServer, SimpleHTTPRequestHandler |
| from urllib.parse import unquote, urlsplit |
| |
| def print_to_handle(handle, line): |
| handle.write(line + '\n') |
| |
| # Populated from cmdline params |
| emrun_options = None |
| |
| # Represents the process object handle to the browser we opened to run the html |
| # page. |
| browser_process = None |
| |
| previous_browser_processes = None |
| current_browser_processes = None |
| |
| navigation_has_occurred = False |
| |
| # Stores the browser executable that was run with --browser= parameter. |
| browser_exe = None |
| |
| # If we have routed browser output to file with --log_stdout and/or |
| # --log_stderr, these track the handles. |
| browser_stdout_handle = sys.stdout |
| browser_stderr_handle = sys.stderr |
| |
| # This flag tracks whether the html page has sent any stdout messages back to |
| # us. Used to detect whether we might have gotten detached from the browser |
| # process we spawned, in which case we are not able to detect when user closes |
| # the browser with the close button. |
| have_received_messages = False |
| |
| # At startup print a warning message once if user did not build with --emrun. |
| emrun_not_enabled_nag_printed = False |
| |
| # Stores the exit() code of the html page when/if it quits. |
| page_exit_code = None |
| |
| # If this is set to a non-empty string, all processes by this name will be |
| # killed at exit. This is used to clean up after browsers that spawn |
| # subprocesses to handle the actual browser launch. For example opera has a |
| # launcher.exe that runs the actual opera browser. So killing browser_process |
| # would just kill launcher.exe and not the opera |
| # browser itself. |
| processname_killed_atexit = "" |
| |
| # Using "0.0.0.0" means "all interfaces", which should allow connecting to this |
| # server via LAN addresses. Using "localhost" should allow only connecting from |
| # local computer. |
| default_webserver_hostname = '0.0.0.0' |
| |
| # If user does not specify a --port parameter, this port is used to launch the |
| # server. |
| default_webserver_port = 6931 |
| |
| # Location of Android Debug Bridge executable |
| ADB = None |
| |
| # Host OS detection to autolocate browsers and other OS-specific support needs. |
| WINDOWS = False |
| LINUX = False |
| MACOS = False |
| if os.name == 'nt': |
| WINDOWS = True |
| import winreg |
| elif platform.system() == 'Linux': |
| LINUX = True |
| elif platform.mac_ver()[0] != '': |
| MACOS = True |
| import plistlib |
| |
| # If you are running on an OS that is not any of these, must add explicit support for it. |
| if not WINDOWS and not LINUX and not MACOS: |
| raise Exception("Unknown OS!") |
| |
| |
| # Returns wallclock time in seconds. |
| def tick(): |
| # Would like to return time.clock() since it's apparently better for |
| # precision, but it is broken on macOS 10.10 and Python 2.7.8. |
| return time.time() |
| |
| |
| # Absolute wallclock time in seconds specifying when the previous HTTP stdout |
| # message from the page was received. |
| last_message_time = tick() |
| |
| # Absolute wallclock time in seconds telling when we launched emrun. |
| page_start_time = tick() |
| |
| # Stores the time of most recent http page serve. |
| page_last_served_time = None |
| |
| |
| def format_html(msg): |
| """Returns given log message formatted to be outputted on a HTML page.""" |
| if not msg.endswith('\n'): |
| msg += '\n' |
| msg = cgi.escape(msg) |
| msg = msg.replace('\r\n', '<br />').replace('\n', '<br />') |
| return msg |
| |
| |
| # HTTP requests are handled from separate threads - synchronize them to avoid race conditions |
| http_mutex = threading.RLock() |
| |
| |
| def logi(msg): |
| """Prints a log message to 'info' stdout channel. Always printed. |
| """ |
| global last_message_time |
| with http_mutex: |
| if emrun_options.log_html: |
| sys.stdout.write(format_html(msg)) |
| else: |
| print_to_handle(sys.stdout, msg) |
| sys.stdout.flush() |
| last_message_time = tick() |
| |
| |
| def logv(msg): |
| """Prints a verbose log message to stdout channel. |
| Only shown if run with --verbose. |
| """ |
| global last_message_time |
| if emrun_options.verbose: |
| with http_mutex: |
| if emrun_options.log_html: |
| sys.stdout.write(format_html(msg)) |
| else: |
| print_to_handle(sys.stdout, msg) |
| sys.stdout.flush() |
| last_message_time = tick() |
| |
| |
| def loge(msg): |
| """Prints an error message to stderr channel. |
| """ |
| global last_message_time |
| with http_mutex: |
| if emrun_options.log_html: |
| sys.stderr.write(format_html(msg)) |
| else: |
| print_to_handle(sys.stderr, msg) |
| sys.stderr.flush() |
| last_message_time = tick() |
| |
| |
| def format_eol(msg): |
| if WINDOWS: |
| msg = msg.replace('\r\n', '\n').replace('\n', '\r\n') |
| return msg |
| |
| |
| def browser_logi(msg): |
| """Prints a message to the browser stdout output stream. |
| """ |
| global last_message_time |
| msg = format_eol(msg) |
| print_to_handle(browser_stdout_handle, msg) |
| browser_stdout_handle.flush() |
| last_message_time = tick() |
| |
| |
| def browser_loge(msg): |
| """Prints a message to the browser stderr output stream. |
| """ |
| global last_message_time |
| msg = format_eol(msg) |
| print_to_handle(browser_stderr_handle, msg) |
| browser_stderr_handle.flush() |
| last_message_time = tick() |
| |
| |
| def unquote_u(source): |
| """Unquotes a unicode string. |
| (translates ascii-encoded utf string back to utf) |
| """ |
| result = unquote(source) |
| if '%u' in result: |
| result = result.replace('%u', '\\u').decode('unicode_escape') |
| return result |
| |
| |
| temp_firefox_profile_dir = None |
| |
| |
| def delete_emrun_safe_firefox_profile(): |
| """Deletes the temporary created Firefox profile (if one exists)""" |
| global temp_firefox_profile_dir |
| if temp_firefox_profile_dir is not None: |
| logv('remove_tree("' + temp_firefox_profile_dir + '")') |
| remove_tree(temp_firefox_profile_dir) |
| temp_firefox_profile_dir = None |
| |
| |
| # Firefox has a lot of default behavior that makes it unsuitable for |
| # automated/unattended run. |
| # This function creates a temporary profile directory that customized Firefox |
| # with various flags that enable automated runs. |
| def create_emrun_safe_firefox_profile(): |
| global temp_firefox_profile_dir |
| temp_firefox_profile_dir = tempfile.mkdtemp(prefix='temp_emrun_firefox_profile_') |
| with open(os.path.join(temp_firefox_profile_dir, 'prefs.js'), 'w') as f: |
| f.write(''' |
| // Lift the default max 20 workers limit to something higher to avoid hangs when page needs to spawn a lot of threads. |
| user_pref("dom.workers.maxPerDomain", 100); |
| // Always allow opening popups |
| user_pref("browser.popups.showPopupBlocker", false); |
| user_pref("dom.disable_open_during_load", false); |
| // Don't ask user if he wants to set Firefox as the default system browser |
| user_pref("browser.shell.checkDefaultBrowser", false); |
| user_pref("browser.shell.skipDefaultBrowserCheck", true); |
| // If automated runs crash, don't resume old tabs on the next run or show safe mode dialogs or anything else extra. |
| user_pref("browser.sessionstore.resume_from_crash", false); |
| user_pref("services.sync.prefs.sync.browser.sessionstore.restore_on_demand", false); |
| user_pref("browser.sessionstore.restore_on_demand", false); |
| user_pref("browser.sessionstore.max_resumed_crashes", -1); |
| user_pref("toolkit.startup.max_resumed_crashes", -1); |
| // Don't show the slow script dialog popup |
| user_pref("dom.max_script_run_time", 0); |
| user_pref("dom.max_chrome_script_run_time", 0); |
| // Don't open a home page at startup |
| user_pref("startup.homepage_override_url", "about:blank"); |
| user_pref("startup.homepage_welcome_url", "about:blank"); |
| user_pref("browser.startup.homepage", "about:blank"); |
| // Don't try to perform browser (auto)update on the background |
| user_pref("app.update.auto", false); |
| user_pref("app.update.enabled", false); |
| user_pref("app.update.silent", false); |
| user_pref("app.update.mode", 0); |
| user_pref("app.update.service.enabled", false); |
| // Don't check compatibility with add-ons, or (auto)update them |
| user_pref("extensions.lastAppVersion", ''); |
| user_pref("plugins.hide_infobar_for_outdated_plugin", true); |
| user_pref("plugins.update.url", ''); |
| // Disable health reporter |
| user_pref("datareporting.healthreport.service.enabled", false); |
| // Disable crash reporter |
| user_pref("toolkit.crashreporter.enabled", false); |
| // Don't show WhatsNew on first run after every update |
| user_pref("browser.startup.homepage_override.mstone","ignore"); |
| // Don't show 'know your rights' and a bunch of other nag windows at startup |
| user_pref("browser.rights.3.shown", true); |
| user_pref('devtools.devedition.promo.shown', true); |
| user_pref('extensions.shownSelectionUI', true); |
| user_pref('browser.newtabpage.introShown', true); |
| user_pref('browser.download.panel.shown', true); |
| user_pref('browser.customizemode.tip0.shown', true); |
| user_pref("browser.toolbarbuttons.introduced.pocket-button", true); |
| // Don't ask the user if he wants to close the browser when there are multiple tabs. |
| user_pref("browser.tabs.warnOnClose", false); |
| // Allow the launched script window to close itself, so that we don't need to kill the browser process in order to move on. |
| user_pref("dom.allow_scripts_to_close_windows", true); |
| // Set various update timers to a large value in the future in order to not |
| // trigger a large mass of update HTTP traffic on each Firefox run on the clean profile. |
| // 2147483647 seconds since Unix epoch is sometime in the year 2038, and this is the max integer accepted by Firefox. |
| user_pref("app.update.lastUpdateTime.addon-background-update-timer", 2147483647); |
| user_pref("app.update.lastUpdateTime.background-update-timer", 2147483647); |
| user_pref("app.update.lastUpdateTime.blocklist-background-update-timer", 2147483647); |
| user_pref("app.update.lastUpdateTime.browser-cleanup-thumbnails", 2147483647); |
| user_pref("app.update.lastUpdateTime.experiments-update-timer", 2147483647); |
| user_pref("app.update.lastUpdateTime.search-engine-update-timer", 2147483647); |
| user_pref("app.update.lastUpdateTime.xpi-signature-verification", 2147483647); |
| user_pref("extensions.getAddons.cache.lastUpdate", 2147483647); |
| user_pref("media.gmp-eme-adobe.lastUpdate", 2147483647); |
| user_pref("media.gmp-gmpopenh264.lastUpdate", 2147483647); |
| user_pref("datareporting.healthreport.nextDataSubmissionTime", "2147483647000"); |
| // Sending Firefox Health Report Telemetry data is not desirable, since these are automated runs. |
| user_pref("datareporting.healthreport.uploadEnabled", false); |
| user_pref("datareporting.healthreport.service.enabled", false); |
| user_pref("datareporting.healthreport.service.firstRun", false); |
| user_pref("toolkit.telemetry.enabled", false); |
| user_pref("toolkit.telemetry.unified", false); |
| user_pref("datareporting.policy.dataSubmissionEnabled", false); |
| user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true); |
| // Allow window.dump() to print directly to console |
| user_pref("browser.dom.window.dump.enabled", true); |
| // Disable background add-ons related update & information check pings |
| user_pref("extensions.update.enabled", false); |
| user_pref("extensions.getAddons.cache.enabled", false); |
| // Enable wasm |
| user_pref("javascript.options.wasm", true); |
| // Enable SharedArrayBuffer (this profile is for a testing environment, so Spectre/Meltdown don't apply) |
| user_pref("javascript.options.shared_memory", true); |
| ''') |
| if emrun_options.private_browsing: |
| f.write(''' |
| // Start in private browsing mode to not cache anything to disk (everything will be wiped anyway after this run) |
| user_pref("browser.privatebrowsing.autostart", true); |
| ''') |
| logv('create_emrun_safe_firefox_profile: Created new Firefox profile "' + temp_firefox_profile_dir + '"') |
| return temp_firefox_profile_dir |
| |
| |
| def is_browser_process_alive(): |
| """Returns whether the browser page we spawned is still running. |
| (note, not perfect atm, in case we are running in detached mode) |
| """ |
| # If navigation to the web page has not yet occurred, we behave as if the |
| # browser has not yet even loaded the page, and treat it as if the browser |
| # is running (as it is just starting up) |
| if not navigation_has_occurred: |
| return True |
| |
| if browser_process and browser_process.poll() is None: |
| return True |
| |
| if current_browser_processes: |
| try: |
| import psutil |
| for p in current_browser_processes: |
| if psutil.pid_exists(p['pid']): |
| return True |
| return False |
| except Exception: |
| # Fail gracefully if psutil not available |
| logv('psutil is not available, emrun may not be able to accurately track whether the browser process is alive or not') |
| |
| # We do not have a track of the browser process ID that we spawned. |
| # Make an assumption that the browser process is open as long until |
| # the C program calls exit(). |
| return page_exit_code is None |
| |
| |
| def kill_browser_process(): |
| """Kills browser_process and processname_killed_atexit. Also removes the |
| temporary Firefox profile that was created, if one exists. |
| """ |
| global browser_process, processname_killed_atexit, current_browser_processes |
| if browser_process and browser_process.poll() is None: |
| try: |
| logv('Terminating browser process pid=' + str(browser_process.pid) + '..') |
| browser_process.kill() |
| except Exception as e: |
| logv('Failed with error ' + str(e) + '!') |
| |
| browser_process = None |
| # We have a hold of the target browser process explicitly, no need to resort to killall, |
| # so clear that record out. |
| processname_killed_atexit = '' |
| |
| if current_browser_processes: |
| for pid in current_browser_processes: |
| try: |
| logv('Terminating browser process pid=' + str(pid['pid']) + '..') |
| os.kill(pid['pid'], 9) |
| except Exception as e: |
| logv('Failed with error ' + str(e) + '!') |
| |
| current_browser_processes = None |
| # We have a hold of the target browser process explicitly, no need to resort to killall, |
| # so clear that record out. |
| processname_killed_atexit = '' |
| |
| if len(processname_killed_atexit): |
| if emrun_options.android: |
| logv("Terminating Android app '" + processname_killed_atexit + "'.") |
| subprocess.call([ADB, 'shell', 'am', 'force-stop', processname_killed_atexit]) |
| else: |
| logv("Terminating all processes that have string '" + processname_killed_atexit + "' in their name.") |
| if WINDOWS: |
| process_image = processname_killed_atexit if '.exe' in processname_killed_atexit else (processname_killed_atexit + '.exe') |
| process = subprocess.Popen(['taskkill', '/F', '/IM', process_image, '/T'], stdout=subprocess.PIPE, stderr=subprocess.PIPE) |
| process.communicate() |
| else: |
| try: |
| subprocess.call(['pkill', processname_killed_atexit]) |
| except OSError: |
| try: |
| subprocess.call(['killall', processname_killed_atexit]) |
| except OSError: |
| loge('Both commands pkill and killall failed to clean up the spawned browser process. Perhaps neither of these utilities is available on your system?') |
| delete_emrun_safe_firefox_profile() |
| # Clear the process name to represent that the browser is now dead. |
| processname_killed_atexit = '' |
| |
| delete_emrun_safe_firefox_profile() |
| |
| |
| # Heuristic that attempts to search for the browser process IDs that emrun spawned. |
| # This depends on the assumption that no other browser process IDs have been spawned |
| # during the short time perioed between the time that emrun started, and the browser |
| # process navigated to the page. |
| # This heuristic is needed because all modern browsers are multiprocess systems - |
| # starting a browser process from command line generally launches just a "stub" spawner |
| # process that immediately exits. |
| def detect_browser_processes(): |
| if not browser_exe: |
| return # Running with --no_browser, we are not binding to a spawned browser. |
| |
| global current_browser_processes |
| logv('First navigation occurred. Identifying currently running browser processes') |
| running_browser_processes = list_processes_by_name(browser_exe) |
| |
| def pid_existed(pid): |
| for proc in previous_browser_processes: |
| if proc['pid'] == pid: |
| return True |
| return False |
| |
| for p in running_browser_processes: |
| logv('Detected running browser process id: ' + str(p['pid']) + ', existed already at emrun startup? ' + str(pid_existed(p['pid']))) |
| |
| current_browser_processes = [p for p in running_browser_processes if not pid_existed(p['pid'])] |
| |
| if len(current_browser_processes) == 0: |
| logv('Was unable to detect the browser process that was spawned by emrun. This may occur if the target page was opened in a tab on a browser process that already existed before emrun started up.') |
| |
| |
| # Our custom HTTP web server that will server the target page to run via .html. |
| # This is used so that we can load the page via a http:// URL instead of a |
| # file:// URL, since those wouldn't work too well unless user allowed XHR |
| # without CORS rules. Also, the target page will route its stdout and stderr |
| # back to here via HTTP requests. |
| class HTTPWebServer(socketserver.ThreadingMixIn, HTTPServer): |
| """Log messaging arriving via HTTP can come in out of sequence. Implement a |
| sequencing mechanism to enforce ordered transmission.""" |
| expected_http_seq_num = 1 |
| # Stores messages that have arrived out of order, pending for a send as soon |
| # as the missing message arrives. Kept in sorted order, first element is the |
| # oldest message received. |
| http_message_queue = [] |
| |
| def handle_incoming_message(self, seq_num, log, data): |
| global have_received_messages |
| with http_mutex: |
| have_received_messages = True |
| |
| if seq_num == -1: |
| # Message arrived without a sequence number? Just log immediately |
| log(data) |
| elif seq_num == self.expected_http_seq_num: |
| log(data) |
| self.expected_http_seq_num += 1 |
| self.print_messages_due() |
| elif seq_num < self.expected_http_seq_num: |
| log(data) |
| else: |
| self.http_message_queue += [(seq_num, data, log)] |
| self.http_message_queue.sort(key=itemgetter(0)) |
| if len(self.http_message_queue) > 16: |
| self.print_next_message() |
| |
| # If it's been too long since we we got a message, prints out the oldest |
| # queued message, ignoring the proper order. This ensures that if any |
| # messages are actually lost, that the message queue will be orderly flushed. |
| def print_timed_out_messages(self): |
| global last_message_time |
| with http_mutex: |
| now = tick() |
| max_message_queue_time = 5 |
| if len(self.http_message_queue) and now - last_message_time > max_message_queue_time: |
| self.print_next_message() |
| |
| # Skips to printing the next message in queue now, independent of whether |
| # there was missed messages in the sequence numbering. |
| def print_next_message(self): |
| with http_mutex: |
| if len(self.http_message_queue): |
| self.expected_http_seq_num = self.http_message_queue[0][0] |
| self.print_messages_due() |
| |
| # Completely flushes all out-of-order messages in the queue. |
| def print_all_messages(self): |
| with http_mutex: |
| while len(self.http_message_queue): |
| self.print_next_message() |
| |
| # Prints any messages that are now due after we logged some other previous |
| # messages. |
| def print_messages_due(self): |
| with http_mutex: |
| while len(self.http_message_queue): |
| msg = self.http_message_queue[0] |
| if msg[0] == self.expected_http_seq_num: |
| msg[2](msg[1]) |
| self.expected_http_seq_num += 1 |
| self.http_message_queue.pop(0) |
| else: |
| return |
| |
| def serve_forever(self, timeout=0.5): |
| global last_message_time, page_exit_code, emrun_not_enabled_nag_printed |
| self.is_running = True |
| self.timeout = timeout |
| logv("Entering web server loop.") |
| while self.is_running: |
| now = tick() |
| # Did user close browser? |
| if not emrun_options.no_browser and not is_browser_process_alive(): |
| logv("Shutting down because browser is no longer alive") |
| delete_emrun_safe_firefox_profile() |
| if not emrun_options.serve_after_close: |
| logv("Browser process has shut down, quitting web server.") |
| self.is_running = False |
| |
| # Serve HTTP |
| self.handle_request() |
| # Process message log queue |
| self.print_timed_out_messages() |
| |
| # If web page was silent for too long without printing anything, kill process. |
| time_since_message = now - last_message_time |
| if emrun_options.silence_timeout != 0 and time_since_message > emrun_options.silence_timeout: |
| self.shutdown() |
| logi('No activity in ' + str(emrun_options.silence_timeout) + ' seconds. Quitting web server with return code ' + str(emrun_options.timeout_returncode) + '. (--silence_timeout option)') |
| page_exit_code = emrun_options.timeout_returncode |
| emrun_options.kill_exit = True |
| |
| # If the page has been running too long as a whole, kill process. |
| time_since_start = now - page_start_time |
| if emrun_options.timeout != 0 and time_since_start > emrun_options.timeout: |
| self.shutdown() |
| logi('Page has not finished in ' + str(emrun_options.timeout) + ' seconds. Quitting web server with return code ' + str(emrun_options.timeout_returncode) + '. (--timeout option)') |
| emrun_options.kill_exit = True |
| page_exit_code = emrun_options.timeout_returncode |
| |
| # If we detect that the page is not running with emrun enabled, print a warning message. |
| if not emrun_not_enabled_nag_printed and page_last_served_time is not None: |
| time_since_page_serve = now - page_last_served_time |
| if not have_received_messages and time_since_page_serve > 10: |
| logv('The html page you are running is not emrun-capable. Stdout, stderr and exit(returncode) capture will not work. Recompile the application with the --emrun linker flag to enable this, or pass --no_emrun_detect to emrun to hide this check.') |
| emrun_not_enabled_nag_printed = True |
| |
| # Clean up at quit, print any leftover messages in queue. |
| self.print_all_messages() |
| logv("Web server loop done.") |
| |
| def handle_error(self, request, client_address): |
| err = sys.exc_info()[1].args[0] |
| # Filter out the useless '[Errno 10054] An existing connection was forcibly |
| # closed by the remote host' errors that occur when we forcibly kill the |
| # client. |
| if err != 10054: |
| socketserver.BaseServer.handle_error(self, request, client_address) |
| |
| def shutdown(self): |
| self.is_running = False |
| self.print_all_messages() |
| return 1 |
| |
| |
| # Processes HTTP request back to the browser. |
| class HTTPHandler(SimpleHTTPRequestHandler): |
| def send_head(self): |
| self.protocol_version = 'HTTP/1.1' |
| global page_last_served_time |
| path = self.translate_path(self.path) |
| f = None |
| |
| # A browser has navigated to this page - check which PID got spawned for |
| # the browser |
| global navigation_has_occurred |
| if not navigation_has_occurred and current_browser_processes is None: |
| detect_browser_processes() |
| |
| navigation_has_occurred = True |
| |
| if os.path.isdir(path): |
| if not self.path.endswith('/'): |
| self.send_response(301) |
| self.send_header("Location", self.path + "/") |
| self.end_headers() |
| return None |
| for index in "index.html", "index.htm": |
| index = os.path.join(path, index) |
| if os.path.isfile(index): |
| path = index |
| break |
| else: |
| # Manually implement directory listing support. |
| return self.list_directory(path) |
| |
| try: |
| f = open(path, 'rb') |
| except IOError: |
| self.send_error(404, "File not found: " + path) |
| return None |
| |
| self.send_response(200) |
| guess_file_type = path |
| # All files of type x.gz are served as gzip-compressed, which means the |
| # browser will transparently decode the file before passing the |
| # uncompressed bytes to the JS page. |
| # Note: In a slightly silly manner, detect files ending with "gz" and not |
| # ".gz", since both Unity and UE4 generate multiple files with .jsgz, |
| # .datagz, .memgz, .symbolsgz suffixes and so on, so everything goes. |
| # Note 2: If the JS application would like to receive the actual bits of a |
| # gzipped file, instead of having the browser decompress it immediately, |
| # then it can't use the suffix .gz when using emrun. |
| # To work around, one can use the suffix .gzip instead. |
| if 'Accept-Encoding' in self.headers and 'gzip' in self.headers['Accept-Encoding'] and path.lower().endswith('gz'): |
| self.send_header('Content-Encoding', 'gzip') |
| logv('Serving ' + path + ' as gzip-compressed.') |
| guess_file_type = guess_file_type[:-2] |
| if guess_file_type.endswith('.'): |
| guess_file_type = guess_file_type[:-1] |
| |
| ctype = self.guess_type(guess_file_type) |
| if guess_file_type.lower().endswith('.wasm'): |
| ctype = 'application/wasm' |
| if guess_file_type.lower().endswith('.js'): |
| ctype = 'application/javascript' |
| self.send_header('Content-type', ctype) |
| fs = os.fstat(f.fileno()) |
| self.send_header("Content-Length", str(fs[6])) |
| self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) |
| self.send_header('Cache-Control', 'no-cache, must-revalidate') |
| self.send_header('Connection', 'close') |
| self.send_header('Expires', '-1') |
| self.send_header('Access-Control-Allow-Origin', '*') |
| self.send_header('Cross-Origin-Opener-Policy', 'same-origin') |
| self.send_header('Cross-Origin-Embedder-Policy', 'require-corp') |
| self.send_header('Cross-Origin-Resource-Policy', 'cross-origin') |
| self.end_headers() |
| page_last_served_time = tick() |
| return f |
| |
| def log_request(self, code): |
| # Filter out 200 OK messages to remove noise. |
| if code != 200: |
| SimpleHTTPRequestHandler.log_request(self, code) |
| |
| def log_message(self, format, *args): |
| msg = '%s - - [%s] %s\n' % (self.address_string(), self.log_date_time_string(), format % args) |
| # Filter out 404 messages on favicon.ico not being found to remove noise. |
| if 'favicon.ico' not in msg: |
| sys.stderr.write(msg) |
| |
| def do_POST(self): |
| self.protocol_version = 'HTTP/1.1' |
| global page_exit_code, have_received_messages |
| |
| (_, _, path, query, _) = urlsplit(self.path) |
| logv('POST: "' + self.path + '" (path: "' + path + '", query: "' + query + '")') |
| if query.startswith('file='): |
| # Binary file dump/upload handling. Requests to |
| # "stdio.html?file=filename" will write binary data to the given file. |
| data = self.rfile.read(int(self.headers['Content-Length'])) |
| filename = query[len('file='):] |
| dump_out_directory = 'dump_out' |
| try: |
| os.mkdir(dump_out_directory) |
| except OSError: |
| pass |
| filename = os.path.join(dump_out_directory, os.path.normpath(filename)) |
| with open(filename, 'wb') as fh: |
| fh.write(data) |
| logi('Wrote ' + str(len(data)) + ' bytes to file "' + filename + '".') |
| have_received_messages = True |
| elif path == '/system_info': |
| system_info = json.loads(get_system_info(format_json=True)) |
| try: |
| browser_info = json.loads(get_browser_info(browser_exe, format_json=True)) |
| except ValueError: |
| browser_info = '' |
| data = {'system': system_info, 'browser': browser_info} |
| self.send_response(200) |
| self.send_header('Content-type', 'application/json') |
| self.send_header('Cache-Control', 'no-cache, must-revalidate') |
| self.send_header('Connection', 'close') |
| self.send_header('Expires', '-1') |
| self.end_headers() |
| self.wfile.write(json.dumps(data)) |
| return |
| else: |
| data = self.rfile.read(int(self.headers['Content-Length'])) |
| if str is not bytes and isinstance(data, bytes): |
| data = data.decode('utf-8') |
| data = data.replace("+", " ") |
| data = unquote_u(data) |
| |
| if data == '^pageload^': # Browser is just notifying that it has successfully launched the page. |
| have_received_messages = True |
| elif data.startswith('^exit^'): |
| if not emrun_options.serve_after_exit: |
| page_exit_code = int(data[6:]) |
| logv('Web page has quit with a call to exit() with return code ' + str(page_exit_code) + '. Shutting down web server. Pass --serve_after_exit to keep serving even after the page terminates with exit().') |
| self.server.shutdown() |
| return |
| else: |
| # The user page sent a message with POST. Parse the message and log it to stdout/stderr. |
| is_stdout = False |
| is_stderr = False |
| seq_num = -1 |
| # The html shell is expected to send messages of form ^out^(number)^(message) or ^err^(number)^(message). |
| if data.startswith('^err^'): |
| is_stderr = True |
| elif data.startswith('^out^'): |
| is_stdout = True |
| if is_stderr or is_stdout: |
| try: |
| i = data.index('^', 5) |
| seq_num = int(data[5:i]) |
| data = data[i + 1:] |
| except ValueError: |
| pass |
| |
| log = browser_loge if is_stderr else browser_logi |
| self.server.handle_incoming_message(seq_num, log, data) |
| |
| self.send_response(200) |
| self.send_header('Content-type', 'text/plain') |
| self.send_header('Cache-Control', 'no-cache, must-revalidate') |
| self.send_header('Connection', 'close') |
| self.send_header('Expires', '-1') |
| self.end_headers() |
| self.wfile.write(b'OK') |
| |
| |
| # Returns stdout by running command with universal_newlines=True |
| def check_output(cmd, universal_newlines=True, *args, **kwargs): |
| if hasattr(subprocess, "run"): |
| return subprocess.run(cmd, universal_newlines=universal_newlines, stdout=subprocess.PIPE, check=True, *args, **kwargs).stdout |
| else: |
| # check_output is considered as an old API so prefer subprocess.run if possible |
| return subprocess.check_output(cmd, universal_newlines=universal_newlines, *args, **kwargs) |
| |
| |
| # From http://stackoverflow.com/questions/4842448/getting-processor-information-in-python |
| # Returns a string with something like "AMD64, Intel(R) Core(TM) i5-2557M CPU @ |
| # 1.70GHz, Intel64 Family 6 Model 42 Stepping 7, GenuineIntel" |
| def get_cpu_info(): |
| physical_cores = 1 |
| logical_cores = 1 |
| frequency = 0 |
| try: |
| if WINDOWS: |
| from win32com.client import GetObject |
| root_winmgmts = GetObject('winmgmts:root\\cimv2') |
| cpus = root_winmgmts.ExecQuery('Select * from Win32_Processor') |
| cpu_name = cpus[0].Name + ', ' + platform.processor() |
| physical_cores = int(check_output(['wmic', 'cpu', 'get', 'NumberOfCores']).split('\n')[1].strip()) |
| logical_cores = int(check_output(['wmic', 'cpu', 'get', 'NumberOfLogicalProcessors']).split('\n')[1].strip()) |
| frequency = int(check_output(['wmic', 'cpu', 'get', 'MaxClockSpeed']).split('\n')[1].strip()) |
| elif MACOS: |
| cpu_name = check_output(['sysctl', '-n', 'machdep.cpu.brand_string']).strip() |
| physical_cores = int(check_output(['sysctl', '-n', 'machdep.cpu.core_count']).strip()) |
| logical_cores = int(check_output(['sysctl', '-n', 'machdep.cpu.thread_count']).strip()) |
| frequency = int(check_output(['sysctl', '-n', 'hw.cpufrequency']).strip()) // 1000000 |
| elif LINUX: |
| all_info = check_output(['cat', '/proc/cpuinfo']).strip() |
| for line in all_info.split("\n"): |
| if 'model name' in line: |
| cpu_name = re.sub('.*model name.*:', '', line, 1).strip() |
| lscpu = check_output(['lscpu']) |
| frequency = int(float(re.search('CPU MHz: (.*)', lscpu).group(1).strip()) + 0.5) |
| sockets = int(re.search(r'Socket\(s\): (.*)', lscpu).group(1).strip()) |
| physical_cores = sockets * int(re.search(r'Core\(s\) per socket: (.*)', lscpu).group(1).strip()) |
| logical_cores = physical_cores * int(re.search(r'Thread\(s\) per core: (.*)', lscpu).group(1).strip()) |
| except Exception as e: |
| import traceback |
| loge(traceback.format_exc()) |
| return {'model': 'Unknown ("' + str(e) + '")', |
| 'physicalCores': 1, |
| 'logicalCores': 1, |
| 'frequency': 0 |
| } |
| |
| return {'model': platform.machine() + ', ' + cpu_name, |
| 'physicalCores': physical_cores, |
| 'logicalCores': logical_cores, |
| 'frequency': frequency |
| } |
| |
| |
| def get_android_cpu_infoline(): |
| lines = check_output([ADB, 'shell', 'cat', '/proc/cpuinfo']).split('\n') |
| processor = '' |
| hardware = '' |
| for line in lines: |
| if line.startswith('Processor'): |
| processor = line[line.find(':') + 1:].strip() |
| elif line.startswith('Hardware'): |
| hardware = line[line.find(':') + 1:].strip() |
| |
| freq = int(check_output([ADB, 'shell', 'cat', '/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq']).strip()) // 1000 |
| return 'CPU: ' + processor + ', ' + hardware + ' @ ' + str(freq) + ' MHz' |
| |
| |
| def win_get_gpu_info(): |
| gpus = [] |
| |
| def find_gpu_model(model): |
| for gpu in gpus: |
| if gpu['model'] == model: |
| return gpu |
| return None |
| |
| for i in range(0, 16): |
| try: |
| hHardwareReg = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'HARDWARE') |
| hDeviceMapReg = winreg.OpenKey(hHardwareReg, 'DEVICEMAP') |
| hVideoReg = winreg.OpenKey(hDeviceMapReg, 'VIDEO') |
| VideoCardString = winreg.QueryValueEx(hVideoReg, '\\Device\\Video' + str(i))[0] |
| # Get Rid of Registry/Machine from the string |
| VideoCardStringSplit = VideoCardString.split('\\') |
| ClearnVideoCardString = "\\".join(VideoCardStringSplit[3:]) |
| # Go up one level for detailed |
| # VideoCardStringRoot = "\\".join(VideoCardStringSplit[3:len(VideoCardStringSplit)-1]) |
| |
| # Get the graphics card information |
| hVideoCardReg = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, ClearnVideoCardString) |
| try: |
| VideoCardDescription = winreg.QueryValueEx(hVideoCardReg, 'Device Description')[0] |
| except WindowsError: |
| VideoCardDescription = winreg.QueryValueEx(hVideoCardReg, 'DriverDesc')[0] |
| |
| try: |
| driverVersion = winreg.QueryValueEx(hVideoCardReg, 'DriverVersion')[0] |
| VideoCardDescription += ', driver version ' + driverVersion |
| except WindowsError: |
| pass |
| |
| try: |
| driverDate = winreg.QueryValueEx(hVideoCardReg, 'DriverDate')[0] |
| VideoCardDescription += ' (' + driverDate + ')' |
| except WindowsError: |
| pass |
| |
| VideoCardMemorySize = winreg.QueryValueEx(hVideoCardReg, 'HardwareInformation.MemorySize')[0] |
| try: |
| vram = struct.unpack('l', bytes(VideoCardMemorySize))[0] |
| except struct.error: |
| vram = int(VideoCardMemorySize) |
| if not find_gpu_model(VideoCardDescription): |
| gpus += [{'model': VideoCardDescription, 'ram': vram}] |
| except WindowsError: |
| pass |
| return gpus |
| |
| |
| def linux_get_gpu_info(): |
| glinfo = '' |
| try: |
| glxinfo = check_output('glxinfo') |
| for line in glxinfo.split("\n"): |
| if "OpenGL vendor string:" in line: |
| gl_vendor = line[len("OpenGL vendor string:"):].strip() |
| if "OpenGL version string:" in line: |
| gl_version = line[len("OpenGL version string:"):].strip() |
| if "OpenGL renderer string:" in line: |
| gl_renderer = line[len("OpenGL renderer string:"):].strip() |
| glinfo = gl_vendor + ' ' + gl_renderer + ', GL version ' + gl_version |
| except Exception as e: |
| logv(e) |
| |
| adapterinfo = '' |
| try: |
| vgainfo = check_output(['lshw', '-C', 'display'], stderr=subprocess.PIPE) |
| vendor = re.search("vendor: (.*)", vgainfo).group(1).strip() |
| product = re.search("product: (.*)", vgainfo).group(1).strip() |
| description = re.search("description: (.*)", vgainfo).group(1).strip() |
| clock = re.search("clock: (.*)", vgainfo).group(1).strip() |
| adapterinfo = vendor + ' ' + product + ', ' + description + ' (' + clock + ')' |
| except Exception as e: |
| logv(e) |
| |
| ram = 0 |
| try: |
| vgainfo = check_output('lspci -v -s $(lspci | grep VGA | cut -d " " -f 1)', shell=True, stderr=subprocess.PIPE) |
| ram = int(re.search(r"\[size=([0-9]*)M\]", vgainfo).group(1)) * 1024 * 1024 |
| except Exception as e: |
| logv(e) |
| |
| model = (adapterinfo + ' ' + glinfo).strip() |
| if not model: |
| model = 'Unknown' |
| return [{'model': model, 'ram': ram}] |
| |
| |
| def macos_get_gpu_info(): |
| gpus = [] |
| try: |
| info = check_output(['system_profiler', 'SPDisplaysDataType']) |
| info = info.split("Chipset Model:")[1:] |
| for gpu in info: |
| model_name = gpu.split('\n')[0].strip() |
| bus = re.search("Bus: (.*)", gpu).group(1).strip() |
| memory = int(re.search("VRAM (.*?): (.*) MB", gpu).group(2).strip()) |
| gpus += [{'model': model_name + ' (' + bus + ')', 'ram': memory * 1024 * 1024}] |
| return gpus |
| except Exception: |
| pass |
| |
| |
| def get_gpu_info(): |
| if WINDOWS: |
| return win_get_gpu_info() |
| elif LINUX: |
| return linux_get_gpu_info() |
| elif MACOS: |
| return macos_get_gpu_info() |
| else: |
| return [] |
| |
| |
| def get_executable_version(filename): |
| try: |
| if WINDOWS: |
| import win32api |
| info = win32api.GetFileVersionInfo(filename, "\\") |
| ms = info['FileVersionMS'] |
| ls = info['FileVersionLS'] |
| version = win32api.HIWORD(ms), win32api.LOWORD(ms), win32api.HIWORD(ls), win32api.LOWORD(ls) |
| return '.'.join(map(str, version)) |
| elif MACOS: |
| plistfile = filename[0:filename.find('MacOS')] + 'Info.plist' |
| info = plistlib.readPlist(plistfile) |
| # Data in Info.plists is a bit odd, this check combo gives best information on each browser. |
| if 'firefox' in filename.lower(): |
| return info['CFBundleShortVersionString'] |
| if 'opera' in filename.lower(): |
| return info['CFBundleVersion'] |
| else: |
| return info['CFBundleShortVersionString'] |
| elif LINUX: |
| if 'firefox' in filename.lower(): |
| version = check_output([filename, '-v']) |
| version = version.replace('Mozilla Firefox ', '') |
| return version.strip() |
| else: |
| return "" |
| except Exception as e: |
| logv(e) |
| return "" |
| |
| |
| def get_browser_build_date(filename): |
| try: |
| if MACOS: |
| plistfile = filename[0:filename.find('MacOS')] + 'Info.plist' |
| info = plistlib.readPlist(plistfile) |
| # Data in Info.plists is a bit odd, this check combo gives best information on each browser. |
| if 'firefox' in filename.lower(): |
| return '20' + '-'.join(map((lambda x: x.zfill(2)), info['CFBundleVersion'][2:].split('.'))) |
| except Exception as e: |
| logv(e) |
| |
| # No exact information about the build date, so take the last modified date of the file. |
| # This is not right, but assuming that one installed the browser shortly after the update was |
| # available, it's shooting close. |
| try: |
| return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(os.path.getmtime(filename))) |
| except Exception as e: |
| logv(e) |
| return '(unknown)' |
| |
| |
| def get_browser_info(filename, format_json): |
| if format_json: |
| return json.dumps({ |
| 'name': browser_display_name(filename), |
| 'version': get_executable_version(filename), |
| 'buildDate': get_browser_build_date(filename) |
| }, indent=2) |
| else: |
| return 'Browser: ' + browser_display_name(filename) + ' ' + get_executable_version(filename) + ', build ' + get_browser_build_date(filename) |
| |
| |
| # http://stackoverflow.com/questions/580924/python-windows-file-version-attribute |
| def win_get_file_properties(fname): |
| propNames = ('Comments', 'InternalName', 'ProductName', |
| 'CompanyName', 'LegalCopyright', 'ProductVersion', |
| 'FileDescription', 'LegalTrademarks', 'PrivateBuild', |
| 'FileVersion', 'OriginalFilename', 'SpecialBuild') |
| |
| props = {'FixedFileInfo': None, 'StringFileInfo': None, 'FileVersion': None} |
| |
| import win32api |
| # backslash as parm returns dictionary of numeric info corresponding to VS_FIXEDFILEINFO struc |
| fixedInfo = win32api.GetFileVersionInfo(fname, '\\') |
| props['FixedFileInfo'] = fixedInfo |
| props['FileVersion'] = "%d.%d.%d.%d" % (fixedInfo['FileVersionMS'] / 65536, |
| fixedInfo['FileVersionMS'] % 65536, |
| fixedInfo['FileVersionLS'] / 65536, |
| fixedInfo['FileVersionLS'] % 65536) |
| |
| # \VarFileInfo\Translation returns list of available (language, codepage) |
| # pairs that can be used to retreive string info. We are using only the first pair. |
| lang, codepage = win32api.GetFileVersionInfo(fname, '\\VarFileInfo\\Translation')[0] |
| |
| # any other must be of the form \StringfileInfo\%04X%04X\parm_name, middle |
| # two are language/codepage pair returned from above |
| |
| strInfo = {} |
| for propName in propNames: |
| strInfoPath = u'\\StringFileInfo\\%04X%04X\\%s' % (lang, codepage, propName) |
| ## print str_info |
| strInfo[propName] = win32api.GetFileVersionInfo(fname, strInfoPath) |
| |
| props['StringFileInfo'] = strInfo |
| |
| return props |
| |
| |
| def get_computer_model(): |
| try: |
| if MACOS: |
| try: |
| with open(os.path.join(os.getenv("HOME"), '.emrun.hwmodel.cached'), 'r') as f: |
| model = f.read() |
| return model |
| except IOError: |
| pass |
| |
| try: |
| # http://apple.stackexchange.com/questions/98080/can-a-macs-model-year-be-determined-via-terminal-command |
| serial = check_output(['system_profiler', 'SPHardwareDataType']) |
| serial = re.search("Serial Number (.*): (.*)", serial) |
| serial = serial.group(2).strip()[-4:] |
| cmd = ['curl', '-s', 'http://support-sp.apple.com/sp/product?cc=' + serial] |
| logv(str(cmd)) |
| model = check_output(cmd) |
| model = re.search('<configCode>(.*)</configCode>', model) |
| model = model.group(1).strip() |
| with open(os.path.join(os.getenv("HOME"), '.emrun.hwmodel.cached'), 'w') as fh: |
| fh.write(model) # Cache the hardware model to disk |
| return model |
| except Exception: |
| hwmodel = check_output(['sysctl', 'hw.model']) |
| hwmodel = re.search('hw.model: (.*)', hwmodel).group(1).strip() |
| return hwmodel |
| elif WINDOWS: |
| manufacturer = check_output(['wmic', 'baseboard', 'get', 'manufacturer']).split('\n')[1].strip() |
| version = check_output(['wmic', 'baseboard', 'get', 'version']).split('\n')[1].strip() |
| product = check_output(['wmic', 'baseboard', 'get', 'product']).split('\n')[1].strip() |
| if 'Apple' in manufacturer: |
| return manufacturer + ' ' + version + ', ' + product |
| else: |
| return manufacturer + ' ' + product + ', ' + version |
| elif LINUX: |
| board_vendor = check_output(['cat', '/sys/devices/virtual/dmi/id/board_vendor']).strip() |
| board_name = check_output(['cat', '/sys/devices/virtual/dmi/id/board_name']).strip() |
| board_version = check_output(['cat', '/sys/devices/virtual/dmi/id/board_version']).strip() |
| |
| bios_vendor = check_output(['cat', '/sys/devices/virtual/dmi/id/bios_vendor']).strip() |
| bios_version = check_output(['cat', '/sys/devices/virtual/dmi/id/bios_version']).strip() |
| bios_date = check_output(['cat', '/sys/devices/virtual/dmi/id/bios_date']).strip() |
| return board_vendor + ' ' + board_name + ' ' + board_version + ', ' + bios_vendor + ' ' + bios_version + ' (' + bios_date + ')' |
| except Exception as e: |
| logv(str(e)) |
| return 'Generic' |
| |
| |
| def get_os_version(): |
| bitness = ' (64bit)' if platform.machine() in ['AMD64', 'x86_64'] else ' (32bit)' |
| try: |
| if WINDOWS: |
| versionHandle = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion") |
| productName = winreg.QueryValueEx(versionHandle, "ProductName") |
| |
| version = '' |
| try: |
| version = ' ' + check_output(['wmic', 'os', 'get', 'version']).split('\n')[1].strip() |
| except Exception: |
| pass |
| return productName[0] + version + bitness |
| elif MACOS: |
| return 'macOS ' + platform.mac_ver()[0] + bitness |
| elif LINUX: |
| kernel_version = check_output(['uname', '-r']).strip() |
| return ' '.join(platform.linux_distribution()) + ', linux kernel ' + kernel_version + ' ' + platform.architecture()[0] + bitness |
| except Exception: |
| return 'Unknown OS' |
| |
| |
| def get_system_memory(): |
| try: |
| if LINUX or emrun_options.android: |
| if emrun_options.android: |
| lines = check_output([ADB, 'shell', 'cat', '/proc/meminfo']).split('\n') |
| else: |
| mem = open('/proc/meminfo', 'r') |
| lines = mem.readlines() |
| mem.close() |
| for i in lines: |
| sline = i.split() |
| if str(sline[0]) == 'MemTotal:': |
| return int(sline[1]) * 1024 |
| elif WINDOWS: |
| import win32api |
| return win32api.GlobalMemoryStatusEx()['TotalPhys'] |
| elif MACOS: |
| return int(check_output(['sysctl', '-n', 'hw.memsize']).strip()) |
| except Exception: |
| return -1 |
| |
| |
| # Finds the given executable 'program' in PATH. Operates like the Unix tool 'which'. |
| def which(program): |
| def is_exe(fpath): |
| return os.path.isfile(fpath) and os.access(fpath, os.X_OK) |
| |
| fpath, fname = os.path.split(program) |
| if fpath: |
| if is_exe(program): |
| return program |
| else: |
| for path in os.environ["PATH"].split(os.pathsep): |
| path = path.strip('"') |
| exe_file = os.path.join(path, program) |
| if is_exe(exe_file): |
| return exe_file |
| |
| if WINDOWS and '.' not in fname: |
| if is_exe(exe_file + '.exe'): |
| return exe_file + '.exe' |
| if is_exe(exe_file + '.cmd'): |
| return exe_file + '.cmd' |
| if is_exe(exe_file + '.bat'): |
| return exe_file + '.bat' |
| |
| return None |
| |
| |
| def win_get_default_browser(): |
| # Look in the registry for the default system browser on Windows without relying on |
| # 'start %1' since that method has an issue, see comment below. |
| try: |
| with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Classes\http\shell\open\command") as key: |
| cmd = winreg.QueryValue(key, None) |
| if cmd: |
| parts = shlex.split(cmd) |
| if len(parts): |
| return [parts[0]] |
| except WindowsError: |
| logv("Unable to find default browser key in Windows registry. Trying fallback.") |
| |
| # Fall back to 'start "" %1', which we have to treat as if user passed --serve_forever, since |
| # for some reason, we are not able to detect when the browser closes when this is passed. |
| # |
| # If the first argument to 'start' is quoted, then 'start' will create a new cmd.exe window with |
| # that quoted string as the title. If the URL contained spaces, it would be quoted by subprocess, |
| # and if we did 'start %1', it would create a new cmd.exe window with the URL as title instead of |
| # actually launching the browser. Therefore, we must pass a dummy quoted first argument for start |
| # to interpret as the title. For this purpose, we use the empty string, which will be quoted |
| # as "". See #9253 for details. |
| return ['cmd', '/C', 'start', ''] |
| |
| |
| def find_browser(name): |
| if WINDOWS and name == 'start': |
| return win_get_default_browser() |
| if MACOS and name == 'open': |
| return [name] |
| |
| if os.path.isfile(os.path.abspath(name)): |
| return [name] |
| if os.path.isfile(os.path.abspath(name) + '.exe'): |
| return [os.path.abspath(name) + '.exe'] |
| if os.path.isfile(os.path.abspath(name) + '.cmd'): |
| return [os.path.abspath(name) + '.cmd'] |
| if os.path.isfile(os.path.abspath(name) + '.bat'): |
| return [os.path.abspath(name) + '.bat'] |
| |
| path_lookup = which(name) |
| if path_lookup is not None: |
| return [path_lookup] |
| |
| browser_locations = [] |
| if MACOS: |
| # Note: by default Firefox beta installs as 'Firefox.app', you must manually rename it to |
| # FirefoxBeta.app after installation. |
| browser_locations = [('firefox', '/Applications/Firefox.app/Contents/MacOS/firefox'), |
| ('firefox_beta', '/Applications/FirefoxBeta.app/Contents/MacOS/firefox'), |
| ('firefox_aurora', '/Applications/FirefoxAurora.app/Contents/MacOS/firefox'), |
| ('firefox_nightly', '/Applications/FirefoxNightly.app/Contents/MacOS/firefox'), |
| ('safari', '/Applications/Safari.app/Contents/MacOS/Safari'), |
| ('opera', '/Applications/Opera.app/Contents/MacOS/Opera'), |
| ('chrome', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'), |
| ('chrome_canary', '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary')] |
| elif WINDOWS: |
| pf_locations = ['ProgramFiles(x86)', 'ProgramFiles', 'ProgramW6432'] |
| |
| for pf_env in pf_locations: |
| if pf_env not in os.environ: |
| continue |
| program_files = os.environ[pf_env] if WINDOWS else '' |
| |
| browser_locations += [('chrome', os.path.join(program_files, 'Google/Chrome/Application/chrome.exe')), |
| ('chrome_canary', os.path.expanduser("~/AppData/Local/Google/Chrome SxS/Application/chrome.exe")), |
| ('firefox_nightly', os.path.join(program_files, 'Nightly/firefox.exe')), |
| ('firefox_aurora', os.path.join(program_files, 'Aurora/firefox.exe')), |
| ('firefox_beta', os.path.join(program_files, 'Beta/firefox.exe')), |
| ('firefox_beta', os.path.join(program_files, 'FirefoxBeta/firefox.exe')), |
| ('firefox_beta', os.path.join(program_files, 'Firefox Beta/firefox.exe')), |
| ('firefox', os.path.join(program_files, 'Mozilla Firefox/firefox.exe')), |
| ('iexplore', os.path.join(program_files, 'Internet Explorer/iexplore.exe')), |
| ('opera', os.path.join(program_files, 'Opera/launcher.exe'))] |
| |
| elif LINUX: |
| browser_locations = [('firefox', os.path.expanduser('~/firefox/firefox')), |
| ('firefox_beta', os.path.expanduser('~/firefox_beta/firefox')), |
| ('firefox_aurora', os.path.expanduser('~/firefox_aurora/firefox')), |
| ('firefox_nightly', os.path.expanduser('~/firefox_nightly/firefox')), |
| ('chrome', which('google-chrome-stable')), |
| ('chrome', which('google-chrome'))] |
| |
| for alias, browser_exe in browser_locations: |
| if name == alias: |
| if browser_exe is not None and os.path.isfile(browser_exe): |
| return [browser_exe] |
| |
| return None # Could not find the browser |
| |
| |
| def get_android_model(): |
| manufacturer = check_output([ADB, 'shell', 'getprop', 'ro.product.manufacturer']).strip() |
| brand = check_output([ADB, 'shell', 'getprop', 'ro.product.brand']).strip() |
| model = check_output([ADB, 'shell', 'getprop', 'ro.product.model']).strip() |
| board = check_output([ADB, 'shell', 'getprop', 'ro.product.board']).strip() |
| device = check_output([ADB, 'shell', 'getprop', 'ro.product.device']).strip() |
| name = check_output([ADB, 'shell', 'getprop', 'ro.product.name']).strip() |
| return manufacturer + ' ' + brand + ' ' + model + ' ' + board + ' ' + device + ' ' + name |
| |
| |
| def get_android_os_version(): |
| ver = check_output([ADB, 'shell', 'getprop', 'ro.build.version.release']).strip() |
| apiLevel = check_output([ADB, 'shell', 'getprop', 'ro.build.version.sdk']).strip() |
| if not apiLevel: |
| apiLevel = check_output([ADB, 'shell', 'getprop', 'ro.build.version.sdk_int']).strip() |
| |
| os = '' |
| if ver: |
| os += 'Android ' + ver + ' ' |
| if apiLevel: |
| os += 'SDK API Level ' + apiLevel + ' ' |
| os += check_output([ADB, 'shell', 'getprop', 'ro.build.description']).strip() |
| return os |
| |
| |
| def list_android_browsers(): |
| apps = check_output([ADB, 'shell', 'pm', 'list', 'packages', '-f']).replace('\r\n', '\n') |
| browsers = [] |
| for line in apps.split('\n'): |
| line = line.strip() |
| if line.endswith('=org.mozilla.firefox'): |
| browsers += ['firefox'] |
| if line.endswith('=org.mozilla.firefox_beta'): |
| browsers += ['firefox_beta'] |
| if line.endswith('=org.mozilla.fennec_aurora'): |
| browsers += ['firefox_aurora'] |
| if line.endswith('=org.mozilla.fennec'): |
| browsers += ['firefox_nightly'] |
| if line.endswith('=com.android.chrome'): |
| browsers += ['chrome'] |
| if line.endswith('=com.chrome.beta'): |
| browsers += ['chrome_beta'] |
| if line.endswith('=com.chrome.dev'): |
| browsers += ['chrome_dev'] |
| if line.endswith('=com.chrome.canary'): |
| browsers += ['chrome_canary'] |
| if line.endswith('=com.opera.browser'): |
| browsers += ['opera'] |
| if line.endswith('=com.opera.mini.android'): |
| browsers += ['opera_mini'] |
| if line.endswith('=mobi.mgeek.TunnyBrowser'): |
| browsers += ['dolphin'] |
| |
| browsers.sort() |
| logi('emrun has automatically found the following browsers on the connected Android device:') |
| for browser in browsers: |
| logi(' - ' + browser) |
| |
| |
| def list_pc_browsers(): |
| browsers = ['firefox', 'firefox_beta', 'firefox_aurora', 'firefox_nightly', 'chrome', 'chrome_canary', 'iexplore', 'safari', 'opera'] |
| logi('emrun has automatically found the following browsers in the default install locations on the system:') |
| logi('') |
| for browser in browsers: |
| browser_exe = find_browser(browser) |
| if type(browser_exe) == list: |
| browser_exe = browser_exe[0] |
| if browser_exe: |
| logi(' - ' + browser + ': ' + browser_display_name(browser_exe) + ' ' + get_executable_version(browser_exe)) |
| logi('') |
| logi('You can pass the --browser <id> option to launch with the given browser above.') |
| logi('Even if your browser was not detected, you can use --browser /path/to/browser/executable to launch with that browser.') |
| |
| |
| def browser_display_name(browser): |
| b = browser.lower() |
| if 'iexplore' in b: |
| return 'Microsoft Internet Explorer' |
| if 'chrome' in b: |
| return 'Google Chrome' |
| if 'firefox' in b: |
| # Try to identify firefox flavor explicitly, to help show issues where emrun would launch the wrong browser. |
| try: |
| product_name = win_get_file_properties(browser)['StringFileInfo']['ProductName'] if WINDOWS else 'firefox' |
| if product_name.lower() != 'firefox': |
| return 'Mozilla Firefox ' + product_name |
| except Exception: |
| pass |
| return 'Mozilla Firefox' |
| if 'opera' in b: |
| return 'Opera' |
| if 'safari' in b: |
| return 'Apple Safari' |
| return browser |
| |
| |
| def subprocess_env(): |
| e = os.environ.copy() |
| # https://bugzilla.mozilla.org/show_bug.cgi?id=745154 |
| e['MOZ_DISABLE_AUTO_SAFE_MODE'] = '1' |
| e['MOZ_DISABLE_SAFE_MODE_KEY'] = '1' # https://bugzilla.mozilla.org/show_bug.cgi?id=653410#c9 |
| e['JIT_OPTION_asmJSAtomicsEnable'] = 'true' # https://bugzilla.mozilla.org/show_bug.cgi?id=1299359#c0 |
| return e |
| |
| |
| # Removes a directory tree even if it was readonly, and doesn't throw exception on failure. |
| def remove_tree(d): |
| os.chmod(d, stat.S_IWRITE) |
| try: |
| def remove_readonly_and_try_again(func, path, exc_info): |
| if not (os.stat(path).st_mode & stat.S_IWRITE): |
| os.chmod(path, stat.S_IWRITE) |
| func(path) |
| else: |
| raise |
| shutil.rmtree(d, onerror=remove_readonly_and_try_again) |
| except Exception: |
| pass |
| |
| |
| def get_system_info(format_json): |
| if emrun_options.android: |
| if format_json: |
| return json.dumps({'model': get_android_model(), |
| 'os': get_android_os_version(), |
| 'ram': get_system_memory(), |
| 'cpu': get_android_cpu_infoline() |
| }, indent=2) |
| else: |
| info = 'Model: ' + get_android_model() + '\n' |
| info += 'OS: ' + get_android_os_version() + ' with ' + str(get_system_memory() // 1024 // 1024) + ' MB of System RAM\n' |
| info += 'CPU: ' + get_android_cpu_infoline() + '\n' |
| return info.strip() |
| else: |
| try: |
| with open(os.path.expanduser('~/.emrun.generated.guid')) as fh: |
| unique_system_id = fh.read().strip() |
| except Exception: |
| import uuid |
| unique_system_id = str(uuid.uuid4()) |
| try: |
| open(os.path.expanduser('~/.emrun.generated.guid'), 'w').write(unique_system_id) |
| except Exception as e: |
| logv(e) |
| |
| if format_json: |
| return json.dumps({'name': socket.gethostname(), |
| 'model': get_computer_model(), |
| 'os': get_os_version(), |
| 'ram': get_system_memory(), |
| 'cpu': get_cpu_info(), |
| 'gpu': get_gpu_info(), |
| 'uuid': unique_system_id}, indent=2) |
| else: |
| cpu = get_cpu_info() |
| gpus = get_gpu_info() |
| info = 'Computer name: ' + socket.gethostname() + '\n' # http://stackoverflow.com/questions/799767/getting-name-of-windows-computer-running-python-script |
| info += 'Model: ' + get_computer_model() + '\n' |
| info += 'OS: ' + get_os_version() + ' with ' + str(get_system_memory() // 1024 // 1024) + ' MB of System RAM\n' |
| info += 'CPU: ' + cpu['model'] + ', ' + str(cpu['frequency']) + ' MHz, ' + str(cpu['physicalCores']) + ' physical cores, ' + str(cpu['logicalCores']) + ' logical cores\n' |
| if len(gpus) == 1: |
| info += 'GPU: ' + gpus[0]['model'] + ' with ' + str(gpus[0]['ram'] // 1024 // 1024) + " MB of VRAM\n" |
| elif len(gpus) > 1: |
| for i in range(0, len(gpus)): |
| info += 'GPU' + str(i) + ": " + gpus[i]['model'] + ' with ' + str(gpus[i]['ram'] // 1024 // 1024) + ' MBs of VRAM\n' |
| info += 'UUID: ' + unique_system_id |
| return info.strip() |
| |
| |
| # Be resilient to quotes and whitespace |
| def unwrap(s): |
| s = s.strip() |
| if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")): |
| s = s[1:-1].strip() |
| return s |
| |
| |
| def list_processes_by_name(exe_full_path): |
| pids = [] |
| try: |
| import psutil |
| for proc in psutil.process_iter(): |
| try: |
| pinfo = proc.as_dict(attrs=['pid', 'name', 'exe']) |
| if pinfo['exe'].lower().replace('\\', '/') == exe_full_path.lower().replace('\\', '/'): |
| pids.append(pinfo) |
| except Exception: |
| # Fail gracefully if unable to iterate over a specific process |
| pass |
| except Exception: |
| # Fail gracefully if psutil not available |
| logv('import psutil failed, unable to detect browser processes') |
| pass |
| |
| logv('Searching for processes by full path name "' + exe_full_path + '".. found ' + str(len(pids)) + ' entries') |
| |
| return pids |
| |
| |
| def run(): |
| global browser_process, browser_exe, processname_killed_atexit, emrun_options, emrun_not_enabled_nag_printed |
| usage_str = """\ |
| emrun [emrun_options] filename.html -- [html_cmdline_options] |
| |
| where emrun_options specifies command line options for emrun itself, whereas |
| html_cmdline_options specifies startup arguments to the program. |
| |
| If you are seeing "unrecognized arguments" when trying to pass |
| arguments to your page, remember to add `--` between arguments |
| to emrun itself and arguments to your page. |
| """ |
| parser = argparse.ArgumentParser(usage=usage_str) |
| |
| parser.add_argument('--kill_start', action='store_true', |
| help='If true, any previously running instances of ' |
| 'the target browser are killed before starting.') |
| |
| parser.add_argument('--kill_exit', action='store_true', |
| help='If true, the spawned browser process is forcibly ' |
| 'killed when it calls exit(). Note: Using this ' |
| 'option may require explicitly passing the option ' |
| '--browser=/path/to/browser, to avoid emrun being ' |
| 'detached from the browser process it spawns.') |
| |
| parser.add_argument('--no_server', action='store_true', |
| help='If specified, a HTTP web server is not launched ' |
| 'to host the page to run.') |
| |
| parser.add_argument('--no_browser', action='store_true', |
| help='If specified, emrun will not launch a web browser ' |
| 'to run the page.') |
| |
| parser.add_argument('--no_emrun_detect', action='store_true', |
| help='If specified, skips printing the warning message ' |
| 'if html page is detected to not have been built ' |
| 'with --emrun linker flag.') |
| |
| parser.add_argument('--serve_after_close', action='store_true', |
| help='If true, serves the web page even after the ' |
| 'application quits by user closing the web page.') |
| |
| parser.add_argument('--serve_after_exit', action='store_true', |
| help='If true, serves the web page even after the ' |
| 'application quits by a call to exit().') |
| |
| parser.add_argument('--serve_root', |
| help='If set, specifies the root path that the emrun ' |
| 'web server serves. If not specified, the directory ' |
| 'where the target .html page lives in is served.') |
| |
| parser.add_argument('--verbose', action='store_true', |
| help='Enable verbose logging from emrun internal operation.') |
| |
| parser.add_argument('--hostname', default=default_webserver_hostname, |
| help='Specifies the hostname the server runs in.') |
| |
| parser.add_argument('--port', default=default_webserver_port, type=int, |
| help='Specifies the port the server runs in.') |
| |
| parser.add_argument('--log_stdout', |
| help='Specifies a log filename where the browser process ' |
| 'stdout data will be appended to.') |
| |
| parser.add_argument('--log_stderr', |
| help='Specifies a log filename where the browser process stderr data will be appended to.') |
| |
| parser.add_argument('--silence_timeout', type=int, default=0, |
| help='If no activity is received in this many seconds, ' |
| 'the browser process is assumed to be hung, and the web ' |
| 'server is shut down and the target browser killed. ' |
| 'Disabled by default.') |
| |
| parser.add_argument('--timeout', type=int, default=0, |
| help='If the browser process does not quit or the page ' |
| 'exit() in this many seconds, the browser is assumed ' |
| 'to be hung, and the web server is shut down and the ' |
| 'target browser killed. Disabled by default.') |
| |
| parser.add_argument('--timeout_returncode', type=int, default=99999, |
| help='Sets the exit code that emrun reports back to ' |
| 'caller in the case that a page timeout occurs. ' |
| 'Default: 99999.') |
| |
| parser.add_argument('--list_browsers', action='store_true', |
| help='Prints out all detected browser that emrun is able ' |
| 'to use with the --browser command and exits.') |
| |
| parser.add_argument('--browser', |
| help='Specifies the browser executable to run the web page in.') |
| |
| parser.add_argument('--browser_args', default='', |
| help='Specifies the arguments to the browser executable.') |
| |
| parser.add_argument('--android', action='store_true', |
| help='Launches the page in a browser of an Android ' |
| 'device connected to an USB on the local system. (via adb)') |
| |
| parser.add_argument('--system_info', action='store_true', |
| help='Prints information about the current system at startup.') |
| |
| parser.add_argument('--browser_info', action='store_true', |
| help='Prints information about the target browser to launch at startup.') |
| |
| parser.add_argument('--json', action='store_true', |
| help='If specified, --system_info and --browser_info are ' |
| 'outputted in JSON format.') |
| |
| parser.add_argument('--safe_firefox_profile', action='store_true', |
| help='If true, the browser is launched into a new clean ' |
| 'Firefox profile that is suitable for unattended ' |
| 'automated runs. (If target browser != Firefox, ' |
| 'this parameter is ignored)') |
| |
| parser.add_argument('--log_html', action='store_true', |
| help='If set, information lines are printed out an HTML-friendly format.') |
| |
| parser.add_argument('--private_browsing', action='store_true', |
| help='If specified, opens browser in private/incognito mode.') |
| |
| parser.add_argument('serve', nargs='?', default='') |
| |
| parser.add_argument('cmdlineparams', nargs='*') |
| |
| options = emrun_options = parser.parse_args() |
| |
| if options.android: |
| global ADB |
| ADB = which('adb') |
| if not ADB: |
| loge("Could not find the adb tool. Install Android SDK and add the directory of adb to PATH.") |
| return 1 |
| |
| if not options.browser and not options.android: |
| if WINDOWS: |
| options.browser = 'start' |
| elif LINUX: |
| options.browser = which('xdg-open') |
| if not options.browser: |
| options.browser = 'firefox' |
| elif MACOS: |
| options.browser = 'open' |
| |
| if options.list_browsers: |
| if options.android: |
| list_android_browsers() |
| else: |
| list_pc_browsers() |
| return |
| |
| if not options.serve and (options.system_info or options.browser_info): |
| # Don't run if only --system_info or --browser_info was passed. |
| options.no_server = options.no_browser = True |
| |
| if not options.serve and not (options.no_server and options.no_browser): |
| logi(usage_str) |
| logi('') |
| logi('Type emrun --help for a detailed list of available options.') |
| return |
| |
| if options.serve: |
| file_to_serve = options.serve |
| else: |
| file_to_serve = '.' |
| file_to_serve_is_url = file_to_serve.startswith('file://') or file_to_serve.startswith('http://') or file_to_serve.startswith('https://') |
| |
| if options.serve_root: |
| serve_dir = os.path.abspath(options.serve_root) |
| else: |
| if file_to_serve == '.' or file_to_serve_is_url: |
| serve_dir = os.path.abspath('.') |
| else: |
| if file_to_serve.endswith('/') or file_to_serve.endswith('\\') or os.path.isdir(file_to_serve): |
| serve_dir = file_to_serve |
| else: |
| serve_dir = os.path.dirname(os.path.abspath(file_to_serve)) |
| if file_to_serve_is_url: |
| url = file_to_serve |
| else: |
| url = os.path.relpath(os.path.abspath(file_to_serve), serve_dir) |
| if len(options.cmdlineparams): |
| url += '?' + '&'.join(options.cmdlineparams) |
| hostname = socket.gethostbyname(socket.gethostname()) if options.android else options.hostname |
| url = 'http://' + hostname + ':' + str(options.port) + '/' + url |
| |
| os.chdir(serve_dir) |
| if not options.no_server: |
| if options.no_browser: |
| logi('Web server root directory: ' + os.path.abspath('.')) |
| else: |
| logv('Web server root directory: ' + os.path.abspath('.')) |
| |
| if options.android: |
| if not options.no_browser or options.browser_info: |
| if not options.browser: |
| loge("Running on Android requires that you explicitly specify the browser to run with --browser <id>. Run emrun --android --list_browsers to obtain a list of installed browsers you can use.") |
| return 1 |
| elif options.browser == 'firefox': |
| browser_app = 'org.mozilla.firefox/.App' |
| elif options.browser == 'firefox_beta': |
| browser_app = 'org.mozilla.firefox_beta/.App' |
| elif options.browser == 'firefox_aurora' or options.browser == 'fennec_aurora': |
| browser_app = 'org.mozilla.fennec_aurora/.App' |
| elif options.browser == 'firefox_nightly' or options.browser == 'fennec': |
| browser_app = 'org.mozilla.fennec/.App' |
| elif options.browser == 'chrome': |
| browser_app = 'com.android.chrome/com.google.android.apps.chrome.Main' |
| elif options.browser == 'chrome_beta': |
| browser_app = 'com.chrome.beta/com.google.android.apps.chrome.Main' |
| elif options.browser == 'chrome_dev': |
| browser_app = 'com.chrome.dev/com.google.android.apps.chrome.Main' |
| elif options.browser == 'chrome_canary': |
| browser_app = 'com.chrome.canary/com.google.android.apps.chrome.Main' |
| elif options.browser == 'opera': |
| browser_app = 'com.opera.browser/com.opera.Opera' |
| elif options.browser == 'opera_mini': |
| # Launching the URL works, but page seems to never load (Fails with 'Network problem' even when other browsers work) |
| browser_app = 'com.opera.mini.android/.Browser' |
| elif options.browser == 'dolphin': |
| # Current stable Dolphin as of 12/2013 does not have WebGL support. |
| browser_app = 'mobi.mgeek.TunnyBrowser/.BrowserActivity' |
| else: |
| loge("Don't know how to launch browser " + options.browser + ' on Android!') |
| return 1 |
| # To add support for a new Android browser in the list above: |
| # 1. Install the browser to Android phone, connect it via adb to PC. |
| # 2. Type 'adb shell pm list packages -f' to locate the package name of that application. |
| # 3. Type 'adb pull <packagename>.apk' to copy the apk of that application to PC. |
| # 4. Type 'aapt d xmltree <packagename>.apk AndroidManifest.xml > manifest.txt' to extract the manifest from the package. |
| # 5. Locate the name of the main activity for the browser in manifest.txt and add an entry to above list in form 'appname/mainactivityname' |
| |
| url = url.replace('&', '\\&') |
| browser = [ADB, 'shell', 'am', 'start', '-a', 'android.intent.action.VIEW', '-n', browser_app, '-d', url] |
| processname_killed_atexit = browser_app[:browser_app.find('/')] |
| else: # Launching a web page on local system. |
| if options.browser: |
| options.browser = unwrap(options.browser) |
| |
| if not options.no_browser or options.browser_info: |
| browser = find_browser(str(options.browser)) |
| if not browser: |
| loge('Unable to find browser "' + str(options.browser) + '"! Check the correctness of the passed --browser=xxx parameter!') |
| return 1 |
| browser_exe = browser[0] |
| browser_args = shlex.split(unwrap(options.browser_args)) |
| |
| if MACOS and ('safari' in browser_exe.lower() or browser_exe == 'open'): |
| # Safari has a bug that a command line 'Safari http://page.com' does |
| # not launch that page, but instead launches 'file:///http://page.com'. |
| # To remedy this, must use the open -a command to run Safari, but |
| # unfortunately this will end up spawning Safari process detached from |
| # emrun. |
| browser = ['open', '-a', 'Safari'] + (browser[1:] if len(browser) > 1 else []) |
| browser_exe = '/Applications/Safari.app/Contents/MacOS/Safari' |
| processname_killed_atexit = 'Safari' |
| elif 'chrome' in browser_exe.lower(): |
| processname_killed_atexit = 'chrome' |
| browser_args += ['--enable-nacl', '--enable-pnacl', '--disable-restore-session-state', '--enable-webgl', '--no-default-browser-check', '--no-first-run', '--allow-file-access-from-files'] |
| if options.private_browsing: |
| browser_args += ['--incognito'] |
| # if options.no_server: |
| # browser_args += ['--disable-web-security'] |
| elif 'firefox' in browser_exe.lower(): |
| processname_killed_atexit = 'firefox' |
| elif 'iexplore' in browser_exe.lower(): |
| processname_killed_atexit = 'iexplore' |
| if options.private_browsing: |
| browser_args += ['-private'] |
| elif 'opera' in browser_exe.lower(): |
| processname_killed_atexit = 'opera' |
| |
| # In Windows cmdline, & character delimits multiple commmands, so must |
| # use ^ to escape them. |
| if browser_exe == 'cmd': |
| url = url.replace('&', '^&') |
| url = url.replace('0.0.0.0', 'localhost') |
| browser += browser_args + [url] |
| |
| if options.kill_start: |
| pname = processname_killed_atexit |
| kill_browser_process() |
| processname_killed_atexit = pname |
| |
| # Copy the profile over to Android. |
| if options.android and options.safe_firefox_profile: |
| profile_dir = create_emrun_safe_firefox_profile() |
| |
| def run(cmd): |
| logi(str(cmd)) |
| subprocess.call(cmd) |
| |
| run(['adb', 'shell', 'rm', '-rf', '/mnt/sdcard/safe_firefox_profile']) |
| run(['adb', 'shell', 'mkdir', '/mnt/sdcard/safe_firefox_profile']) |
| run(['adb', 'push', os.path.join(profile_dir, 'prefs.js'), '/mnt/sdcard/safe_firefox_profile/prefs.js']) |
| browser += ['--es', 'args', '"--profile /mnt/sdcard/safe_firefox_profile"'] |
| |
| # Create temporary Firefox profile to run the page with. This is important to |
| # run after kill_browser_process()/kill_start op above, since that cleans up |
| # the temporary profile if one exists. |
| if processname_killed_atexit == 'firefox' and options.safe_firefox_profile and not options.no_browser and not options.android: |
| profile_dir = create_emrun_safe_firefox_profile() |
| |
| browser += ['-no-remote', '--profile', profile_dir.replace('\\', '/')] |
| |
| if options.system_info: |
| logi('Time of run: ' + time.strftime("%x %X")) |
| logi(get_system_info(format_json=options.json)) |
| |
| if options.browser_info: |
| if options.android: |
| if options.json: |
| logi(json.dumps({'browser': 'Android ' + browser_app}, indent=2)) |
| else: |
| logi('Browser: Android ' + browser_app) |
| else: |
| logi(get_browser_info(browser_exe, format_json=options.json)) |
| |
| # Suppress run warning if requested. |
| if options.no_emrun_detect: |
| emrun_not_enabled_nag_printed = True |
| |
| if options.log_stdout: |
| global browser_stdout_handle |
| browser_stdout_handle = open(options.log_stdout, 'a') |
| if options.log_stderr: |
| global browser_stderr_handle |
| if options.log_stderr == options.log_stdout: |
| browser_stderr_handle = browser_stdout_handle |
| else: |
| browser_stderr_handle = open(options.log_stderr, 'a') |
| |
| if not options.no_server: |
| logv('Starting web server: http://%s:%i/' % (options.hostname, options.port)) |
| httpd = HTTPWebServer((options.hostname, options.port), HTTPHandler) |
| |
| if not options.no_browser: |
| logv("Starting browser: %s" % ' '.join(browser)) |
| # if browser[0] == 'cmd': |
| # Workaround an issue where passing 'cmd /C start' is not able to detect |
| # when the user closes the page. |
| # serve_forever = True |
| global previous_browser_processes |
| logv(browser_exe) |
| previous_browser_processes = list_processes_by_name(browser_exe) |
| for p in previous_browser_processes: |
| logv('Before spawning web browser, found a running ' + os.path.basename(browser_exe) + ' browser process id: ' + str(p['pid'])) |
| browser_process = subprocess.Popen(browser, env=subprocess_env()) |
| logv('Launched browser process with pid=' + str(browser_process.pid)) |
| if options.kill_exit: |
| atexit.register(kill_browser_process) |
| # For Android automation, we execute adb, so this process does not |
| # represent a browser and no point killing it. |
| if options.android: |
| browser_process = None |
| elif not options.no_server: |
| logi('Now listening at http://%s:%i/' % (options.hostname, options.port)) |
| |
| if browser_process: |
| premature_quit_code = browser_process.poll() |
| if premature_quit_code is not None: |
| options.serve_after_close = True |
| logv('Warning: emrun got immediately detached from the target browser process (the process quit with exit code ' + str(premature_quit_code) + '). Cannot detect when user closes the browser. Behaving as if --serve_after_close was passed in.') |
| if not options.browser: |
| logv('Try passing the --browser=/path/to/browser option to avoid this from occurring. See https://github.com/emscripten-core/emscripten/issues/3234 for more discussion.') |
| |
| if not options.no_server: |
| try: |
| httpd.serve_forever() |
| except KeyboardInterrupt: |
| pass |
| httpd.server_close() |
| |
| logv('Closed web server.') |
| |
| if not options.no_browser: |
| if options.kill_exit: |
| kill_browser_process() |
| else: |
| if is_browser_process_alive(): |
| logv('Not terminating browser process, pass --kill_exit to terminate the browser when it calls exit().') |
| # If we have created a temporary Firefox profile, we would really really |
| # like to wait until the browser closes, or otherwise we'll just have to |
| # litter temp files and keep the temporary profile alive. It is possible |
| # here that the browser is cooperatively shutting down, but has not yet |
| # had time to do so, so wait for a short while. |
| if temp_firefox_profile_dir is not None: |
| time.sleep(3) |
| |
| if not is_browser_process_alive(): |
| # Browser is no longer running, make sure to clean up the temp Firefox |
| # profile, if we created one. |
| delete_emrun_safe_firefox_profile() |
| |
| return page_exit_code |
| |
| |
| def main(): |
| returncode = run() |
| logv('emrun quitting with process exit code ' + str(returncode)) |
| if temp_firefox_profile_dir is not None: |
| logi('Warning: Had to leave behind a temporary Firefox profile directory ' + temp_firefox_profile_dir + ' because --safe_firefox_profile was set and the browser did not quit before emrun did.') |
| return returncode |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |