2020-09-14 05:05:11 +03:00
#!/usr/bin/env python3
2018-09-14 21:08:09 +03:00
# 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.
2017-08-16 15:15:12 +03:00
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
"""
2017-08-16 15:15:12 +03:00
2021-03-16 20:54:26 +03:00
# 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
2021-03-16 20:54:26 +03:00
import shutil
2019-02-17 20:38:58 +03:00
import socket
2021-03-16 20:54:26 +03:00
import stat
2019-02-17 20:38:58 +03:00
import struct
import subprocess
import sys
import tempfile
import threading
import time
2017-08-16 15:15:12 +03:00
from operator import itemgetter
2017-10-03 17:03:20 +03:00
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
2021-03-16 20:54:26 +03:00
def print_to_handle ( handle , line ) :
print >> handle , line # noqa: F633
2017-10-03 17:03:20 +03:00
else :
import socketserver
from http . server import HTTPServer , SimpleHTTPRequestHandler
from urllib . parse import unquote , urlsplit
2021-03-16 20:54:26 +03:00
def print_to_handle ( handle , line ) :
handle . write ( line + ' \n ' )
2017-08-16 15:15:12 +03:00
# Populated from cmdline params
emrun_options = None
2020-06-25 01:09:39 +03:00
# Represents the process object handle to the browser we opened to run the html
# page.
2017-08-16 15:15:12 +03:00
browser_process = None
2021-02-04 14:00:33 +03:00
previous_browser_processes = None
current_browser_processes = None
2019-12-17 15:01:39 +03:00
navigation_has_occurred = False
# Stores the browser executable that was run with --browser= parameter.
browser_exe = None
2020-06-25 01:09:39 +03:00
# If we have routed browser output to file with --log_stdout and/or
# --log_stderr, these track the handles.
2017-08-16 15:15:12 +03:00
browser_stdout_handle = sys . stdout
browser_stderr_handle = sys . stderr
2020-06-25 01:09:39 +03:00
# 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.
2017-08-16 15:15:12 +03:00
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.
2021-02-04 14:00:33 +03:00
page_exit_code = None
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
# 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.
2017-08-16 15:15:12 +03:00
processname_killed_atexit = " "
2020-06-25 01:09:39 +03:00
# 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.
2019-12-17 15:01:39 +03:00
default_webserver_hostname = ' 0.0.0.0 '
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
# If user does not specify a --port parameter, this port is used to launch the
# server.
2017-08-16 15:15:12 +03:00
default_webserver_port = 6931
# Location of Android Debug Bridge executable
2020-06-25 01:09:39 +03:00
ADB = None
2017-08-16 15:15:12 +03:00
# 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
2017-08-16 15:15:12 +03:00
if os . name == ' nt ' :
WINDOWS = True
2020-10-29 01:32:49 +03:00
import winreg
2017-08-16 15:15:12 +03:00
elif platform . system ( ) == ' Linux ' :
LINUX = True
elif platform . mac_ver ( ) [ 0 ] != ' ' :
2018-01-24 19:01:18 +03:00
MACOS = True
2017-08-16 15:15:12 +03:00
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 :
2017-08-16 15:15:12 +03:00
raise Exception ( " Unknown OS! " )
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +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.
2017-08-16 15:15:12 +03:00
return time . time ( )
2019-02-17 20:38:58 +03:00
2020-06-25 01:09:39 +03:00
# Absolute wallclock time in seconds specifying when the previous HTTP stdout
# message from the page was received.
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +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. """
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +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 ( )
2017-08-16 15:15:12 +03:00
def logi ( msg ) :
2020-06-25 01:09:39 +03:00
""" Prints a log message to ' info ' stdout channel. Always printed.
"""
2017-08-16 15:15:12 +03:00
global last_message_time
with http_mutex :
if emrun_options . log_html :
sys . stdout . write ( format_html ( msg ) )
else :
2021-03-16 20:54:26 +03:00
print_to_handle ( sys . stdout , msg )
2017-08-16 15:15:12 +03:00
sys . stdout . flush ( )
last_message_time = tick ( )
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
def logv ( msg ) :
2020-06-25 01:09:39 +03:00
""" Prints a verbose log message to stdout channel.
Only shown if run with - - verbose .
"""
global last_message_time
2020-10-29 07:06:38 +03:00
if emrun_options . verbose :
with http_mutex :
2017-08-16 15:15:12 +03:00
if emrun_options . log_html :
sys . stdout . write ( format_html ( msg ) )
else :
2021-03-16 20:54:26 +03:00
print_to_handle ( sys . stdout , msg )
2017-08-16 15:15:12 +03:00
sys . stdout . flush ( )
last_message_time = tick ( )
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
def loge ( msg ) :
2020-06-25 01:09:39 +03:00
""" Prints an error message to stderr channel.
"""
2017-08-16 15:15:12 +03:00
global last_message_time
with http_mutex :
if emrun_options . log_html :
sys . stderr . write ( format_html ( msg ) )
else :
2021-03-16 20:54:26 +03:00
print_to_handle ( sys . stderr , msg )
2017-08-16 15:15:12 +03:00
sys . stderr . flush ( )
last_message_time = tick ( )
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +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
2017-08-16 15:15:12 +03:00
def browser_logi ( msg ) :
2020-06-25 01:09:39 +03:00
""" Prints a message to the browser stdout output stream.
"""
global last_message_time
2017-08-16 15:15:12 +03:00
msg = format_eol ( msg )
2021-03-16 20:54:26 +03:00
print_to_handle ( browser_stdout_handle , msg )
2017-08-16 15:15:12 +03:00
browser_stdout_handle . flush ( )
last_message_time = tick ( )
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
def browser_loge ( msg ) :
2020-06-25 01:09:39 +03:00
""" Prints a message to the browser stderr output stream.
"""
global last_message_time
2017-08-16 15:15:12 +03:00
msg = format_eol ( msg )
2021-03-16 20:54:26 +03:00
print_to_handle ( browser_stderr_handle , msg )
2017-08-16 15:15:12 +03:00
browser_stderr_handle . flush ( )
last_message_time = tick ( )
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
def unquote_u ( source ) :
2020-06-25 01:09:39 +03:00
""" Unquotes a unicode string.
( translates ascii - encoded utf string back to utf )
"""
2017-08-16 15:15:12 +03:00
result = unquote ( source )
if ' %u ' in result :
2019-02-17 20:38:58 +03:00
result = result . replace ( ' %u ' , ' \\ u ' ) . decode ( ' unicode_escape ' )
2017-08-16 15:15:12 +03:00
return result
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
temp_firefox_profile_dir = None
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +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) """
2017-08-16 15:15:12 +03:00
global temp_firefox_profile_dir
2019-02-17 20:38:58 +03:00
if temp_firefox_profile_dir is not None :
2017-08-16 15:15:12 +03:00
logv ( ' remove_tree( " ' + temp_firefox_profile_dir + ' " ) ' )
2021-03-16 20:54:26 +03:00
remove_tree ( temp_firefox_profile_dir )
2017-08-16 15:15:12 +03:00
temp_firefox_profile_dir = None
2019-02-17 20:38:58 +03:00
2020-06-25 01:09:39 +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.
2017-08-16 15:15:12 +03:00
def create_emrun_safe_firefox_profile ( ) :
2020-06-25 01:09:39 +03:00
global temp_firefox_profile_dir
2017-08-16 15:15:12 +03:00
temp_firefox_profile_dir = tempfile . mkdtemp ( prefix = ' temp_emrun_firefox_profile_ ' )
2019-01-16 11:10:57 +03:00
with open ( os . path . join ( temp_firefox_profile_dir , ' prefs.js ' ) , ' w ' ) as f :
f . write ( '''
2017-08-16 15:15:12 +03:00
/ / 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 ) ;
2019-12-17 15:01:39 +03:00
user_pref ( " toolkit.startup.max_resumed_crashes " , - 1 ) ;
2017-08-16 15:15:12 +03:00
/ / 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 .
2018-08-06 20:09:40 +03:00
/ / 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 " ) ;
2017-08-16 15:15:12 +03:00
/ / 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 ) ;
2018-01-11 13:55:10 +03:00
/ / Enable SharedArrayBuffer ( this profile is for a testing environment , so Spectre / Meltdown don ' t apply)
user_pref ( " javascript.options.shared_memory " , true ) ;
2017-08-16 15:15:12 +03:00
''' )
2019-12-17 15:01:39 +03:00
if emrun_options . private_browsing :
2019-04-02 00:10:30 +03:00
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 ) ;
''' )
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +03:00
def is_browser_process_alive ( ) :
2020-06-25 01:09:39 +03:00
""" Returns whether the browser page we spawned is still running.
( note , not perfect atm , in case we are running in detached mode )
"""
2019-12-17 15:01:39 +03:00
# 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
2021-02-04 14:00:33 +03:00
if current_browser_processes :
2019-12-17 15:01:39 +03:00
try :
import psutil
2021-02-04 14:00:33 +03:00
for p in current_browser_processes :
2019-12-17 15:01:39 +03:00
if psutil . pid_exists ( p [ ' pid ' ] ) :
return True
2021-02-04 14:00:33 +03:00
return False
2019-12-17 15:01:39 +03:00
except Exception :
# Fail gracefully if psutil not available
2021-02-04 14:00:33 +03:00
logv ( ' psutil is not available, emrun may not be able to accurately track whether the browser process is alive or not ' )
2019-12-17 15:01:39 +03:00
2021-02-04 14:00:33 +03:00
# 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
2017-08-16 15:15:12 +03:00
def kill_browser_process ( ) :
2019-02-17 20:38:58 +03:00
""" Kills browser_process and processname_killed_atexit. Also removes the
2020-06-25 01:09:39 +03:00
temporary Firefox profile that was created , if one exists .
"""
2021-02-04 14:00:33 +03:00
global browser_process , processname_killed_atexit , current_browser_processes
if browser_process and browser_process . poll ( ) is None :
2017-08-16 15:15:12 +03:00
try :
2021-02-04 14:00:33 +03:00
logv ( ' Terminating browser process pid= ' + str ( browser_process . pid ) + ' .. ' )
2017-08-16 15:15:12 +03:00
browser_process . kill ( )
2017-10-03 17:03:20 +03:00
except Exception as e :
2017-08-16 15:15:12 +03:00
logv ( ' Failed with error ' + str ( e ) + ' ! ' )
2021-02-04 14:00:33 +03:00
2017-08-16 15:15:12 +03:00
browser_process = None
2021-02-04 14:00:33 +03:00
# We have a hold of the target browser process explicitly, no need to resort to killall,
# so clear that record out.
2017-08-16 15:15:12 +03:00
processname_killed_atexit = ' '
2021-02-04 14:00:33 +03:00
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 = ' '
2018-07-02 03:57:48 +03:00
if len ( processname_killed_atexit ) :
2017-08-16 15:15:12 +03:00
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 ] )
2019-08-13 00:58:11 +03:00
except OSError :
2017-08-16 15:15:12 +03:00
try :
subprocess . call ( [ ' killall ' , processname_killed_atexit ] )
2019-08-13 00:58:11 +03:00
except OSError :
2017-08-16 15:15:12 +03:00
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 = ' '
2021-02-04 14:00:33 +03:00
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 ( ) :
2021-02-24 23:34:33 +03:00
if not browser_exe :
return # Running with --no_browser, we are not binding to a spawned browser.
2021-02-04 14:00:33 +03:00
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
2017-08-16 15:15:12 +03:00
# Our custom HTTP web server that will server the target page to run via .html.
2020-06-25 01:09:39 +03:00
# 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.
2017-10-03 17:03:20 +03:00
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 . """
2020-06-10 23:19:45 +03:00
expected_http_seq_num = 1
2020-06-25 01:09:39 +03:00
# 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.
2017-08-16 15:15:12 +03:00
http_message_queue = [ ]
def handle_incoming_message ( self , seq_num , log , data ) :
global have_received_messages
with http_mutex :
have_received_messages = True
2020-06-25 01:09:39 +03:00
if seq_num == - 1 :
# Message arrived without a sequence number? Just log immediately
2017-08-16 15:15:12 +03:00
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
2020-06-25 01:09:39 +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.
2017-08-16 15:15:12 +03:00
def print_timed_out_messages ( self ) :
global last_message_time
with http_mutex :
now = tick ( )
max_message_queue_time = 5
2018-07-02 03:57:48 +03:00
if len ( self . http_message_queue ) and now - last_message_time > max_message_queue_time :
2017-08-16 15:15:12 +03:00
self . print_next_message ( )
2019-02-17 20:38:58 +03:00
2020-06-25 01:09:39 +03:00
# Skips to printing the next message in queue now, independent of whether
# there was missed messages in the sequence numbering.
2017-08-16 15:15:12 +03:00
def print_next_message ( self ) :
with http_mutex :
2018-07-02 03:57:48 +03:00
if len ( self . http_message_queue ) :
2017-08-16 15:15:12 +03:00
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 :
2018-07-02 03:57:48 +03:00
while len ( self . http_message_queue ) :
2017-08-16 15:15:12 +03:00
self . print_next_message ( )
2020-06-25 01:09:39 +03:00
# Prints any messages that are now due after we logged some other previous
# messages.
2017-08-16 15:15:12 +03:00
def print_messages_due ( self ) :
with http_mutex :
2018-07-02 03:57:48 +03:00
while len ( self . http_message_queue ) :
2017-08-16 15:15:12 +03:00
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 ) :
2020-06-25 01:09:39 +03:00
global last_message_time , page_exit_code , emrun_not_enabled_nag_printed
2017-08-16 15:15:12 +03:00
self . is_running = True
self . timeout = timeout
2021-02-04 14:00:33 +03:00
logv ( " Entering web server loop. " )
2017-08-16 15:15:12 +03:00
while self . is_running :
now = tick ( )
# Did user close browser?
2019-12-17 15:01:39 +03:00
if not emrun_options . no_browser and not is_browser_process_alive ( ) :
2020-10-29 07:06:38 +03:00
logv ( " Shutting down because browser is no longer alive " )
2019-12-17 15:01:39 +03:00
delete_emrun_safe_firefox_profile ( )
if not emrun_options . serve_after_close :
2021-02-04 14:00:33 +03:00
logv ( " Browser process has shut down, quitting web server. " )
2019-12-17 15:01:39 +03:00
self . is_running = False
2017-08-16 15:15:12 +03:00
# 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
2020-06-25 01:09:39 +03:00
emrun_options . kill_exit = True
2017-08-16 15:15:12 +03:00
# 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) ' )
2020-06-25 01:09:39 +03:00
emrun_options . kill_exit = True
2017-08-16 15:15:12 +03:00
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 :
2019-12-17 15:01:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
emrun_not_enabled_nag_printed = True
# Clean up at quit, print any leftover messages in queue.
self . print_all_messages ( )
2021-02-04 14:00:33 +03:00
logv ( " Web server loop done. " )
2017-08-16 15:15:12 +03:00
def handle_error ( self , request , client_address ) :
2018-02-06 01:45:30 +03:00
err = sys . exc_info ( ) [ 1 ] . args [ 0 ]
2020-06-25 01:09:39 +03:00
# 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.
2017-08-16 15:15:12 +03:00
if err != 10054 :
2017-10-03 17:03:20 +03:00
socketserver . BaseServer . handle_error ( self , request , client_address )
2017-08-16 15:15:12 +03:00
def shutdown ( self ) :
self . is_running = False
self . print_all_messages ( )
return 1
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
# Processes HTTP request back to the browser.
2017-10-03 17:03:20 +03:00
class HTTPHandler ( SimpleHTTPRequestHandler ) :
2017-08-16 15:15:12 +03:00
def send_head ( self ) :
2019-08-02 21:03:16 +03:00
self . protocol_version = ' HTTP/1.1 '
2017-08-16 15:15:12 +03:00
global page_last_served_time
path = self . translate_path ( self . path )
f = None
2019-02-17 20:38:58 +03:00
2020-06-25 01:09:39 +03:00
# A browser has navigated to this page - check which PID got spawned for
# the browser
2021-02-04 14:00:33 +03:00
global navigation_has_occurred
if not navigation_has_occurred and current_browser_processes is None :
detect_browser_processes ( )
2019-12-17 15:01:39 +03:00
navigation_has_occurred = True
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +03:00
self . send_response ( 200 )
guess_file_type = path
2020-06-25 01:09:39 +03:00
# 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.
2017-10-03 17:03:20 +03:00
if ' Accept-Encoding ' in self . headers and ' gzip ' in self . headers [ ' Accept-Encoding ' ] and path . lower ( ) . endswith ( ' gz ' ) :
2017-08-16 15:15:12 +03:00
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 ]
2017-08-16 15:15:12 +03:00
ctype = self . guess_type ( guess_file_type )
2018-01-04 14:55:58 +03:00
if guess_file_type . lower ( ) . endswith ( ' .wasm ' ) :
ctype = ' application/wasm '
2019-02-11 22:55:44 +03:00
if guess_file_type . lower ( ) . endswith ( ' .js ' ) :
ctype = ' application/javascript '
2019-02-17 20:38:58 +03:00
self . send_header ( ' Content-type ' , ctype )
2017-08-16 15:15:12 +03:00
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 ' )
2017-08-16 15:15:12 +03:00
self . send_header ( ' Access-Control-Allow-Origin ' , ' * ' )
2020-01-03 19:18:02 +03:00
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 ' )
2017-08-16 15:15:12 +03:00
self . end_headers ( )
page_last_served_time = tick ( )
return f
def log_request ( self , code ) :
# Filter out 200 OK messages to remove noise.
2019-08-13 00:58:11 +03:00
if code != 200 :
2017-10-03 17:03:20 +03:00
SimpleHTTPRequestHandler . log_request ( self , code )
2017-08-16 15:15:12 +03:00
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 :
2017-08-16 15:15:12 +03:00
sys . stderr . write ( msg )
def do_POST ( self ) :
2019-08-02 21:03:16 +03:00
self . protocol_version = ' HTTP/1.1 '
2020-06-25 01:09:39 +03:00
global page_exit_code , have_received_messages
2017-08-16 15:15:12 +03:00
2017-10-03 17:03:20 +03:00
( _ , _ , path , query , _ ) = urlsplit ( self . path )
2017-08-16 15:15:12 +03:00
logv ( ' POST: " ' + self . path + ' " (path: " ' + path + ' " , query: " ' + query + ' " ) ' )
2020-06-25 01:09:39 +03:00
if query . startswith ( ' file= ' ) :
# Binary file dump/upload handling. Requests to
# "stdio.html?file=filename" will write binary data to the given file.
2017-08-16 15:15:12 +03:00
data = self . rfile . read ( int ( self . headers [ ' Content-Length ' ] ) )
filename = query [ len ( ' file= ' ) : ]
dump_out_directory = ' dump_out '
try :
os . mkdir ( dump_out_directory )
2019-08-13 00:58:11 +03:00
except OSError :
2017-08-16 15:15:12 +03:00
pass
filename = os . path . join ( dump_out_directory , os . path . normpath ( filename ) )
2021-06-16 03:05:21 +03:00
with open ( filename , ' wb ' ) as fh :
fh . write ( data )
2021-03-16 20:54:26 +03:00
logi ( ' Wrote ' + str ( len ( data ) ) + ' bytes to file " ' + filename + ' " . ' )
2017-08-16 15:15:12 +03:00
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 ) )
2019-08-13 00:58:11 +03:00
except ValueError :
2017-08-16 15:15:12 +03:00
browser_info = ' '
2019-02-17 20:38:58 +03:00
data = { ' system ' : system_info , ' browser ' : browser_info }
2017-08-16 15:15:12 +03:00
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 ' )
2017-08-16 15:15:12 +03:00
self . end_headers ( )
self . wfile . write ( json . dumps ( data ) )
return
else :
data = self . rfile . read ( int ( self . headers [ ' Content-Length ' ] ) )
2018-02-06 01:45:30 +03:00
if str is not bytes and isinstance ( data , bytes ) :
data = data . decode ( ' utf-8 ' )
2017-08-16 15:15:12 +03:00
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
2020-10-29 07:06:38 +03:00
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
2017-08-16 15:15:12 +03:00
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 ' )
2017-08-16 15:15:12 +03:00
self . end_headers ( )
2018-02-06 01:45:30 +03:00
self . wfile . write ( b ' OK ' )
2017-08-16 15:15:12 +03:00
2019-02-17 20:38:58 +03:00
2018-01-18 20:28:27 +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
2017-08-16 15:15:12 +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"
2017-08-16 15:15:12 +03:00
def get_cpu_info ( ) :
physical_cores = 1
logical_cores = 1
frequency = 0
try :
if WINDOWS :
2020-08-11 05:50:09 +03:00
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 ' )
2017-08-16 15:15:12 +03:00
cpu_name = cpus [ 0 ] . Name + ' , ' + platform . processor ( )
2018-01-18 20:28:27 +03:00
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 :
2018-01-18 20:28:27 +03:00
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
2017-08-16 15:15:12 +03:00
elif LINUX :
2018-01-18 20:28:27 +03:00
all_info = check_output ( [ ' cat ' , ' /proc/cpuinfo ' ] ) . strip ( )
2017-08-16 15:15:12 +03:00
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 ( )
2018-01-18 20:28:27 +03:00
lscpu = check_output ( [ ' lscpu ' ] )
2017-08-16 15:15:12 +03:00
frequency = int ( float ( re . search ( ' CPU MHz: (.*) ' , lscpu ) . group ( 1 ) . strip ( ) ) + 0.5 )
2019-01-16 04:05:25 +03:00
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 ( ) )
2017-10-03 17:03:20 +03:00
except Exception as e :
import traceback
2021-03-16 20:54:26 +03:00
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
}
2017-08-16 15:15:12 +03:00
def get_android_cpu_infoline ( ) :
2018-01-18 20:28:27 +03:00
lines = check_output ( [ ADB , ' shell ' , ' cat ' , ' /proc/cpuinfo ' ] ) . split ( ' \n ' )
2017-08-16 15:15:12 +03:00
processor = ' '
hardware = ' '
for line in lines :
if line . startswith ( ' Processor ' ) :
2019-02-17 20:38:58 +03:00
processor = line [ line . find ( ' : ' ) + 1 : ] . strip ( )
2017-08-16 15:15:12 +03:00
elif line . startswith ( ' Hardware ' ) :
2019-02-17 20:38:58 +03:00
hardware = line [ line . find ( ' : ' ) + 1 : ] . strip ( )
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +03:00
return ' CPU: ' + processor + ' , ' + hardware + ' @ ' + str ( freq ) + ' MHz '
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +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
2017-08-16 15:15:12 +03:00
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 ( ' \\ ' )
2017-10-03 17:03:20 +03:00
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])
2017-08-16 15:15:12 +03:00
2019-02-17 20:38:58 +03:00
# Get the graphics card information
2017-10-03 17:03:20 +03:00
hVideoCardReg = winreg . OpenKey ( winreg . HKEY_LOCAL_MACHINE , ClearnVideoCardString )
2017-08-16 15:15:12 +03:00
try :
2019-02-17 20:38:58 +03:00
VideoCardDescription = winreg . QueryValueEx ( hVideoCardReg , ' Device Description ' ) [ 0 ]
2017-08-16 15:15:12 +03:00
except WindowsError :
2019-02-17 20:38:58 +03:00
VideoCardDescription = winreg . QueryValueEx ( hVideoCardReg , ' DriverDesc ' ) [ 0 ]
2017-08-16 15:15:12 +03:00
try :
2019-02-17 20:38:58 +03:00
driverVersion = winreg . QueryValueEx ( hVideoCardReg , ' DriverVersion ' ) [ 0 ]
2017-08-16 15:15:12 +03:00
VideoCardDescription + = ' , driver version ' + driverVersion
2019-08-13 00:58:11 +03:00
except WindowsError :
2017-08-16 15:15:12 +03:00
pass
try :
2019-02-17 20:38:58 +03:00
driverDate = winreg . QueryValueEx ( hVideoCardReg , ' DriverDate ' ) [ 0 ]
2017-08-16 15:15:12 +03:00
VideoCardDescription + = ' ( ' + driverDate + ' ) '
2019-08-13 00:58:11 +03:00
except WindowsError :
2017-08-16 15:15:12 +03:00
pass
2019-02-17 20:38:58 +03:00
VideoCardMemorySize = winreg . QueryValueEx ( hVideoCardReg , ' HardwareInformation.MemorySize ' ) [ 0 ]
2017-08-16 15:15:12 +03:00
try :
2019-02-17 20:38:58 +03:00
vram = struct . unpack ( ' l ' , bytes ( VideoCardMemorySize ) ) [ 0 ]
2017-08-16 15:15:12 +03:00
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 } ]
2017-08-16 15:15:12 +03:00
except WindowsError :
pass
return gpus
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
def linux_get_gpu_info ( ) :
glinfo = ' '
try :
2018-01-18 20:28:27 +03:00
glxinfo = check_output ( ' glxinfo ' )
2017-08-16 15:15:12 +03:00
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 ( )
2017-08-16 15:15:12 +03:00
glinfo = gl_vendor + ' ' + gl_renderer + ' , GL version ' + gl_version
2017-10-03 17:03:20 +03:00
except Exception as e :
2017-08-16 15:15:12 +03:00
logv ( e )
adapterinfo = ' '
try :
2018-01-18 20:28:27 +03:00
vgainfo = check_output ( [ ' lshw ' , ' -C ' , ' display ' ] , stderr = subprocess . PIPE )
2017-08-16 15:15:12 +03:00
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 + ' ) '
2017-10-03 17:03:20 +03:00
except Exception as e :
2017-08-16 15:15:12 +03:00
logv ( e )
ram = 0
try :
2018-01-18 20:28:27 +03:00
vgainfo = check_output ( ' lspci -v -s $(lspci | grep VGA | cut -d " " -f 1) ' , shell = True , stderr = subprocess . PIPE )
2019-01-16 04:05:25 +03:00
ram = int ( re . search ( r " \ [size=([0-9]*)M \ ] " , vgainfo ) . group ( 1 ) ) * 1024 * 1024
2017-10-03 17:03:20 +03:00
except Exception as e :
2017-08-16 15:15:12 +03:00
logv ( e )
model = ( adapterinfo + ' ' + glinfo ) . strip ( )
2019-02-17 20:38:58 +03:00
if not model :
model = ' Unknown '
2017-08-16 15:15:12 +03:00
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 ( ) :
2017-08-16 15:15:12 +03:00
gpus = [ ]
try :
2018-01-18 20:28:27 +03:00
info = check_output ( [ ' system_profiler ' , ' SPDisplaysDataType ' ] )
2017-08-16 15:15:12 +03:00
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
2019-08-13 00:58:11 +03:00
except Exception :
2017-08-16 15:15:12 +03:00
pass
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +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 [ ]
2017-08-16 15:15:12 +03:00
def get_executable_version ( filename ) :
try :
if WINDOWS :
2020-08-11 05:50:09 +03:00
import win32api
2017-08-16 15:15:12 +03:00
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 :
2017-08-16 15:15:12 +03:00
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 ( ) :
2018-01-18 20:28:27 +03:00
version = check_output ( [ filename , ' -v ' ] )
2017-08-16 15:15:12 +03:00
version = version . replace ( ' Mozilla Firefox ' , ' ' )
return version . strip ( )
else :
return " "
2017-10-03 17:03:20 +03:00
except Exception as e :
2017-08-16 15:15:12 +03:00
logv ( e )
return " "
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
def get_browser_build_date ( filename ) :
try :
2018-01-24 19:01:18 +03:00
if MACOS :
2017-08-16 15:15:12 +03:00
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 ( ' . ' ) ) )
2017-10-03 17:03:20 +03:00
except Exception as e :
2017-08-16 15:15:12 +03:00
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 ) ) )
2017-10-03 17:03:20 +03:00
except Exception as e :
2017-08-16 15:15:12 +03:00
logv ( e )
return ' (unknown) '
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +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
2017-08-16 15:15:12 +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 ' )
2017-08-16 15:15:12 +03:00
props = { ' FixedFileInfo ' : None , ' StringFileInfo ' : None , ' FileVersion ' : None }
2020-08-11 05:50:09 +03:00
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
2017-08-16 15:15:12 +03:00
return props
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
def get_computer_model ( ) :
try :
2018-01-24 19:01:18 +03:00
if MACOS :
2017-08-16 15:15:12 +03:00
try :
with open ( os . path . join ( os . getenv ( " HOME " ) , ' .emrun.hwmodel.cached ' ) , ' r ' ) as f :
model = f . read ( )
return model
2019-08-13 00:58:11 +03:00
except IOError :
2017-08-16 15:15:12 +03:00
pass
try :
# http://apple.stackexchange.com/questions/98080/can-a-macs-model-year-be-determined-via-terminal-command
2018-01-18 20:28:27 +03:00
serial = check_output ( [ ' system_profiler ' , ' SPHardwareDataType ' ] )
2017-08-16 15:15:12 +03:00
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 ) )
2018-01-18 20:28:27 +03:00
model = check_output ( cmd )
2017-08-16 15:15:12 +03:00
model = re . search ( ' <configCode>(.*)</configCode> ' , model )
model = model . group ( 1 ) . strip ( )
2021-06-16 03:05:21 +03:00
with open ( os . path . join ( os . getenv ( " HOME " ) , ' .emrun.hwmodel.cached ' ) , ' w ' ) as fh :
fh . write ( model ) # Cache the hardware model to disk
2017-08-16 15:15:12 +03:00
return model
2019-08-13 00:58:11 +03:00
except Exception :
2018-01-18 20:28:27 +03:00
hwmodel = check_output ( [ ' sysctl ' , ' hw.model ' ] )
2017-08-16 15:15:12 +03:00
hwmodel = re . search ( ' hw.model: (.*) ' , hwmodel ) . group ( 1 ) . strip ( )
return hwmodel
elif WINDOWS :
2018-01-18 20:28:27 +03:00
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
2017-08-16 15:15:12 +03:00
elif LINUX :
2018-01-18 20:28:27 +03:00
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 ( )
2017-08-16 15:15:12 +03:00
2018-01-18 20:28:27 +03:00
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 ( )
2017-08-16 15:15:12 +03:00
return board_vendor + ' ' + board_name + ' ' + board_version + ' , ' + bios_vendor + ' ' + bios_version + ' ( ' + bios_date + ' ) '
2017-10-03 17:03:20 +03:00
except Exception as e :
2017-08-16 15:15:12 +03:00
logv ( str ( e ) )
return ' Generic '
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
def get_os_version ( ) :
bitness = ' (64bit) ' if platform . machine ( ) in [ ' AMD64 ' , ' x86_64 ' ] else ' (32bit) '
try :
if WINDOWS :
2017-10-03 17:03:20 +03:00
versionHandle = winreg . OpenKey ( winreg . HKEY_LOCAL_MACHINE , " SOFTWARE \\ Microsoft \\ Windows NT \\ CurrentVersion " )
productName = winreg . QueryValueEx ( versionHandle , " ProductName " )
2017-08-16 15:15:12 +03:00
version = ' '
try :
2018-01-18 20:28:27 +03:00
version = ' ' + check_output ( [ ' wmic ' , ' os ' , ' get ' , ' version ' ] ) . split ( ' \n ' ) [ 1 ] . strip ( )
2019-08-13 00:58:11 +03:00
except Exception :
2017-08-16 15:15:12 +03:00
pass
return productName [ 0 ] + version + bitness
2018-01-24 19:01:18 +03:00
elif MACOS :
2018-02-06 01:45:30 +03:00
return ' macOS ' + platform . mac_ver ( ) [ 0 ] + bitness
2017-08-16 15:15:12 +03:00
elif LINUX :
2018-01-18 20:28:27 +03:00
kernel_version = check_output ( [ ' uname ' , ' -r ' ] ) . strip ( )
2017-08-16 15:15:12 +03:00
return ' ' . join ( platform . linux_distribution ( ) ) + ' , linux kernel ' + kernel_version + ' ' + platform . architecture ( ) [ 0 ] + bitness
2019-08-13 00:58:11 +03:00
except Exception :
2017-08-16 15:15:12 +03:00
return ' Unknown OS '
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
def get_system_memory ( ) :
try :
if LINUX or emrun_options . android :
if emrun_options . android :
2018-01-18 20:28:27 +03:00
lines = check_output ( [ ADB , ' shell ' , ' cat ' , ' /proc/meminfo ' ] ) . split ( ' \n ' )
2017-08-16 15:15:12 +03:00
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 :
2020-08-11 05:50:09 +03:00
import win32api
2017-08-16 15:15:12 +03:00
return win32api . GlobalMemoryStatusEx ( ) [ ' TotalPhys ' ]
2018-01-24 19:01:18 +03:00
elif MACOS :
2018-01-18 20:28:27 +03:00
return int ( check_output ( [ ' sysctl ' , ' -n ' , ' hw.memsize ' ] ) . strip ( ) )
2019-08-13 00:58:11 +03:00
except Exception :
2017-08-16 15:15:12 +03:00
return - 1
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +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 :
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +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 :
2017-10-03 17:03:20 +03:00
with winreg . OpenKey ( winreg . HKEY_CURRENT_USER , r " Software \ Classes \ http \ shell \ open \ command " ) as key :
cmd = winreg . QueryValue ( key , None )
2017-08-16 15:15:12 +03:00
if cmd :
parts = shlex . split ( cmd )
2018-07-02 03:57:48 +03:00
if len ( parts ) :
2017-08-16 15:15:12 +03:00
return [ parts [ 0 ] ]
except WindowsError :
logv ( " Unable to find default browser key in Windows registry. Trying fallback. " )
2020-10-01 04:38:43 +03:00
# Fall back to 'start "" %1', which we have to treat as if user passed --serve_forever, since
2017-08-16 15:15:12 +03:00
# for some reason, we are not able to detect when the browser closes when this is passed.
2020-10-01 04:38:43 +03:00
#
# 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 ' , ' ' ]
2017-08-16 15:15:12 +03:00
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +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 ' :
2017-08-16 15:15:12 +03:00
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 ' ]
2017-08-16 15:15:12 +03:00
path_lookup = which ( name )
2019-02-17 20:38:58 +03:00
if path_lookup is not None :
2017-08-16 15:15:12 +03:00
return [ path_lookup ]
browser_locations = [ ]
2018-01-24 19:01:18 +03:00
if MACOS :
2017-08-16 15:15:12 +03:00
# 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 ' ) ]
2017-08-16 15:15:12 +03:00
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 :
2017-08-16 15:15:12 +03:00
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 ' ) ) ]
2017-08-16 15:15:12 +03:00
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 ' ) ) ]
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
for alias , browser_exe in browser_locations :
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +03:00
return None # Could not find the browser
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
def get_android_model ( ) :
2018-01-18 20:28:27 +03:00
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 ( )
2017-08-16 15:15:12 +03:00
return manufacturer + ' ' + brand + ' ' + model + ' ' + board + ' ' + device + ' ' + name
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
def get_android_os_version ( ) :
2018-01-18 20:28:27 +03:00
ver = check_output ( [ ADB , ' shell ' , ' getprop ' , ' ro.build.version.release ' ] ) . strip ( )
apiLevel = check_output ( [ ADB , ' shell ' , ' getprop ' , ' ro.build.version.sdk ' ] ) . strip ( )
2017-08-16 15:15:12 +03:00
if not apiLevel :
2018-01-18 20:28:27 +03:00
apiLevel = check_output ( [ ADB , ' shell ' , ' getprop ' , ' ro.build.version.sdk_int ' ] ) . strip ( )
2017-08-16 15:15:12 +03:00
os = ' '
if ver :
os + = ' Android ' + ver + ' '
if apiLevel :
os + = ' SDK API Level ' + apiLevel + ' '
2018-01-18 20:28:27 +03:00
os + = check_output ( [ ADB , ' shell ' , ' getprop ' , ' ro.build.description ' ] ) . strip ( )
2017-08-16 15:15:12 +03:00
return os
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +03:00
def list_android_browsers ( ) :
2018-01-18 20:28:27 +03:00
apps = check_output ( [ ADB , ' shell ' , ' pm ' , ' list ' , ' packages ' , ' -f ' ] ) . replace ( ' \r \n ' , ' \n ' )
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +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 ' ]
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +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 :
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +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
2021-03-16 20:54:26 +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
2017-08-16 15:15:12 +03:00
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 )
2017-08-16 15:15:12 +03:00
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 '
2017-08-16 15:15:12 +03:00
info + = ' CPU: ' + get_android_cpu_infoline ( ) + ' \n '
return info . strip ( )
else :
try :
2021-06-16 03:05:21 +03:00
with open ( os . path . expanduser ( ' ~/.emrun.generated.guid ' ) ) as fh :
unique_system_id = fh . read ( ) . strip ( )
2019-08-13 00:58:11 +03:00
except Exception :
2017-08-16 15:15:12 +03:00
import uuid
unique_system_id = str ( uuid . uuid4 ( ) )
try :
open ( os . path . expanduser ( ' ~/.emrun.generated.guid ' ) , ' w ' ) . write ( unique_system_id )
2017-10-03 17:03:20 +03:00
except Exception as e :
2017-08-16 15:15:12 +03:00
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 )
2017-08-16 15:15:12 +03:00
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 '
2017-08-16 15:15:12 +03:00
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 "
2017-08-16 15:15:12 +03:00
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 '
2017-08-16 15:15:12 +03:00
info + = ' UUID: ' + unique_system_id
return info . strip ( )
2019-02-17 20:38:58 +03:00
2018-01-18 20:28:27 +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
2019-12-17 15:01:39 +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
2021-02-04 14:00:33 +03:00
logv ( ' import psutil failed, unable to detect browser processes ' )
2019-12-17 15:01:39 +03:00
pass
2021-02-04 14:00:33 +03:00
logv ( ' Searching for processes by full path name " ' + exe_full_path + ' " .. found ' + str ( len ( pids ) ) + ' entries ' )
2019-12-17 15:01:39 +03:00
return pids
2017-08-16 15:15:12 +03:00
def run ( ) :
2020-06-25 01:09:39 +03:00
global browser_process , browser_exe , processname_killed_atexit , emrun_options , emrun_not_enabled_nag_printed
usage_str = """ \
2020-10-24 02:08:12 +03:00
emrun [ emrun_options ] filename . html - - [ html_cmdline_options ]
2020-06-25 01:09:39 +03:00
where emrun_options specifies command line options for emrun itself , whereas
html_cmdline_options specifies startup arguments to the program .
2020-10-24 02:08:12 +03:00
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 .
2020-06-25 01:09:39 +03:00
"""
2018-01-11 13:55:10 +03:00
parser = argparse . ArgumentParser ( usage = usage_str )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
parser . add_argument ( ' --kill_start ' , action = ' store_true ' ,
help = ' If true, any previously running instances of '
' the target browser are killed before starting. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-10-24 02:08:12 +03:00
parser . add_argument ( ' --no_server ' , action = ' store_true ' ,
2020-06-25 01:09:39 +03:00
help = ' If specified, a HTTP web server is not launched '
' to host the page to run. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
parser . add_argument ( ' --no_browser ' , action = ' store_true ' ,
help = ' If specified, emrun will not launch a web browser '
' to run the page. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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(). ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
parser . add_argument ( ' --verbose ' , action = ' store_true ' ,
2019-02-17 20:38:58 +03:00
help = ' Enable verbose logging from emrun internal operation. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
parser . add_argument ( ' --hostname ' , default = default_webserver_hostname ,
2019-02-17 20:38:58 +03:00
help = ' Specifies the hostname the server runs in. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
parser . add_argument ( ' --log_stdout ' ,
help = ' Specifies a log filename where the browser process '
' stdout data will be appended to. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
parser . add_argument ( ' --browser ' ,
2019-02-17 20:38:58 +03:00
help = ' Specifies the browser executable to run the web page in. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
parser . add_argument ( ' --browser_args ' , default = ' ' ,
2019-02-17 20:38:58 +03:00
help = ' Specifies the arguments to the browser executable. ' )
2018-01-04 14:55:58 +03:00
2020-06-25 01:09:39 +03:00
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) ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
parser . add_argument ( ' --json ' , action = ' store_true ' ,
help = ' If specified, --system_info and --browser_info are '
' outputted in JSON format. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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) ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
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. ' )
2017-08-16 15:15:12 +03:00
2020-06-25 01:09:39 +03:00
parser . add_argument ( ' --private_browsing ' , action = ' store_true ' ,
2019-12-17 15:01:39 +03:00
help = ' If specified, opens browser in private/incognito mode. ' )
2019-04-02 00:10:30 +03:00
2020-10-24 02:08:12 +03:00
parser . add_argument ( ' serve ' , nargs = ' ? ' , default = ' ' )
parser . add_argument ( ' cmdlineparams ' , nargs = ' * ' )
options = emrun_options = parser . parse_args ( )
2017-08-16 15:15:12 +03:00
if options . android :
2020-06-25 01:09:39 +03:00
global ADB
2017-08-16 15:15:12 +03:00
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 :
2019-03-11 23:41:37 +03:00
options . browser = ' open '
2017-08-16 15:15:12 +03:00
if options . list_browsers :
if options . android :
list_android_browsers ( )
else :
list_pc_browsers ( )
return
2020-10-24 02:08:12 +03:00
if not options . serve and ( options . system_info or options . browser_info ) :
2020-06-25 01:09:39 +03:00
# Don't run if only --system_info or --browser_info was passed.
options . no_server = options . no_browser = True
2017-08-16 15:15:12 +03:00
2020-10-24 02:08:12 +03:00
if not options . serve and not ( options . no_server and options . no_browser ) :
2017-08-16 15:15:12 +03:00
logi ( usage_str )
logi ( ' ' )
logi ( ' Type emrun --help for a detailed list of available options. ' )
return
2020-10-24 02:08:12 +03:00
if options . serve :
file_to_serve = options . serve
else :
file_to_serve = ' . '
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +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 :
2019-12-17 15:01:39 +03:00
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 ) )
2017-08-16 15:15:12 +03:00
if file_to_serve_is_url :
url = file_to_serve
else :
url = os . path . relpath ( os . path . abspath ( file_to_serve ) , serve_dir )
2020-10-24 02:08:12 +03:00
if len ( options . cmdlineparams ) :
url + = ' ? ' + ' & ' . join ( options . cmdlineparams )
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +03:00
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)
2017-08-16 15:15:12 +03:00
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.
2017-08-16 15:15:12 +03:00
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.
2017-08-16 15:15:12 +03:00
if options . browser :
2018-01-18 20:28:27 +03:00
options . browser = unwrap ( options . browser )
2017-08-16 15:15:12 +03:00
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 ]
2018-01-18 20:28:27 +03:00
browser_args = shlex . split ( unwrap ( options . browser_args ) )
2017-08-16 15:15:12 +03:00
2021-02-04 14:00:33 +03:00
if MACOS and ( ' safari ' in browser_exe . lower ( ) or browser_exe == ' open ' ) :
2020-06-25 01:09:39 +03:00
# 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.
2021-02-04 14:00:33 +03:00
browser = [ ' open ' , ' -a ' , ' Safari ' ] + ( browser [ 1 : ] if len ( browser ) > 1 else [ ] )
browser_exe = ' /Applications/Safari.app/Contents/MacOS/Safari '
2017-08-16 15:15:12 +03:00
processname_killed_atexit = ' Safari '
elif ' chrome ' in browser_exe . lower ( ) :
processname_killed_atexit = ' chrome '
2019-04-02 00:10:30 +03:00
browser_args + = [ ' --enable-nacl ' , ' --enable-pnacl ' , ' --disable-restore-session-state ' , ' --enable-webgl ' , ' --no-default-browser-check ' , ' --no-first-run ' , ' --allow-file-access-from-files ' ]
2019-12-17 15:01:39 +03:00
if options . private_browsing :
2019-04-02 00:10:30 +03:00
browser_args + = [ ' --incognito ' ]
2017-08-16 15:15:12 +03:00
# 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 '
2019-12-17 15:01:39 +03:00
if options . private_browsing :
2019-04-02 00:10:30 +03:00
browser_args + = [ ' -private ' ]
2017-08-16 15:15:12 +03:00
elif ' opera ' in browser_exe . lower ( ) :
processname_killed_atexit = ' opera '
2020-06-25 01:09:39 +03:00
# In Windows cmdline, & character delimits multiple commmands, so must
# use ^ to escape them.
2017-08-16 15:15:12 +03:00
if browser_exe == ' cmd ' :
url = url . replace ( ' & ' , ' ^& ' )
2019-12-17 15:01:39 +03:00
url = url . replace ( ' 0.0.0.0 ' , ' localhost ' )
2017-08-16 15:15:12 +03:00
browser + = browser_args + [ url ]
2020-06-25 01:09:39 +03:00
if options . kill_start :
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +03:00
def run ( cmd ) :
2021-03-16 20:54:26 +03:00
logi ( str ( cmd ) )
2017-08-16 15:15:12 +03:00
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 " ' ]
2020-06-25 01:09:39 +03:00
# 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.
2017-08-16 15:15:12 +03:00
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 ( )
2019-12-17 15:01:39 +03:00
browser + = [ ' -no-remote ' , ' --profile ' , profile_dir . replace ( ' \\ ' , ' / ' ) ]
2017-08-16 15:15:12 +03:00
if options . system_info :
logi ( ' Time of run: ' + time . strftime ( " %x %X " ) )
2021-03-16 20:54:26 +03:00
logi ( get_system_info ( format_json = options . json ) )
2017-08-16 15:15:12 +03:00
if options . browser_info :
if options . android :
if options . json :
2021-03-16 20:54:26 +03:00
logi ( json . dumps ( { ' browser ' : ' Android ' + browser_app } , indent = 2 ) )
2017-08-16 15:15:12 +03:00
else :
logi ( ' Browser: Android ' + browser_app )
else :
2021-03-16 20:54:26 +03:00
logi ( get_browser_info ( browser_exe , format_json = options . json ) )
2017-08-16 15:15:12 +03:00
# Suppress run warning if requested.
if options . no_emrun_detect :
emrun_not_enabled_nag_printed = True
if options . log_stdout :
2020-06-25 01:09:39 +03:00
global browser_stdout_handle
2018-02-06 01:45:30 +03:00
browser_stdout_handle = open ( options . log_stdout , ' a ' )
2017-08-16 15:15:12 +03:00
if options . log_stderr :
2020-06-25 01:09:39 +03:00
global browser_stderr_handle
2017-08-16 15:15:12 +03:00
if options . log_stderr == options . log_stdout :
browser_stderr_handle = browser_stdout_handle
else :
2018-02-06 01:45:30 +03:00
browser_stderr_handle = open ( options . log_stderr , ' a ' )
2017-08-16 15:15:12 +03:00
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 :
2021-02-04 14:00:33 +03:00
logv ( " Starting browser: %s " % ' ' . join ( browser ) )
2019-02-17 20:38:58 +03:00
# if browser[0] == 'cmd':
2020-06-25 01:09:39 +03:00
# 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
2021-02-04 14:00:33 +03:00
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 ' ] ) )
2017-08-16 15:15:12 +03:00
browser_process = subprocess . Popen ( browser , env = subprocess_env ( ) )
2021-02-04 14:00:33 +03:00
logv ( ' Launched browser process with pid= ' + str ( browser_process . pid ) )
2020-06-25 01:09:39 +03:00
if options . kill_exit :
2017-08-16 15:15:12 +03:00
atexit . register ( kill_browser_process )
2020-06-25 01:09:39 +03:00
# For Android automation, we execute adb, so this process does not
# represent a browser and no point killing it.
2017-08-16 15:15:12 +03:00
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 :
2017-08-16 15:15:12 +03:00
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 :
2019-01-18 22:11:03 +03:00
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
2017-08-16 15:15:12 +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 :
2020-06-25 01:09:39 +03:00
if options . kill_exit :
2017-08-16 15:15:12 +03:00
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(). ' )
2020-06-25 01:09:39 +03:00
# 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 )
2017-08-16 15:15:12 +03:00
if not is_browser_process_alive ( ) :
2020-06-25 01:09:39 +03:00
# Browser is no longer running, make sure to clean up the temp Firefox
# profile, if we created one.
2017-08-16 15:15:12 +03:00
delete_emrun_safe_firefox_profile ( )
return page_exit_code
2019-02-17 20:38:58 +03:00
2017-08-16 15:15:12 +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 :
2017-08-16 15:15:12 +03:00
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
2017-08-16 15:15:12 +03:00
if __name__ == ' __main__ ' :
sys . exit ( main ( ) )