emscripten/emrun.py

1860 строки
76 KiB
Python
Исходник Обычный вид История

#!/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.
2019-02-17 20:38:58 +03:00
"""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.
2019-02-17 20:38:58 +03:00
import argparse
import atexit
import cgi
import json
import os
import platform
import re
import shlex
import shutil
2019-02-17 20:38:58 +03:00
import socket
import stat
2019-02-17 20:38:58 +03:00
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
2018-01-24 19:01:18 +03:00
MACOS = False
if os.name == 'nt':
WINDOWS = True
import winreg
elif platform.system() == 'Linux':
LINUX = True
elif platform.mac_ver()[0] != '':
2018-01-24 19:01:18 +03:00
MACOS = True
import plistlib
# If you are running on an OS that is not any of these, must add explicit support for it.
2018-01-24 19:01:18 +03:00
if not WINDOWS and not LINUX and not MACOS:
raise Exception("Unknown OS!")
2019-02-17 20:38:58 +03:00
# Returns wallclock time in seconds.
def tick():
2018-01-24 19:01:18 +03:00
# 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()
2019-02-17 20:38:58 +03:00
# 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
2019-02-17 20:38:58 +03:00
def format_html(msg):
2019-02-17 20:38:58 +03:00
"""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
2019-02-17 20:38:58 +03:00
# HTTP requests are handled from separate threads - synchronize them to avoid race conditions
2019-02-17 20:38:58 +03:00
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()
2019-02-17 20:38:58 +03:00
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()
2019-02-17 20:38:58 +03:00
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()
2019-02-17 20:38:58 +03:00
def format_eol(msg):
if WINDOWS:
msg = msg.replace('\r\n', '\n').replace('\n', '\r\n')
return msg
2019-02-17 20:38:58 +03:00
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()
2019-02-17 20:38:58 +03:00
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()
2019-02-17 20:38:58 +03:00
def unquote_u(source):
"""Unquotes a unicode string.
(translates ascii-encoded utf string back to utf)
"""
result = unquote(source)
if '%u' in result:
2019-02-17 20:38:58 +03:00
result = result.replace('%u', '\\u').decode('unicode_escape')
return result
2019-02-17 20:38:58 +03:00
temp_firefox_profile_dir = None
2019-02-17 20:38:58 +03:00
def delete_emrun_safe_firefox_profile():
2019-02-17 20:38:58 +03:00
"""Deletes the temporary created Firefox profile (if one exists)"""
global temp_firefox_profile_dir
2019-02-17 20:38:58 +03:00
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
2019-02-17 20:38:58 +03:00
# 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
2019-02-17 20:38:58 +03:00
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
2019-02-17 20:38:58 +03:00
def kill_browser_process():
2019-02-17 20:38:58 +03:00
"""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.')
2019-02-17 20:38:58 +03:00
# 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):
2019-02-17 20:38:58 +03:00
"""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()
2019-02-17 20:38:58 +03:00
# 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()
2019-02-17 20:38:58 +03:00
# 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
2019-02-17 20:38:58 +03:00
# 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
2019-02-17 20:38:58 +03:00
# 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
2019-02-17 20:38:58 +03:00
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]
2019-02-17 20:38:58 +03:00
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'
2019-02-17 20:38:58 +03:00
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))
2019-02-17 20:38:58 +03:00
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):
2019-02-17 20:38:58 +03:00
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 = ''
2019-02-17 20:38:58 +03:00
data = {'system': system_info, 'browser': browser_info}
self.send_response(200)
2019-02-17 20:38:58 +03:00
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)
2019-02-17 20:38:58 +03:00
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')
2019-02-17 20:38:58 +03:00
# 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)
2019-02-17 20:38:58 +03:00
# From http://stackoverflow.com/questions/4842448/getting-processor-information-in-python
2019-02-17 20:38:58 +03:00
# 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
2019-02-17 20:38:58 +03:00
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())
2018-01-24 19:01:18 +03:00
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"):
2019-02-17 20:38:58 +03:00
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())
2019-02-17 20:38:58 +03:00
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'):
2019-02-17 20:38:58 +03:00
processor = line[line.find(':') + 1:].strip()
elif line.startswith('Hardware'):
2019-02-17 20:38:58 +03:00
hardware = line[line.find(':') + 1:].strip()
2019-02-17 20:38:58 +03:00
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'
2019-02-17 20:38:58 +03:00
def win_get_gpu_info():
gpus = []
def find_gpu_model(model):
for gpu in gpus:
2019-02-17 20:38:58 +03:00
if gpu['model'] == model:
return gpu
return None
for i in range(0, 16):
try:
2019-02-17 20:38:58 +03:00
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:])
2019-02-17 20:38:58 +03:00
# Go up one level for detailed
# VideoCardStringRoot = "\\".join(VideoCardStringSplit[3:len(VideoCardStringSplit)-1])
2019-02-17 20:38:58 +03:00
# Get the graphics card information
hVideoCardReg = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, ClearnVideoCardString)
try:
2019-02-17 20:38:58 +03:00
VideoCardDescription = winreg.QueryValueEx(hVideoCardReg, 'Device Description')[0]
except WindowsError:
2019-02-17 20:38:58 +03:00
VideoCardDescription = winreg.QueryValueEx(hVideoCardReg, 'DriverDesc')[0]
try:
2019-02-17 20:38:58 +03:00
driverVersion = winreg.QueryValueEx(hVideoCardReg, 'DriverVersion')[0]
VideoCardDescription += ', driver version ' + driverVersion
except WindowsError:
pass
try:
2019-02-17 20:38:58 +03:00
driverDate = winreg.QueryValueEx(hVideoCardReg, 'DriverDate')[0]
VideoCardDescription += ' (' + driverDate + ')'
except WindowsError:
pass
2019-02-17 20:38:58 +03:00
VideoCardMemorySize = winreg.QueryValueEx(hVideoCardReg, 'HardwareInformation.MemorySize')[0]
try:
2019-02-17 20:38:58 +03:00
vram = struct.unpack('l', bytes(VideoCardMemorySize))[0]
except struct.error:
vram = int(VideoCardMemorySize)
if not find_gpu_model(VideoCardDescription):
2019-02-17 20:38:58 +03:00
gpus += [{'model': VideoCardDescription, 'ram': vram}]
except WindowsError:
pass
return gpus
2019-02-17 20:38:58 +03:00
def linux_get_gpu_info():
glinfo = ''
try:
glxinfo = check_output('glxinfo')
for line in glxinfo.split("\n"):
2019-02-17 20:38:58 +03:00
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()
2019-02-17 20:38:58 +03:00
if not model:
model = 'Unknown'
return [{'model': model, 'ram': ram}]
2019-02-17 20:38:58 +03:00
2018-01-24 19:01:18 +03:00
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
2019-02-17 20:38:58 +03:00
def get_gpu_info():
2019-02-17 20:38:58 +03:00
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))
2018-01-24 19:01:18 +03:00
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 ""
2019-02-17 20:38:58 +03:00
def get_browser_build_date(filename):
try:
2018-01-24 19:01:18 +03:00
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)'
2019-02-17 20:38:58 +03:00
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)
2019-02-17 20:38:58 +03:00
# http://stackoverflow.com/questions/580924/python-windows-file-version-attribute
def win_get_file_properties(fname):
propNames = ('Comments', 'InternalName', 'ProductName',
2019-02-17 20:38:58 +03:00
'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
2019-02-17 20:38:58 +03:00
def get_computer_model():
try:
2018-01-24 19:01:18 +03:00
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()
2019-02-17 20:38:58 +03:00
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'
2019-02-17 20:38:58 +03:00
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
2018-01-24 19:01:18 +03:00
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'
2019-02-17 20:38:58 +03:00
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']
2018-01-24 19:01:18 +03:00
elif MACOS:
return int(check_output(['sysctl', '-n', 'hw.memsize']).strip())
except Exception:
return -1
2019-02-17 20:38:58 +03:00
# 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
2019-02-17 20:38:58 +03:00
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
2019-02-17 20:38:58 +03:00
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', '']
2019-02-17 20:38:58 +03:00
def find_browser(name):
if WINDOWS and name == 'start':
return win_get_default_browser()
2018-01-24 19:01:18 +03:00
if MACOS and name == 'open':
return [name]
if os.path.isfile(os.path.abspath(name)):
return [name]
2019-02-17 20:38:58 +03:00
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)
2019-02-17 20:38:58 +03:00
if path_lookup is not None:
return [path_lookup]
browser_locations = []
2018-01-24 19:01:18 +03:00
if MACOS:
# Note: by default Firefox beta installs as 'Firefox.app', you must manually rename it to
# FirefoxBeta.app after installation.
2019-02-17 20:38:58 +03:00
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:
2019-02-17 20:38:58 +03:00
if pf_env not in os.environ:
continue
program_files = os.environ[pf_env] if WINDOWS else ''
2019-02-17 20:38:58 +03:00
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:
2019-02-17 20:38:58 +03:00
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]
2019-02-17 20:38:58 +03:00
return None # Could not find the browser
2019-02-17 20:38:58 +03:00
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
2019-02-17 20:38:58 +03:00
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
2019-02-17 20:38:58 +03:00
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)
2019-02-17 20:38:58 +03:00
def list_pc_browsers():
2019-02-17 20:38:58 +03:00
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.')
2019-02-17 20:38:58 +03:00
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
2019-02-17 20:38:58 +03:00
except Exception:
pass
return 'Mozilla Firefox'
if 'opera' in b:
return 'Opera'
if 'safari' in b:
return 'Apple Safari'
return browser
2019-02-17 20:38:58 +03:00
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
2019-02-17 20:38:58 +03:00
# 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:
2019-02-17 20:38:58 +03:00
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'
2019-02-17 20:38:58 +03:00
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:
2019-02-17 20:38:58 +03:00
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'
2019-02-17 20:38:58 +03:00
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:
2019-02-17 20:38:58 +03:00
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)):
2019-02-17 20:38:58 +03:00
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()
2019-02-17 20:38:58 +03:00
# 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
2019-02-17 20:38:58 +03:00
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',
2019-02-17 20:38:58 +03:00
help='Enable verbose logging from emrun internal operation.')
parser.add_argument('--hostname', default=default_webserver_hostname,
2019-02-17 20:38:58 +03:00
help='Specifies the hostname the server runs in.')
parser.add_argument('--port', default=default_webserver_port, type=int,
2019-02-17 20:38:58 +03:00
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',
2019-02-17 20:38:58 +03:00
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',
2019-02-17 20:38:58 +03:00
help='Specifies the browser executable to run the web page in.')
parser.add_argument('--browser_args', default='',
2019-02-17 20:38:58 +03:00
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',
2019-02-17 20:38:58 +03:00
help='Prints information about the current system at startup.')
parser.add_argument('--browser_info', action='store_true',
2019-02-17 20:38:58 +03:00
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',
2019-02-17 20:38:58 +03:00
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'
2018-01-24 19:01:18 +03:00
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://')
2019-02-17 20:38:58 +03:00
if options.serve_root:
serve_dir = os.path.abspath(options.serve_root)
else:
2019-02-17 20:38:58 +03:00
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
2019-02-17 20:38:58 +03:00
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'
2019-02-17 20:38:58 +03:00
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'
2019-02-17 20:38:58 +03:00
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('/')]
2019-02-17 20:38:58 +03:00
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()
2019-02-17 20:38:58 +03:00
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))
2019-02-17 20:38:58 +03:00
# if browser[0] == 'cmd':
# Workaround an issue where passing 'cmd /C start' is not able to detect
# when the user closes the page.
2019-02-17 20:38:58 +03:00
# 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()
2019-02-17 20:38:58 +03:00
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.')
2019-02-17 20:38:58 +03:00
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.
2019-02-17 20:38:58 +03:00
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
2019-02-17 20:38:58 +03:00
def main():
returncode = run()
logv('emrun quitting with process exit code ' + str(returncode))
2019-02-17 20:38:58 +03:00
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
2019-02-17 20:38:58 +03:00
if __name__ == '__main__':
sys.exit(main())