1860 строки
76 KiB
Python
Executable File
1860 строки
76 KiB
Python
Executable File
#!/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())
|