#!/usr/bin/env python
# Copyright 2017 The Emscripten Authors. All rights reserved.
# Emscripten is available under two separate licenses, the MIT license and the
# University of Illinois/NCSA Open Source License. Both these licenses can be
# found in the LICENSE file.
"""emrun: Implements machinery that allows running a .html page as if it was a
standard executable file.
Usage: emrun
').replace('\n', '
')
return msg
# HTTP requests are handled from separate threads - synchronize them to avoid race conditions
http_mutex = threading.RLock()
def logi(msg):
"""Prints a log message to 'info' stdout channel. Always printed."""
global last_message_time
with http_mutex:
if emrun_options.log_html:
sys.stdout.write(format_html(msg))
else:
print(msg, file=sys.stdout)
sys.stdout.flush()
last_message_time = tick()
def logv(msg):
"""Prints a verbose log message to stdout channel. Only shown if run with --verbose."""
global emrun_options, last_message_time
with http_mutex:
if emrun_options.verbose:
if emrun_options.log_html:
sys.stdout.write(format_html(msg))
else:
print(msg, file=sys.stdout)
sys.stdout.flush()
last_message_time = tick()
def loge(msg):
"""Prints an error message to stderr channel."""
global last_message_time
with http_mutex:
if emrun_options.log_html:
sys.stderr.write(format_html(msg))
else:
print(msg, file=sys.stderr)
sys.stderr.flush()
last_message_time = tick()
def format_eol(msg):
if WINDOWS:
msg = msg.replace('\r\n', '\n').replace('\n', '\r\n')
return msg
def browser_logi(msg):
"""Prints a message to the browser stdout output stream."""
global browser_stdout_handle, last_message_time
msg = format_eol(msg)
print(msg, file=browser_stdout_handle)
browser_stdout_handle.flush()
last_message_time = tick()
def browser_loge(msg):
"""Prints a message to the browser stderr output stream."""
global browser_stderr_handle, last_message_time
msg = format_eol(msg)
print(msg, file=browser_stderr_handle)
browser_stderr_handle.flush()
last_message_time = tick()
def unquote_u(source):
"""Unquotes a unicode string. (translates ascii-encoded utf string back to utf)"""
result = unquote(source)
if '%u' in result:
result = result.replace('%u', '\\u').decode('unicode_escape')
return result
temp_firefox_profile_dir = None
def delete_emrun_safe_firefox_profile():
"""Deletes the temporary created Firefox profile (if one exists)"""
global temp_firefox_profile_dir
if temp_firefox_profile_dir is not None:
logv('remove_tree("' + temp_firefox_profile_dir + '")')
remove_tree(temp_firefox_profile_dir)
temp_firefox_profile_dir = None
# Firefox has a lot of default behavior that makes it unsuitable for automated/unattended run.
# This function creates a temporary profile directory that customized Firefox with various flags that enable
# automated runs.
def create_emrun_safe_firefox_profile():
global temp_firefox_profile_dir, emrun_options
temp_firefox_profile_dir = tempfile.mkdtemp(prefix='temp_emrun_firefox_profile_')
with open(os.path.join(temp_firefox_profile_dir, 'prefs.js'), 'w') as f:
f.write('''
// Lift the default max 20 workers limit to something higher to avoid hangs when page needs to spawn a lot of threads.
user_pref("dom.workers.maxPerDomain", 100);
// Always allow opening popups
user_pref("browser.popups.showPopupBlocker", false);
user_pref("dom.disable_open_during_load", false);
// Don't ask user if he wants to set Firefox as the default system browser
user_pref("browser.shell.checkDefaultBrowser", false);
user_pref("browser.shell.skipDefaultBrowserCheck", true);
// If automated runs crash, don't resume old tabs on the next run or show safe mode dialogs or anything else extra.
user_pref("browser.sessionstore.resume_from_crash", false);
user_pref("services.sync.prefs.sync.browser.sessionstore.restore_on_demand", false);
user_pref("browser.sessionstore.restore_on_demand", false);
user_pref("browser.sessionstore.max_resumed_crashes", -1);
user_pref("toolkit.startup.max_resumed_crashes", -1);
// Don't show the slow script dialog popup
user_pref("dom.max_script_run_time", 0);
user_pref("dom.max_chrome_script_run_time", 0);
// Don't open a home page at startup
user_pref("startup.homepage_override_url", "about:blank");
user_pref("startup.homepage_welcome_url", "about:blank");
user_pref("browser.startup.homepage", "about:blank");
// Don't try to perform browser (auto)update on the background
user_pref("app.update.auto", false);
user_pref("app.update.enabled", false);
user_pref("app.update.silent", false);
user_pref("app.update.mode", 0);
user_pref("app.update.service.enabled", false);
// Don't check compatibility with add-ons, or (auto)update them
user_pref("extensions.lastAppVersion", '');
user_pref("plugins.hide_infobar_for_outdated_plugin", true);
user_pref("plugins.update.url", '');
// Disable health reporter
user_pref("datareporting.healthreport.service.enabled", false);
// Disable crash reporter
user_pref("toolkit.crashreporter.enabled", false);
// Don't show WhatsNew on first run after every update
user_pref("browser.startup.homepage_override.mstone","ignore");
// Don't show 'know your rights' and a bunch of other nag windows at startup
user_pref("browser.rights.3.shown", true);
user_pref('devtools.devedition.promo.shown', true);
user_pref('extensions.shownSelectionUI', true);
user_pref('browser.newtabpage.introShown', true);
user_pref('browser.download.panel.shown', true);
user_pref('browser.customizemode.tip0.shown', true);
user_pref("browser.toolbarbuttons.introduced.pocket-button", true);
// Don't ask the user if he wants to close the browser when there are multiple tabs.
user_pref("browser.tabs.warnOnClose", false);
// Allow the launched script window to close itself, so that we don't need to kill the browser process in order to move on.
user_pref("dom.allow_scripts_to_close_windows", true);
// Set various update timers to a large value in the future in order to not
// trigger a large mass of update HTTP traffic on each Firefox run on the clean profile.
// 2147483647 seconds since Unix epoch is sometime in the year 2038, and this is the max integer accepted by Firefox.
user_pref("app.update.lastUpdateTime.addon-background-update-timer", 2147483647);
user_pref("app.update.lastUpdateTime.background-update-timer", 2147483647);
user_pref("app.update.lastUpdateTime.blocklist-background-update-timer", 2147483647);
user_pref("app.update.lastUpdateTime.browser-cleanup-thumbnails", 2147483647);
user_pref("app.update.lastUpdateTime.experiments-update-timer", 2147483647);
user_pref("app.update.lastUpdateTime.search-engine-update-timer", 2147483647);
user_pref("app.update.lastUpdateTime.xpi-signature-verification", 2147483647);
user_pref("extensions.getAddons.cache.lastUpdate", 2147483647);
user_pref("media.gmp-eme-adobe.lastUpdate", 2147483647);
user_pref("media.gmp-gmpopenh264.lastUpdate", 2147483647);
user_pref("datareporting.healthreport.nextDataSubmissionTime", "2147483647000");
// Sending Firefox Health Report Telemetry data is not desirable, since these are automated runs.
user_pref("datareporting.healthreport.uploadEnabled", false);
user_pref("datareporting.healthreport.service.enabled", false);
user_pref("datareporting.healthreport.service.firstRun", false);
user_pref("toolkit.telemetry.enabled", false);
user_pref("toolkit.telemetry.unified", false);
user_pref("datareporting.policy.dataSubmissionEnabled", false);
user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
// Allow window.dump() to print directly to console
user_pref("browser.dom.window.dump.enabled", true);
// Disable background add-ons related update & information check pings
user_pref("extensions.update.enabled", false);
user_pref("extensions.getAddons.cache.enabled", false);
// Enable wasm
user_pref("javascript.options.wasm", true);
// Enable SharedArrayBuffer (this profile is for a testing environment, so Spectre/Meltdown don't apply)
user_pref("javascript.options.shared_memory", true);
''')
if emrun_options.private_browsing:
f.write('''
// Start in private browsing mode to not cache anything to disk (everything will be wiped anyway after this run)
user_pref("browser.privatebrowsing.autostart", true);
''')
logv('create_emrun_safe_firefox_profile: Created new Firefox profile "' + temp_firefox_profile_dir + '"')
return temp_firefox_profile_dir
def is_browser_process_alive():
"""Returns whether the browser page we spawned is still running. (note, not
perfect atm, in case we are running in detached mode)"""
global browser_process, current_browser_process_pids, navigation_has_occurred
# If navigation to the web page has not yet occurred, we behave as if the
# browser has not yet even loaded the page, and treat it as if the browser
# is running (as it is just starting up)
if not navigation_has_occurred:
return True
if browser_process and browser_process.poll() is None:
return True
if current_browser_process_pids is not None:
try:
import psutil
for p in current_browser_process_pids:
if psutil.pid_exists(p['pid']):
return True
except Exception:
# Fail gracefully if psutil not available
return True
return False
def kill_browser_process():
"""Kills browser_process and processname_killed_atexit. Also removes the
temporary Firefox profile that was created, if one exists."""
global browser_process, processname_killed_atexit, emrun_options, ADB
if browser_process:
logv('Terminating browser process..')
try:
browser_process.kill()
delete_emrun_safe_firefox_profile()
except Exception as e:
logv('Failed with error ' + str(e) + '!')
browser_process = None
processname_killed_atexit = ''
return
if len(processname_killed_atexit):
if emrun_options.android:
logv("Terminating Android app '" + processname_killed_atexit + "'.")
subprocess.call([ADB, 'shell', 'am', 'force-stop', processname_killed_atexit])
else:
logv("Terminating all processes that have string '" + processname_killed_atexit + "' in their name.")
if WINDOWS:
process_image = processname_killed_atexit if '.exe' in processname_killed_atexit else (processname_killed_atexit + '.exe')
process = subprocess.Popen(['taskkill', '/F', '/IM', process_image, '/T'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
process.communicate()
else:
try:
subprocess.call(['pkill', processname_killed_atexit])
except OSError:
try:
subprocess.call(['killall', processname_killed_atexit])
except OSError:
loge('Both commands pkill and killall failed to clean up the spawned browser process. Perhaps neither of these utilities is available on your system?')
delete_emrun_safe_firefox_profile()
# Clear the process name to represent that the browser is now dead.
processname_killed_atexit = ''
# Our custom HTTP web server that will server the target page to run via .html.
# This is used so that we can load the page via a http:// URL instead of a file:// URL, since those wouldn't work too well unless user allowed XHR without CORS rules.
# Also, the target page will route its stdout and stderr back to here via HTTP requests.
class HTTPWebServer(socketserver.ThreadingMixIn, HTTPServer):
"""Log messaging arriving via HTTP can come in out of sequence. Implement a
sequencing mechanism to enforce ordered transmission."""
expected_http_seq_num = -1
# Stores messages that have arrived out of order, pending for a send as soon as the missing message arrives.
# Kept in sorted order, first element is the oldest message received.
http_message_queue = []
def handle_incoming_message(self, seq_num, log, data):
global have_received_messages
with http_mutex:
have_received_messages = True
if self.expected_http_seq_num == -1:
self.expected_http_seq_num = seq_num + 1
log(data)
elif seq_num == -1: # Message arrived without a sequence number? Just log immediately
log(data)
elif seq_num == self.expected_http_seq_num:
log(data)
self.expected_http_seq_num += 1
self.print_messages_due()
elif seq_num < self.expected_http_seq_num:
log(data)
else:
self.http_message_queue += [(seq_num, data, log)]
self.http_message_queue.sort(key=itemgetter(0))
if len(self.http_message_queue) > 16:
self.print_next_message()
# If it's been too long since we we got a message, prints out the oldest queued message, ignoring the proper order.
# This ensures that if any messages are actually lost, that the message queue will be orderly flushed.
def print_timed_out_messages(self):
global last_message_time
with http_mutex:
now = tick()
max_message_queue_time = 5
if len(self.http_message_queue) and now - last_message_time > max_message_queue_time:
self.print_next_message()
# Skips to printing the next message in queue now, independent of whether there was missed messages in the sequence numbering.
def print_next_message(self):
with http_mutex:
if len(self.http_message_queue):
self.expected_http_seq_num = self.http_message_queue[0][0]
self.print_messages_due()
# Completely flushes all out-of-order messages in the queue.
def print_all_messages(self):
with http_mutex:
while len(self.http_message_queue):
self.print_next_message()
# Prints any messages that are now due after we logged some other previous messages.
def print_messages_due(self):
with http_mutex:
while len(self.http_message_queue):
msg = self.http_message_queue[0]
if msg[0] == self.expected_http_seq_num:
msg[2](msg[1])
self.expected_http_seq_num += 1
self.http_message_queue.pop(0)
else:
return
def serve_forever(self, timeout=0.5):
global emrun_options, last_message_time, page_exit_code, have_received_messages, emrun_not_enabled_nag_printed
self.is_running = True
self.timeout = timeout
while self.is_running:
now = tick()
# Did user close browser?
if not emrun_options.no_browser and not is_browser_process_alive():
delete_emrun_safe_firefox_profile()
if not emrun_options.serve_after_close:
self.is_running = False
# Serve HTTP
self.handle_request()
# Process message log queue
self.print_timed_out_messages()
# If web page was silent for too long without printing anything, kill process.
time_since_message = now - last_message_time
if emrun_options.silence_timeout != 0 and time_since_message > emrun_options.silence_timeout:
self.shutdown()
logi('No activity in ' + str(emrun_options.silence_timeout) + ' seconds. Quitting web server with return code ' + str(emrun_options.timeout_returncode) + '. (--silence_timeout option)')
page_exit_code = emrun_options.timeout_returncode
emrun_options.kill_on_exit = True
# If the page has been running too long as a whole, kill process.
time_since_start = now - page_start_time
if emrun_options.timeout != 0 and time_since_start > emrun_options.timeout:
self.shutdown()
logi('Page has not finished in ' + str(emrun_options.timeout) + ' seconds. Quitting web server with return code ' + str(emrun_options.timeout_returncode) + '. (--timeout option)')
emrun_options.kill_on_exit = True
page_exit_code = emrun_options.timeout_returncode
# If we detect that the page is not running with emrun enabled, print a warning message.
if not emrun_not_enabled_nag_printed and page_last_served_time is not None:
time_since_page_serve = now - page_last_served_time
if not have_received_messages and time_since_page_serve > 10:
logv('The html page you are running is not emrun-capable. Stdout, stderr and exit(returncode) capture will not work. Recompile the application with the --emrun linker flag to enable this, or pass --no_emrun_detect to emrun to hide this check.')
emrun_not_enabled_nag_printed = True
# Clean up at quit, print any leftover messages in queue.
self.print_all_messages()
def handle_error(self, request, client_address):
err = sys.exc_info()[1].args[0]
# Filter out the useless '[Errno 10054] An existing connection was forcibly closed by the remote host' errors that occur when we
# forcibly kill the client.
if err != 10054:
socketserver.BaseServer.handle_error(self, request, client_address)
def shutdown(self):
self.is_running = False
self.print_all_messages()
return 1
# Processes HTTP request back to the browser.
class HTTPHandler(SimpleHTTPRequestHandler):
def send_head(self):
self.protocol_version = 'HTTP/1.1'
global page_last_served_time
path = self.translate_path(self.path)
f = None
# A browser has navigated to this page - check which PID got spawned for the browser
global previous_browser_process_pids, current_browser_process_pids, navigation_has_occurred
if not navigation_has_occurred and current_browser_process_pids is None:
running_browser_process_pids = list_processes_by_name(browser_exe)
for p in running_browser_process_pids:
def pid_existed(pid):
for proc in previous_browser_process_pids:
if proc['pid'] == pid:
return True
return False
current_browser_process_pids = list(filter(lambda x: not pid_existed(x['pid']), running_browser_process_pids))
navigation_has_occurred = True
if os.path.isdir(path):
if not self.path.endswith('/'):
self.send_response(301)
self.send_header("Location", self.path + "/")
self.end_headers()
return None
for index in "index.html", "index.htm":
index = os.path.join(path, index)
if os.path.isfile(index):
path = index
break
else:
# Manually implement directory listing support.
return self.list_directory(path)
try:
f = open(path, 'rb')
except IOError:
self.send_error(404, "File not found: " + path)
return None
self.send_response(200)
guess_file_type = path
# All files of type x.gz are served as gzip-compressed, which means the browser will transparently decode the file before passing the uncompressed bytes to the JS page.
# Note: In a slightly silly manner, detect files ending with "gz" and not ".gz", since both Unity and UE4 generate multiple files with .jsgz, .datagz, .memgz, .symbolsgz suffixes and so on,
# so everything goes.
# Note 2: If the JS application would like to receive the actual bits of a gzipped file, instead of having the browser decompress it immediately, then it can't use the suffix .gz when using emrun.
# To work around, one can use the suffix .gzip instead.
if 'Accept-Encoding' in self.headers and 'gzip' in self.headers['Accept-Encoding'] and path.lower().endswith('gz'):
self.send_header('Content-Encoding', 'gzip')
logv('Serving ' + path + ' as gzip-compressed.')
guess_file_type = guess_file_type[:-2]
if guess_file_type.endswith('.'):
guess_file_type = guess_file_type[:-1]
ctype = self.guess_type(guess_file_type)
if guess_file_type.lower().endswith('.wasm'):
ctype = 'application/wasm'
if guess_file_type.lower().endswith('.js'):
ctype = 'application/javascript'
self.send_header('Content-type', ctype)
fs = os.fstat(f.fileno())
self.send_header("Content-Length", str(fs[6]))
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
self.send_header('Cache-Control', 'no-cache, must-revalidate')
self.send_header('Connection', 'close')
self.send_header('Expires', '-1')
self.send_header('Access-Control-Allow-Origin', '*')
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
self.send_header('Cross-Origin-Resource-Policy', 'cross-origin')
self.end_headers()
page_last_served_time = tick()
return f
def log_request(self, code):
# Filter out 200 OK messages to remove noise.
if code != 200:
SimpleHTTPRequestHandler.log_request(self, code)
def log_message(self, format, *args):
msg = '%s - - [%s] %s\n' % (self.address_string(), self.log_date_time_string(), format % args)
# Filter out 404 messages on favicon.ico not being found to remove noise.
if 'favicon.ico' not in msg:
sys.stderr.write(msg)
def do_POST(self):
self.protocol_version = 'HTTP/1.1'
global page_exit_code, emrun_options, have_received_messages, browser_exe
(_, _, path, query, _) = urlsplit(self.path)
logv('POST: "' + self.path + '" (path: "' + path + '", query: "' + query + '")')
if query.startswith('file='): # Binary file dump/upload handling. Requests to "stdio.html?file=filename" will write binary data to the given file.
data = self.rfile.read(int(self.headers['Content-Length']))
filename = query[len('file='):]
dump_out_directory = 'dump_out'
try:
os.mkdir(dump_out_directory)
except OSError:
pass
filename = os.path.join(dump_out_directory, os.path.normpath(filename))
open(filename, 'wb').write(data)
print('Wrote ' + str(len(data)) + ' bytes to file "' + filename + '".')
have_received_messages = True
elif path == '/system_info':
system_info = json.loads(get_system_info(format_json=True))
try:
browser_info = json.loads(get_browser_info(browser_exe, format_json=True))
except ValueError:
browser_info = ''
data = {'system': system_info, 'browser': browser_info}
self.send_response(200)
self.send_header('Content-type', 'application/json')
self.send_header('Cache-Control', 'no-cache, must-revalidate')
self.send_header('Connection', 'close')
self.send_header('Expires', '-1')
self.end_headers()
self.wfile.write(json.dumps(data))
return
else:
data = self.rfile.read(int(self.headers['Content-Length']))
if str is not bytes and isinstance(data, bytes):
data = data.decode('utf-8')
data = data.replace("+", " ")
data = unquote_u(data)
# 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
is_exit = data.startswith('^exit^')
if data == '^pageload^': # Browser is just notifying that it has successfully launched the page.
have_received_messages = True
elif not is_exit:
log = browser_loge if is_stderr else browser_logi
self.server.handle_incoming_message(seq_num, log, data)
elif 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()
self.send_response(200)
self.send_header('Content-type', 'text/plain')
self.send_header('Cache-Control', 'no-cache, must-revalidate')
self.send_header('Connection', 'close')
self.send_header('Expires', '-1')
self.end_headers()
self.wfile.write(b'OK')
# Returns stdout by running command with universal_newlines=True
def check_output(cmd, universal_newlines=True, *args, **kwargs):
if hasattr(subprocess, "run"):
return subprocess.run(cmd, universal_newlines=universal_newlines, stdout=subprocess.PIPE, check=True, *args, **kwargs).stdout
else:
# check_output is considered as an old API so prefer subprocess.run if possible
return subprocess.check_output(cmd, universal_newlines=universal_newlines, *args, **kwargs)
# From http://stackoverflow.com/questions/4842448/getting-processor-information-in-python
# Returns a string with something like "AMD64, Intel(R) Core(TM) i5-2557M CPU @
# 1.70GHz, Intel64 Family 6 Model 42 Stepping 7, GenuineIntel"
def get_cpu_info():
physical_cores = 1
logical_cores = 1
frequency = 0
try:
if WINDOWS:
import_win32api_modules()
root_winmgmts = GetObject('winmgmts:root\\cimv2')
cpus = root_winmgmts.ExecQuery('Select * from Win32_Processor')
cpu_name = cpus[0].Name + ', ' + platform.processor()
physical_cores = int(check_output(['wmic', 'cpu', 'get', 'NumberOfCores']).split('\n')[1].strip())
logical_cores = int(check_output(['wmic', 'cpu', 'get', 'NumberOfLogicalProcessors']).split('\n')[1].strip())
frequency = int(check_output(['wmic', 'cpu', 'get', 'MaxClockSpeed']).split('\n')[1].strip())
elif MACOS:
cpu_name = check_output(['sysctl', '-n', 'machdep.cpu.brand_string']).strip()
physical_cores = int(check_output(['sysctl', '-n', 'machdep.cpu.core_count']).strip())
logical_cores = int(check_output(['sysctl', '-n', 'machdep.cpu.thread_count']).strip())
frequency = int(check_output(['sysctl', '-n', 'hw.cpufrequency']).strip()) // 1000000
elif LINUX:
all_info = check_output(['cat', '/proc/cpuinfo']).strip()
for line in all_info.split("\n"):
if 'model name' in line:
cpu_name = re.sub('.*model name.*:', '', line, 1).strip()
lscpu = check_output(['lscpu'])
frequency = int(float(re.search('CPU MHz: (.*)', lscpu).group(1).strip()) + 0.5)
sockets = int(re.search(r'Socket\(s\): (.*)', lscpu).group(1).strip())
physical_cores = sockets * int(re.search(r'Core\(s\) per socket: (.*)', lscpu).group(1).strip())
logical_cores = physical_cores * int(re.search(r'Thread\(s\) per core: (.*)', lscpu).group(1).strip())
except Exception as e:
import traceback
print(traceback.format_exc())
return {'model': 'Unknown ("' + str(e) + '")',
'physicalCores': 1,
'logicalCores': 1,
'frequency': 0
}
return {'model': platform.machine() + ', ' + cpu_name,
'physicalCores': physical_cores,
'logicalCores': logical_cores,
'frequency': frequency
}
def get_android_cpu_infoline():
lines = check_output([ADB, 'shell', 'cat', '/proc/cpuinfo']).split('\n')
processor = ''
hardware = ''
for line in lines:
if line.startswith('Processor'):
processor = line[line.find(':') + 1:].strip()
elif line.startswith('Hardware'):
hardware = line[line.find(':') + 1:].strip()
freq = int(check_output([ADB, 'shell', 'cat', '/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq']).strip()) // 1000
return 'CPU: ' + processor + ', ' + hardware + ' @ ' + str(freq) + ' MHz'
def win_get_gpu_info():
gpus = []
def find_gpu_model(model):
for gpu in gpus:
if gpu['model'] == model:
return gpu
return None
try:
import_win32api_modules()
except Exception:
return []
for i in range(0, 16):
try:
hHardwareReg = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'HARDWARE')
hDeviceMapReg = winreg.OpenKey(hHardwareReg, 'DEVICEMAP')
hVideoReg = winreg.OpenKey(hDeviceMapReg, 'VIDEO')
VideoCardString = winreg.QueryValueEx(hVideoReg, '\\Device\\Video' + str(i))[0]
# Get Rid of Registry/Machine from the string
VideoCardStringSplit = VideoCardString.split('\\')
ClearnVideoCardString = "\\".join(VideoCardStringSplit[3:])
# Go up one level for detailed
# VideoCardStringRoot = "\\".join(VideoCardStringSplit[3:len(VideoCardStringSplit)-1])
# Get the graphics card information
hVideoCardReg = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, ClearnVideoCardString)
try:
VideoCardDescription = winreg.QueryValueEx(hVideoCardReg, 'Device Description')[0]
except WindowsError:
VideoCardDescription = winreg.QueryValueEx(hVideoCardReg, 'DriverDesc')[0]
try:
driverVersion = winreg.QueryValueEx(hVideoCardReg, 'DriverVersion')[0]
VideoCardDescription += ', driver version ' + driverVersion
except WindowsError:
pass
try:
driverDate = winreg.QueryValueEx(hVideoCardReg, 'DriverDate')[0]
VideoCardDescription += ' (' + driverDate + ')'
except WindowsError:
pass
VideoCardMemorySize = winreg.QueryValueEx(hVideoCardReg, 'HardwareInformation.MemorySize')[0]
try:
vram = struct.unpack('l', bytes(VideoCardMemorySize))[0]
except struct.error:
vram = int(VideoCardMemorySize)
if not find_gpu_model(VideoCardDescription):
gpus += [{'model': VideoCardDescription, 'ram': vram}]
except WindowsError:
pass
return gpus
def linux_get_gpu_info():
glinfo = ''
try:
glxinfo = check_output('glxinfo')
for line in glxinfo.split("\n"):
if "OpenGL vendor string:" in line:
gl_vendor = line[len("OpenGL vendor string:"):].strip()
if "OpenGL version string:" in line:
gl_version = line[len("OpenGL version string:"):].strip()
if "OpenGL renderer string:" in line:
gl_renderer = line[len("OpenGL renderer string:"):].strip()
glinfo = gl_vendor + ' ' + gl_renderer + ', GL version ' + gl_version
except Exception as e:
logv(e)
adapterinfo = ''
try:
vgainfo = check_output(['lshw', '-C', 'display'], stderr=subprocess.PIPE)
vendor = re.search("vendor: (.*)", vgainfo).group(1).strip()
product = re.search("product: (.*)", vgainfo).group(1).strip()
description = re.search("description: (.*)", vgainfo).group(1).strip()
clock = re.search("clock: (.*)", vgainfo).group(1).strip()
adapterinfo = vendor + ' ' + product + ', ' + description + ' (' + clock + ')'
except Exception as e:
logv(e)
ram = 0
try:
vgainfo = check_output('lspci -v -s $(lspci | grep VGA | cut -d " " -f 1)', shell=True, stderr=subprocess.PIPE)
ram = int(re.search(r"\[size=([0-9]*)M\]", vgainfo).group(1)) * 1024 * 1024
except Exception as e:
logv(e)
model = (adapterinfo + ' ' + glinfo).strip()
if not model:
model = 'Unknown'
return [{'model': model, 'ram': ram}]
def macos_get_gpu_info():
gpus = []
try:
info = check_output(['system_profiler', 'SPDisplaysDataType'])
info = info.split("Chipset Model:")[1:]
for gpu in info:
model_name = gpu.split('\n')[0].strip()
bus = re.search("Bus: (.*)", gpu).group(1).strip()
memory = int(re.search("VRAM (.*?): (.*) MB", gpu).group(2).strip())
gpus += [{'model': model_name + ' (' + bus + ')', 'ram': memory * 1024 * 1024}]
return gpus
except Exception:
pass
def get_gpu_info():
if WINDOWS:
return win_get_gpu_info()
elif LINUX:
return linux_get_gpu_info()
elif MACOS:
return macos_get_gpu_info()
else:
return []
def get_executable_version(filename):
try:
if WINDOWS:
info = win32api.GetFileVersionInfo(filename, "\\")
ms = info['FileVersionMS']
ls = info['FileVersionLS']
version = win32api.HIWORD(ms), win32api.LOWORD(ms), win32api.HIWORD(ls), win32api.LOWORD(ls)
return '.'.join(map(str, version))
elif MACOS:
plistfile = filename[0:filename.find('MacOS')] + 'Info.plist'
info = plistlib.readPlist(plistfile)
# Data in Info.plists is a bit odd, this check combo gives best information on each browser.
if 'firefox' in filename.lower():
return info['CFBundleShortVersionString']
if 'opera' in filename.lower():
return info['CFBundleVersion']
else:
return info['CFBundleShortVersionString']
elif LINUX:
if 'firefox' in filename.lower():
version = check_output([filename, '-v'])
version = version.replace('Mozilla Firefox ', '')
return version.strip()
else:
return ""
except Exception as e:
logv(e)
return ""
def get_browser_build_date(filename):
try:
if MACOS:
plistfile = filename[0:filename.find('MacOS')] + 'Info.plist'
info = plistlib.readPlist(plistfile)
# Data in Info.plists is a bit odd, this check combo gives best information on each browser.
if 'firefox' in filename.lower():
return '20' + '-'.join(map((lambda x: x.zfill(2)), info['CFBundleVersion'][2:].split('.')))
except Exception as e:
logv(e)
# No exact information about the build date, so take the last modified date of the file.
# This is not right, but assuming that one installed the browser shortly after the update was
# available, it's shooting close.
try:
return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(os.path.getmtime(filename)))
except Exception as e:
logv(e)
return '(unknown)'
def get_browser_info(filename, format_json):
if format_json:
return json.dumps({
'name': browser_display_name(filename),
'version': get_executable_version(filename),
'buildDate': get_browser_build_date(filename)
}, indent=2)
else:
return 'Browser: ' + browser_display_name(filename) + ' ' + get_executable_version(filename) + ', build ' + get_browser_build_date(filename)
# http://stackoverflow.com/questions/580924/python-windows-file-version-attribute
def win_get_file_properties(fname):
propNames = ('Comments', 'InternalName', 'ProductName',
'CompanyName', 'LegalCopyright', 'ProductVersion',
'FileDescription', 'LegalTrademarks', 'PrivateBuild',
'FileVersion', 'OriginalFilename', 'SpecialBuild')
props = {'FixedFileInfo': None, 'StringFileInfo': None, 'FileVersion': None}
try:
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
except Exception:
pass
return props
def get_computer_model():
try:
if MACOS:
try:
with open(os.path.join(os.getenv("HOME"), '.emrun.hwmodel.cached'), 'r') as f:
model = f.read()
return model
except IOError:
pass
try:
# http://apple.stackexchange.com/questions/98080/can-a-macs-model-year-be-determined-via-terminal-command
serial = check_output(['system_profiler', 'SPHardwareDataType'])
serial = re.search("Serial Number (.*): (.*)", serial)
serial = serial.group(2).strip()[-4:]
cmd = ['curl', '-s', 'http://support-sp.apple.com/sp/product?cc=' + serial]
logv(str(cmd))
model = check_output(cmd)
model = re.search('