Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
emscripten-core
GitHub Repository: emscripten-core/emscripten
Path: blob/main/emrun.py
4091 views
1
#!/usr/bin/env python3
2
# Copyright 2017 The Emscripten Authors. All rights reserved.
3
# Emscripten is available under two separate licenses, the MIT license and the
4
# University of Illinois/NCSA Open Source License. Both these licenses can be
5
# found in the LICENSE file.
6
7
# This file needs to run on older version of python too (even python 2!) so
8
# suppress these upgrade warnings:
9
# ruff: noqa: UP015, UP024, UP021, UP025
10
11
"""emrun: Implements machinery that allows running a .html page as if it was a
12
standard executable file.
13
14
Usage: emrun <options> filename.html <args to program>
15
16
See emrun --help for more information
17
"""
18
19
# N.B. Do not introduce external dependencies to this file. It is often used
20
# standalone outside Emscripten directory tree.
21
import argparse
22
import atexit
23
import json
24
import math
25
import os
26
import platform
27
import re
28
import shlex
29
import shutil
30
import socket
31
import stat
32
import struct
33
import subprocess
34
import sys
35
import tempfile
36
import threading
37
import time
38
from operator import itemgetter
39
40
if sys.version_info.major == 2:
41
import SocketServer as socketserver
42
from BaseHTTPServer import HTTPServer
43
from SimpleHTTPServer import SimpleHTTPRequestHandler
44
from urllib import unquote
45
from urlparse import urlsplit
46
else:
47
import socketserver
48
from http.server import HTTPServer, SimpleHTTPRequestHandler
49
from urllib.parse import unquote, urlsplit
50
51
# Populated from cmdline params
52
emrun_options = None
53
54
# Represents the process object handle to the browser we opened to run the html
55
# page.
56
browser_process = None
57
58
previous_browser_processes = None
59
current_browser_processes = None
60
61
navigation_has_occurred = False
62
63
# Stores the browser executable that was run with --browser= parameter.
64
browser_exe = None
65
66
# If we have routed browser output to file with --log-stdout and/or
67
# --log-stderr, these track the handles.
68
browser_stdout_handle = sys.stdout
69
browser_stderr_handle = sys.stderr
70
71
# This flag tracks whether the html page has sent any stdout messages back to
72
# us. Used to detect whether we might have gotten detached from the browser
73
# process we spawned, in which case we are not able to detect when user closes
74
# the browser with the close button.
75
have_received_messages = False
76
77
# At startup print a warning message once if user did not build with --emrun.
78
emrun_not_enabled_nag_printed = False
79
80
# Stores the exit() code of the html page when/if it quits.
81
page_exit_code = None
82
83
# If this is set to a non-empty string, all processes by this name will be
84
# killed at exit. This is used to clean up after browsers that spawn
85
# subprocesses to handle the actual browser launch. For example opera has a
86
# launcher.exe that runs the actual opera browser. So killing browser_process
87
# would just kill launcher.exe and not the opera
88
# browser itself.
89
processname_killed_atexit = ""
90
91
# Using "0.0.0.0" means "all interfaces", which should allow connecting to this
92
# server via LAN addresses. Using "localhost" should allow only connecting from
93
# local computer.
94
default_webserver_hostname = '0.0.0.0'
95
96
# If user does not specify a --port parameter, this port is used to launch the
97
# server.
98
default_webserver_port = 6931
99
100
# Location of Android Debug Bridge executable
101
ADB = None
102
103
# Host OS detection to autolocate browsers and other OS-specific support needs.
104
WINDOWS = False
105
LINUX = False
106
MACOS = False
107
if os.name == 'nt':
108
WINDOWS = True
109
import winreg
110
elif platform.system() == 'Linux':
111
LINUX = True
112
elif platform.mac_ver()[0] != '':
113
MACOS = True
114
import plistlib
115
116
# If you are running on an OS that is not any of these, must add explicit support for it.
117
if not WINDOWS and not LINUX and not MACOS:
118
raise Exception("Unknown OS!")
119
120
121
# Returns wallclock time in seconds.
122
def tick():
123
# Would like to return time.clock() since it's apparently better for
124
# precision, but it is broken on macOS 10.10 and Python 2.7.8.
125
return time.time()
126
127
128
# Absolute wallclock time in seconds specifying when the previous HTTP stdout
129
# message from the page was received.
130
last_message_time = tick()
131
132
# Absolute wallclock time in seconds telling when we launched emrun.
133
page_start_time = tick()
134
135
# Stores the time of most recent http page serve.
136
page_last_served_time = None
137
138
139
# HTTP requests are handled from separate threads - synchronize them to avoid race conditions
140
http_mutex = threading.RLock()
141
142
143
def logi(msg):
144
"""Prints a log message to 'info' stdout channel. Always printed.
145
"""
146
global last_message_time
147
with http_mutex:
148
sys.stdout.write(msg + '\n')
149
sys.stdout.flush()
150
last_message_time = tick()
151
152
153
def logv(msg):
154
"""Prints a verbose log message to stdout channel.
155
Only shown if run with --verbose.
156
"""
157
global last_message_time
158
if emrun_options.verbose:
159
with http_mutex:
160
sys.stdout.write(msg + '\n')
161
sys.stdout.flush()
162
last_message_time = tick()
163
164
165
def loge(msg):
166
"""Prints an error message to stderr channel.
167
"""
168
global last_message_time
169
with http_mutex:
170
sys.stderr.write(msg + '\n')
171
sys.stderr.flush()
172
last_message_time = tick()
173
174
175
def format_eol(msg):
176
if WINDOWS:
177
msg = msg.replace('\r\n', '\n').replace('\n', '\r\n')
178
return msg
179
180
181
def browser_logi(msg):
182
"""Prints a message to the browser stdout output stream.
183
"""
184
global last_message_time
185
msg = format_eol(msg)
186
browser_stdout_handle.write(msg + '\n')
187
browser_stdout_handle.flush()
188
last_message_time = tick()
189
190
191
def browser_loge(msg):
192
"""Prints a message to the browser stderr output stream.
193
"""
194
global last_message_time
195
msg = format_eol(msg)
196
browser_stderr_handle.write(msg + '\n')
197
browser_stderr_handle.flush()
198
last_message_time = tick()
199
200
201
def unquote_u(source):
202
"""Unquotes a unicode string.
203
(translates ascii-encoded utf string back to utf)
204
"""
205
result = unquote(source)
206
if '%u' in result:
207
result = result.replace('%u', '\\u').decode('unicode_escape')
208
return result
209
210
211
temp_firefox_profile_dir = None
212
213
214
def delete_emrun_safe_firefox_profile():
215
"""Deletes the temporary created Firefox profile (if one exists)"""
216
global temp_firefox_profile_dir
217
if temp_firefox_profile_dir is not None:
218
logv('remove_tree("' + temp_firefox_profile_dir + '")')
219
remove_tree(temp_firefox_profile_dir)
220
temp_firefox_profile_dir = None
221
222
223
# Firefox has a lot of default behavior that makes it unsuitable for
224
# automated/unattended run.
225
# This function creates a temporary profile directory that customized Firefox
226
# with various flags that enable automated runs.
227
def create_emrun_safe_firefox_profile():
228
global temp_firefox_profile_dir
229
temp_firefox_profile_dir = tempfile.mkdtemp(prefix='temp_emrun_firefox_profile_')
230
with open(os.path.join(temp_firefox_profile_dir, 'prefs.js'), 'w') as f:
231
f.write('''
232
// Lift the default max 20 workers limit to something higher to avoid hangs when page needs to spawn a lot of threads.
233
user_pref("dom.workers.maxPerDomain", 100);
234
// Always allow opening popups
235
user_pref("browser.popups.showPopupBlocker", false);
236
user_pref("dom.disable_open_during_load", false);
237
// Don't ask user if they want to set Firefox as the default system browser
238
user_pref("browser.shell.checkDefaultBrowser", false);
239
user_pref("browser.shell.skipDefaultBrowserCheck", true);
240
// If automated runs crash, don't resume old tabs on the next run or show safe mode dialogs or anything else extra.
241
user_pref("browser.sessionstore.resume_from_crash", false);
242
user_pref("services.sync.prefs.sync.browser.sessionstore.restore_on_demand", false);
243
user_pref("browser.sessionstore.restore_on_demand", false);
244
user_pref("browser.sessionstore.max_resumed_crashes", -1);
245
user_pref("toolkit.startup.max_resumed_crashes", -1);
246
// Don't show the slow script dialog popup
247
user_pref("dom.max_script_run_time", 0);
248
user_pref("dom.max_chrome_script_run_time", 0);
249
// Don't open a home page at startup
250
user_pref("startup.homepage_override_url", "about:blank");
251
user_pref("startup.homepage_welcome_url", "about:blank");
252
user_pref("browser.startup.homepage", "about:blank");
253
// Don't try to perform browser (auto)update on the background
254
user_pref("app.update.auto", false);
255
user_pref("app.update.enabled", false);
256
user_pref("app.update.silent", false);
257
user_pref("app.update.mode", 0);
258
user_pref("app.update.service.enabled", false);
259
// Don't check compatibility with add-ons, or (auto)update them
260
user_pref("extensions.lastAppVersion", '');
261
user_pref("plugins.hide_infobar_for_outdated_plugin", true);
262
user_pref("plugins.update.url", '');
263
// Disable health reporter
264
user_pref("datareporting.healthreport.service.enabled", false);
265
// Disable crash reporter
266
user_pref("toolkit.crashreporter.enabled", false);
267
// Don't show WhatsNew on first run after every update
268
user_pref("browser.startup.homepage_override.mstone","ignore");
269
// Don't show 'know your rights' and a bunch of other nag windows at startup
270
user_pref("browser.rights.3.shown", true);
271
user_pref('devtools.devedition.promo.shown', true);
272
user_pref('extensions.shownSelectionUI', true);
273
user_pref('browser.newtabpage.introShown', true);
274
user_pref('browser.download.panel.shown', true);
275
user_pref('browser.customizemode.tip0.shown', true);
276
user_pref("browser.toolbarbuttons.introduced.pocket-button", true);
277
// Don't ask the user if they want to close the browser when there are multiple tabs.
278
user_pref("browser.tabs.warnOnClose", false);
279
// Allow the launched script window to close itself, so that we don't need to kill the browser process in order to move on.
280
user_pref("dom.allow_scripts_to_close_windows", true);
281
// Set various update timers to a large value in the future in order to not
282
// trigger a large mass of update HTTP traffic on each Firefox run on the clean profile.
283
// 2147483647 seconds since Unix epoch is sometime in the year 2038, and this is the max integer accepted by Firefox.
284
user_pref("app.update.lastUpdateTime.addon-background-update-timer", 2147483647);
285
user_pref("app.update.lastUpdateTime.background-update-timer", 2147483647);
286
user_pref("app.update.lastUpdateTime.blocklist-background-update-timer", 2147483647);
287
user_pref("app.update.lastUpdateTime.browser-cleanup-thumbnails", 2147483647);
288
user_pref("app.update.lastUpdateTime.experiments-update-timer", 2147483647);
289
user_pref("app.update.lastUpdateTime.search-engine-update-timer", 2147483647);
290
user_pref("app.update.lastUpdateTime.xpi-signature-verification", 2147483647);
291
user_pref("extensions.getAddons.cache.lastUpdate", 2147483647);
292
user_pref("media.gmp-eme-adobe.lastUpdate", 2147483647);
293
user_pref("media.gmp-gmpopenh264.lastUpdate", 2147483647);
294
user_pref("datareporting.healthreport.nextDataSubmissionTime", "2147483647000");
295
// Sending Firefox Health Report Telemetry data is not desirable, since these are automated runs.
296
user_pref("datareporting.healthreport.uploadEnabled", false);
297
user_pref("datareporting.healthreport.service.enabled", false);
298
user_pref("datareporting.healthreport.service.firstRun", false);
299
user_pref("toolkit.telemetry.enabled", false);
300
user_pref("toolkit.telemetry.unified", false);
301
user_pref("datareporting.policy.dataSubmissionEnabled", false);
302
user_pref("datareporting.policy.dataSubmissionPolicyBypassNotification", true);
303
// Allow window.dump() to print directly to console
304
user_pref("browser.dom.window.dump.enabled", true);
305
// Disable background add-ons related update & information check pings
306
user_pref("extensions.update.enabled", false);
307
user_pref("extensions.getAddons.cache.enabled", false);
308
// Enable wasm
309
user_pref("javascript.options.wasm", true);
310
// Enable SharedArrayBuffer (this profile is for a testing environment, so Spectre/Meltdown don't apply)
311
user_pref("javascript.options.shared_memory", true);
312
''')
313
if emrun_options.private_browsing:
314
f.write('''
315
// Start in private browsing mode to not cache anything to disk (everything will be wiped anyway after this run)
316
user_pref("browser.privatebrowsing.autostart", true);
317
''')
318
logv('create_emrun_safe_firefox_profile: Created new Firefox profile "' + temp_firefox_profile_dir + '"')
319
return temp_firefox_profile_dir
320
321
322
def is_browser_process_alive():
323
"""Returns whether the browser page we spawned is still running.
324
(note, not perfect atm, in case we are running in detached mode)
325
"""
326
# If navigation to the web page has not yet occurred, we behave as if the
327
# browser has not yet even loaded the page, and treat it as if the browser
328
# is running (as it is just starting up)
329
if not navigation_has_occurred:
330
return True
331
332
if browser_process and browser_process.poll() is None:
333
return True
334
335
if current_browser_processes:
336
try:
337
import psutil
338
for p in current_browser_processes:
339
if psutil.pid_exists(p['pid']):
340
return True
341
return False
342
except Exception:
343
# Fail gracefully if psutil not available
344
logv('psutil is not available, emrun may not be able to accurately track whether the browser process is alive or not')
345
346
# We do not have a track of the browser process ID that we spawned.
347
# Make an assumption that the browser process is open as long until
348
# the C program calls exit().
349
return page_exit_code is None
350
351
352
def kill_browser_process():
353
"""Kills browser_process and processname_killed_atexit. Also removes the
354
temporary Firefox profile that was created, if one exists.
355
"""
356
global browser_process, processname_killed_atexit, current_browser_processes
357
if browser_process and browser_process.poll() is None:
358
try:
359
logv('Terminating browser process pid=' + str(browser_process.pid) + '..')
360
browser_process.kill()
361
except Exception as e:
362
logv('Failed with error ' + str(e) + '!')
363
364
browser_process = None
365
# We have a hold of the target browser process explicitly, no need to resort to killall,
366
# so clear that record out.
367
processname_killed_atexit = ''
368
369
if current_browser_processes:
370
for pid in current_browser_processes:
371
try:
372
logv('Terminating browser process pid=' + str(pid['pid']) + '..')
373
os.kill(pid['pid'], 9)
374
except Exception as e:
375
logv('Failed with error ' + str(e) + '!')
376
377
current_browser_processes = None
378
# We have a hold of the target browser process explicitly, no need to resort to killall,
379
# so clear that record out.
380
processname_killed_atexit = ''
381
382
if len(processname_killed_atexit):
383
if emrun_options.android:
384
logv("Terminating Android app '" + processname_killed_atexit + "'.")
385
subprocess.call([ADB, 'shell', 'am', 'force-stop', processname_killed_atexit])
386
else:
387
logv("Terminating all processes that have string '" + processname_killed_atexit + "' in their name.")
388
if WINDOWS:
389
process_image = processname_killed_atexit if '.exe' in processname_killed_atexit else (processname_killed_atexit + '.exe')
390
process = subprocess.Popen(['taskkill', '/F', '/IM', process_image, '/T'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
391
process.communicate()
392
else:
393
try:
394
subprocess.call(['pkill', processname_killed_atexit])
395
except OSError:
396
try:
397
subprocess.call(['killall', processname_killed_atexit])
398
except OSError:
399
loge('Both commands pkill and killall failed to clean up the spawned browser process. Perhaps neither of these utilities is available on your system?')
400
delete_emrun_safe_firefox_profile()
401
# Clear the process name to represent that the browser is now dead.
402
processname_killed_atexit = ''
403
404
delete_emrun_safe_firefox_profile()
405
406
407
# Heuristic that attempts to search for the browser process IDs that emrun spawned.
408
# This depends on the assumption that no other browser process IDs have been spawned
409
# during the short time perioed between the time that emrun started, and the browser
410
# process navigated to the page.
411
# This heuristic is needed because all modern browsers are multiprocess systems -
412
# starting a browser process from command line generally launches just a "stub" spawner
413
# process that immediately exits.
414
def detect_browser_processes():
415
if not browser_exe:
416
return # Running with --no-browser, we are not binding to a spawned browser.
417
418
global current_browser_processes
419
logv('First navigation occurred. Identifying currently running browser processes')
420
running_browser_processes = list_processes_by_name(browser_exe)
421
422
def pid_existed(pid):
423
for proc in previous_browser_processes:
424
if proc['pid'] == pid:
425
return True
426
return False
427
428
for p in running_browser_processes:
429
logv('Detected running browser process id: ' + str(p['pid']) + ', existed already at emrun startup? ' + str(pid_existed(p['pid'])))
430
431
current_browser_processes = [p for p in running_browser_processes if not pid_existed(p['pid'])]
432
433
if len(current_browser_processes) == 0:
434
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.')
435
436
437
# Our custom HTTP web server that will server the target page to run via .html.
438
# This is used so that we can load the page via a http:// URL instead of a
439
# file:// URL, since those wouldn't work too well unless user allowed XHR
440
# without CORS rules. Also, the target page will route its stdout and stderr
441
# back to here via HTTP requests.
442
class HTTPWebServer(socketserver.ThreadingMixIn, HTTPServer):
443
"""Log messaging arriving via HTTP can come in out of sequence. Implement a
444
sequencing mechanism to enforce ordered transmission."""
445
expected_http_seq_num = 1
446
# Stores messages that have arrived out of order, pending for a send as soon
447
# as the missing message arrives. Kept in sorted order, first element is the
448
# oldest message received.
449
http_message_queue = []
450
451
def handle_incoming_message(self, seq_num, log, data):
452
global have_received_messages
453
with http_mutex:
454
have_received_messages = True
455
456
if seq_num == -1:
457
# Message arrived without a sequence number? Just log immediately
458
log(data)
459
elif seq_num == self.expected_http_seq_num:
460
log(data)
461
self.expected_http_seq_num += 1
462
self.print_messages_due()
463
elif seq_num < self.expected_http_seq_num:
464
log(data)
465
else:
466
self.http_message_queue += [(seq_num, data, log)]
467
self.http_message_queue.sort(key=itemgetter(0))
468
if len(self.http_message_queue) > 16:
469
self.print_next_message()
470
471
# If it's been too long since we we got a message, prints out the oldest
472
# queued message, ignoring the proper order. This ensures that if any
473
# messages are actually lost, that the message queue will be orderly flushed.
474
def print_timed_out_messages(self):
475
global last_message_time
476
with http_mutex:
477
now = tick()
478
max_message_queue_time = 5
479
if len(self.http_message_queue) and now - last_message_time > max_message_queue_time:
480
self.print_next_message()
481
482
# Skips to printing the next message in queue now, independent of whether
483
# there was missed messages in the sequence numbering.
484
def print_next_message(self):
485
with http_mutex:
486
if len(self.http_message_queue):
487
self.expected_http_seq_num = self.http_message_queue[0][0]
488
self.print_messages_due()
489
490
# Completely flushes all out-of-order messages in the queue.
491
def print_all_messages(self):
492
with http_mutex:
493
while len(self.http_message_queue):
494
self.print_next_message()
495
496
# Prints any messages that are now due after we logged some other previous
497
# messages.
498
def print_messages_due(self):
499
with http_mutex:
500
while len(self.http_message_queue):
501
msg = self.http_message_queue[0]
502
if msg[0] == self.expected_http_seq_num:
503
msg[2](msg[1])
504
self.expected_http_seq_num += 1
505
self.http_message_queue.pop(0)
506
else:
507
return
508
509
def serve_forever(self, timeout=0.5):
510
global page_exit_code, emrun_not_enabled_nag_printed
511
self.is_running = True
512
self.timeout = timeout
513
logi('Now listening at http://%s/' % ':'.join(map(str, self.socket.getsockname())))
514
logv("Entering web server loop.")
515
while self.is_running:
516
now = tick()
517
# Did user close browser?
518
if emrun_options.run_browser and not is_browser_process_alive():
519
logv("Shutting down because browser is no longer alive")
520
delete_emrun_safe_firefox_profile()
521
if not emrun_options.serve_after_close:
522
logv("Browser process has shut down, quitting web server.")
523
self.is_running = False
524
525
# Serve HTTP
526
self.handle_request()
527
# Process message log queue
528
self.print_timed_out_messages()
529
530
# If web page was silent for too long without printing anything, kill process.
531
time_since_message = now - last_message_time
532
if emrun_options.silence_timeout != 0 and time_since_message > emrun_options.silence_timeout:
533
self.shutdown()
534
logi('No activity in ' + str(emrun_options.silence_timeout) + ' seconds. Quitting web server with return code ' + str(emrun_options.timeout_returncode) + '. (--silence-timeout option)')
535
page_exit_code = emrun_options.timeout_returncode
536
emrun_options.kill_exit = True
537
538
# If the page has been running too long as a whole, kill process.
539
time_since_start = now - page_start_time
540
if emrun_options.timeout != 0 and time_since_start > emrun_options.timeout:
541
self.shutdown()
542
logi('Page has not finished in ' + str(emrun_options.timeout) + ' seconds. Quitting web server with return code ' + str(emrun_options.timeout_returncode) + '. (--timeout option)')
543
emrun_options.kill_exit = True
544
page_exit_code = emrun_options.timeout_returncode
545
546
# If we detect that the page is not running with emrun enabled, print a warning message.
547
if not emrun_not_enabled_nag_printed and page_last_served_time is not None:
548
time_since_page_serve = now - page_last_served_time
549
if not have_received_messages and time_since_page_serve > 10:
550
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.')
551
emrun_not_enabled_nag_printed = True
552
553
# Clean up at quit, print any leftover messages in queue.
554
self.print_all_messages()
555
logv("Web server loop done.")
556
557
def handle_error(self, request, client_address):
558
err = sys.exc_info()[1].args[0]
559
# Filter out the useless '[Errno 10054] An existing connection was forcibly
560
# closed by the remote host' errors that occur when we forcibly kill the
561
# client.
562
if err != 10054:
563
socketserver.BaseServer.handle_error(self, request, client_address)
564
565
def shutdown(self):
566
self.is_running = False
567
self.print_all_messages()
568
return 1
569
570
571
# Processes HTTP request back to the browser.
572
class HTTPHandler(SimpleHTTPRequestHandler):
573
def send_head(self):
574
self.protocol_version = 'HTTP/1.1'
575
global page_last_served_time
576
path = self.translate_path(self.path)
577
f = None
578
579
# A browser has navigated to this page - check which PID got spawned for
580
# the browser
581
global navigation_has_occurred
582
if not navigation_has_occurred and current_browser_processes is None:
583
detect_browser_processes()
584
585
navigation_has_occurred = True
586
587
if os.path.isdir(path):
588
if not self.path.endswith('/'):
589
self.send_response(301)
590
self.send_header("Location", self.path + "/")
591
self.end_headers()
592
return None
593
for index in "index.html", "index.htm":
594
index = os.path.join(path, index)
595
if os.path.isfile(index):
596
path = index
597
break
598
else:
599
# Manually implement directory listing support.
600
return self.list_directory(path)
601
602
try:
603
f = open(path, 'rb')
604
except IOError:
605
self.send_error(404, "File not found: " + path)
606
return None
607
608
self.send_response(200)
609
guess_file_type = path
610
# All files of type x.gz are served as gzip-compressed, which means the
611
# browser will transparently decode the file before passing the
612
# uncompressed bytes to the JS page.
613
# Note: In a slightly silly manner, detect files ending with "gz" and not
614
# ".gz", since both Unity and UE4 generate multiple files with .jsgz,
615
# .datagz, .memgz, .symbolsgz suffixes and so on, so everything goes.
616
# Note 2: If the JS application would like to receive the actual bits of a
617
# gzipped file, instead of having the browser decompress it immediately,
618
# then it can't use the suffix .gz when using emrun.
619
# To work around, one can use the suffix .gzip instead.
620
if path.lower().endswith('gz'):
621
self.send_header('Content-Encoding', 'gzip')
622
logv('Serving ' + path + ' as gzip-compressed.')
623
guess_file_type = guess_file_type[:-2]
624
if guess_file_type.endswith('.'):
625
guess_file_type = guess_file_type[:-1]
626
elif path.lower().endswith('br'):
627
self.send_header('Content-Encoding', 'br')
628
logv('Serving ' + path + ' as brotli-compressed.')
629
guess_file_type = guess_file_type[:-2]
630
if guess_file_type.endswith('.'):
631
guess_file_type = guess_file_type[:-1]
632
633
ctype = self.guess_type(guess_file_type)
634
if guess_file_type.lower().endswith('.wasm'):
635
ctype = 'application/wasm'
636
if guess_file_type.lower().endswith('.js'):
637
ctype = 'application/javascript'
638
self.send_header('Content-type', ctype)
639
fs = os.fstat(f.fileno())
640
self.send_header("Content-Length", str(fs.st_size))
641
self.send_header("Last-Modified", self.date_time_string(fs.st_mtime))
642
self.send_header('Cache-Control', 'no-cache, must-revalidate')
643
self.send_header('Connection', 'close')
644
self.send_header('Expires', '-1')
645
self.send_header('Access-Control-Allow-Origin', '*')
646
self.send_header('Cross-Origin-Opener-Policy', 'same-origin')
647
self.send_header('Cross-Origin-Embedder-Policy', 'require-corp')
648
self.send_header('Cross-Origin-Resource-Policy', 'cross-origin')
649
self.end_headers()
650
page_last_served_time = tick()
651
return f
652
653
def log_request(self, code):
654
# Filter out 200 OK messages to remove noise.
655
if code != 200:
656
SimpleHTTPRequestHandler.log_request(self, code)
657
658
def log_message(self, format, *args):
659
msg = '%s - - [%s] %s\n' % (self.address_string(), self.log_date_time_string(), format % args)
660
# Filter out 404 messages on favicon.ico not being found to remove noise.
661
if 'favicon.ico' not in msg:
662
sys.stderr.write(msg)
663
664
def do_POST(self):
665
self.protocol_version = 'HTTP/1.1'
666
global page_exit_code, have_received_messages
667
668
(_, _, path, query, _) = urlsplit(self.path)
669
logv('POST: "' + self.path + '" (path: "' + path + '", query: "' + query + '")')
670
if query.startswith('file='):
671
# Binary file dump/upload handling. Requests to
672
# "stdio.html?file=filename" will write binary data to the given file.
673
data = self.rfile.read(int(self.headers['Content-Length']))
674
filename = unquote_u(query[len('file='):])
675
filename = os.path.join(emrun_options.dump_out_directory, os.path.normpath(filename))
676
try:
677
os.makedirs(os.path.dirname(filename))
678
except OSError:
679
pass
680
with open(filename, 'wb') as fh:
681
fh.write(data)
682
logi('Wrote ' + str(len(data)) + ' bytes to file "' + filename + '".')
683
have_received_messages = True
684
elif path == '/system_info':
685
system_info = json.loads(get_system_info(format_json=True))
686
try:
687
browser_info = json.loads(get_browser_info(browser_exe, format_json=True))
688
except ValueError:
689
browser_info = ''
690
data = {'system': system_info, 'browser': browser_info}
691
self.send_response(200)
692
self.send_header('Content-type', 'application/json')
693
self.send_header('Cache-Control', 'no-cache, must-revalidate')
694
self.send_header('Connection', 'close')
695
self.send_header('Expires', '-1')
696
self.end_headers()
697
self.wfile.write(json.dumps(data))
698
return
699
else:
700
data = self.rfile.read(int(self.headers['Content-Length']))
701
if str is not bytes and isinstance(data, bytes):
702
data = data.decode('utf-8')
703
data = data.replace("+", " ")
704
data = unquote_u(data)
705
706
if data == '^pageload^': # Browser is just notifying that it has successfully launched the page.
707
have_received_messages = True
708
elif data.startswith('^exit^'):
709
if not emrun_options.serve_after_exit:
710
page_exit_code = int(data[6:])
711
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().')
712
# Set server socket to nonblocking on shutdown to avoid sporadic deadlocks
713
self.server.socket.setblocking(False)
714
self.server.shutdown()
715
return
716
else:
717
# The user page sent a message with POST. Parse the message and log it to stdout/stderr.
718
is_stdout = False
719
is_stderr = False
720
seq_num = -1
721
# The html shell is expected to send messages of form ^out^(number)^(message) or ^err^(number)^(message).
722
if data.startswith('^err^'):
723
is_stderr = True
724
elif data.startswith('^out^'):
725
is_stdout = True
726
if is_stderr or is_stdout:
727
try:
728
i = data.index('^', 5)
729
seq_num = int(data[5:i])
730
data = data[i + 1:]
731
except ValueError:
732
pass
733
734
log = browser_loge if is_stderr else browser_logi
735
self.server.handle_incoming_message(seq_num, log, data)
736
737
self.send_response(200)
738
self.send_header('Content-type', 'text/plain')
739
self.send_header('Cache-Control', 'no-cache, must-revalidate')
740
self.send_header('Connection', 'close')
741
self.send_header('Expires', '-1')
742
self.end_headers()
743
self.wfile.write(b'OK')
744
745
746
# Returns stdout by running command with universal_newlines=True
747
def check_output(cmd, universal_newlines=True, *args, **kwargs):
748
if hasattr(subprocess, "run"):
749
return subprocess.run(cmd, universal_newlines=universal_newlines, stdout=subprocess.PIPE, check=True, *args, **kwargs).stdout
750
else:
751
# check_output is considered as an old API so prefer subprocess.run if possible
752
return subprocess.check_output(cmd, universal_newlines=universal_newlines, *args, **kwargs)
753
754
755
# From http://stackoverflow.com/questions/4842448/getting-processor-information-in-python
756
# Returns a string with something like "AMD64, Intel(R) Core(TM) i5-2557M CPU @
757
# 1.70GHz, Intel64 Family 6 Model 42 Stepping 7, GenuineIntel"
758
def get_cpu_info():
759
physical_cores = 1
760
logical_cores = 1
761
frequency = 0
762
try:
763
if WINDOWS:
764
from win32com.client import GetObject
765
root_winmgmts = GetObject('winmgmts:root\\cimv2')
766
cpus = root_winmgmts.ExecQuery('Select * from Win32_Processor')
767
cpu_name = cpus[0].Name + ', ' + platform.processor()
768
physical_cores = int(check_output(['wmic', 'cpu', 'get', 'NumberOfCores']).split('\n')[1].strip())
769
logical_cores = int(check_output(['wmic', 'cpu', 'get', 'NumberOfLogicalProcessors']).split('\n')[1].strip())
770
frequency = int(check_output(['wmic', 'cpu', 'get', 'MaxClockSpeed']).split('\n')[1].strip())
771
elif MACOS:
772
cpu_name = check_output(['sysctl', '-n', 'machdep.cpu.brand_string']).strip()
773
physical_cores = int(check_output(['sysctl', '-n', 'machdep.cpu.core_count']).strip())
774
logical_cores = int(check_output(['sysctl', '-n', 'machdep.cpu.thread_count']).strip())
775
frequency = int(check_output(['sysctl', '-n', 'hw.cpufrequency']).strip()) // 1000000
776
elif LINUX:
777
for line in open('/proc/cpuinfo').readlines():
778
if 'model name' in line:
779
cpu_name = re.sub('.*model name.*:', '', line, count=1).strip()
780
lscpu = check_output(['lscpu'])
781
frequency = math.ceil(float(re.search('CPU (max )?MHz: (.*)', lscpu).group(2).strip()))
782
sockets = int(re.search(r'Socket\(s\): (.*)', lscpu).group(1).strip())
783
physical_cores = sockets * int(re.search(r'Core\(s\) per socket: (.*)', lscpu).group(1).strip())
784
logical_cores = physical_cores * int(re.search(r'Thread\(s\) per core: (.*)', lscpu).group(1).strip())
785
except Exception as e:
786
import traceback
787
loge(traceback.format_exc())
788
return {'model': 'Unknown ("' + str(e) + '")',
789
'physicalCores': 1,
790
'logicalCores': 1,
791
'frequency': 0,
792
}
793
794
return {'model': platform.machine() + ', ' + cpu_name,
795
'physicalCores': physical_cores,
796
'logicalCores': logical_cores,
797
'frequency': frequency,
798
}
799
800
801
def get_android_cpu_infoline():
802
lines = check_output([ADB, 'shell', 'cat', '/proc/cpuinfo']).split('\n')
803
processor = ''
804
hardware = ''
805
for line in lines:
806
if line.startswith('Processor'):
807
processor = line[line.find(':') + 1:].strip()
808
elif line.startswith('Hardware'):
809
hardware = line[line.find(':') + 1:].strip()
810
811
freq = int(check_output([ADB, 'shell', 'cat', '/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq']).strip()) // 1000
812
return 'CPU: ' + processor + ', ' + hardware + ' @ ' + str(freq) + ' MHz'
813
814
815
def win_get_gpu_info():
816
gpus = []
817
818
def find_gpu_model(model):
819
for gpu in gpus:
820
if gpu['model'] == model:
821
return gpu
822
return None
823
824
for i in range(16):
825
try:
826
hHardwareReg = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, 'HARDWARE')
827
hDeviceMapReg = winreg.OpenKey(hHardwareReg, 'DEVICEMAP')
828
hVideoReg = winreg.OpenKey(hDeviceMapReg, 'VIDEO')
829
VideoCardString = winreg.QueryValueEx(hVideoReg, '\\Device\\Video' + str(i))[0]
830
# Get Rid of Registry/Machine from the string
831
VideoCardStringSplit = VideoCardString.split('\\')
832
ClearnVideoCardString = "\\".join(VideoCardStringSplit[3:])
833
# Go up one level for detailed
834
# VideoCardStringRoot = "\\".join(VideoCardStringSplit[3:len(VideoCardStringSplit)-1])
835
836
# Get the graphics card information
837
hVideoCardReg = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, ClearnVideoCardString)
838
try:
839
VideoCardDescription = winreg.QueryValueEx(hVideoCardReg, 'Device Description')[0]
840
except WindowsError:
841
VideoCardDescription = winreg.QueryValueEx(hVideoCardReg, 'DriverDesc')[0]
842
843
try:
844
driverVersion = winreg.QueryValueEx(hVideoCardReg, 'DriverVersion')[0]
845
VideoCardDescription += ', driver version ' + driverVersion
846
except WindowsError:
847
pass
848
849
try:
850
driverDate = winreg.QueryValueEx(hVideoCardReg, 'DriverDate')[0]
851
VideoCardDescription += ' (' + driverDate + ')'
852
except WindowsError:
853
pass
854
855
VideoCardMemorySize = winreg.QueryValueEx(hVideoCardReg, 'HardwareInformation.MemorySize')[0]
856
try:
857
vram = struct.unpack('l', bytes(VideoCardMemorySize))[0]
858
except struct.error:
859
vram = int(VideoCardMemorySize)
860
if not find_gpu_model(VideoCardDescription):
861
gpus += [{'model': VideoCardDescription, 'ram': vram}]
862
except WindowsError:
863
pass
864
return gpus
865
866
867
def linux_get_gpu_info():
868
glinfo = ''
869
try:
870
glxinfo = check_output('glxinfo')
871
for line in glxinfo.split("\n"):
872
if "OpenGL vendor string:" in line:
873
gl_vendor = line[len("OpenGL vendor string:"):].strip()
874
if "OpenGL version string:" in line:
875
gl_version = line[len("OpenGL version string:"):].strip()
876
if "OpenGL renderer string:" in line:
877
gl_renderer = line[len("OpenGL renderer string:"):].strip()
878
glinfo = gl_vendor + ' ' + gl_renderer + ', GL version ' + gl_version
879
except Exception as e:
880
logv(e)
881
882
adapterinfo = ''
883
try:
884
vgainfo = check_output(['lshw', '-C', 'display'], stderr=subprocess.PIPE)
885
vendor = re.search("vendor: (.*)", vgainfo).group(1).strip()
886
product = re.search("product: (.*)", vgainfo).group(1).strip()
887
description = re.search("description: (.*)", vgainfo).group(1).strip()
888
clock = re.search("clock: (.*)", vgainfo).group(1).strip()
889
adapterinfo = vendor + ' ' + product + ', ' + description + ' (' + clock + ')'
890
except Exception as e:
891
logv(e)
892
893
ram = 0
894
try:
895
vgainfo = check_output('lspci -v -s $(lspci | grep VGA | cut -d " " -f 1)', shell=True, stderr=subprocess.PIPE)
896
ram = int(re.search(r"\[size=([0-9]*)M\]", vgainfo).group(1)) * 1024 * 1024
897
except Exception as e:
898
logv(e)
899
900
model = (adapterinfo + ' ' + glinfo).strip()
901
if not model:
902
model = 'Unknown'
903
return [{'model': model, 'ram': ram}]
904
905
906
def macos_get_gpu_info():
907
gpus = []
908
try:
909
info = check_output(['system_profiler', 'SPDisplaysDataType'])
910
info = info.split("Chipset Model:")[1:]
911
for gpu in info:
912
model_name = gpu.split('\n')[0].strip()
913
bus = re.search("Bus: (.*)", gpu).group(1).strip()
914
memory = int(re.search("VRAM (.*?): (.*) MB", gpu).group(2).strip())
915
gpus += [{'model': model_name + ' (' + bus + ')', 'ram': memory * 1024 * 1024}]
916
return gpus
917
except Exception:
918
pass
919
920
921
def get_gpu_info():
922
if WINDOWS:
923
return win_get_gpu_info()
924
elif LINUX:
925
return linux_get_gpu_info()
926
elif MACOS:
927
return macos_get_gpu_info()
928
else:
929
return []
930
931
932
def get_executable_version(filename):
933
try:
934
if WINDOWS:
935
import win32api
936
info = win32api.GetFileVersionInfo(filename, "\\")
937
ms = info['FileVersionMS']
938
ls = info['FileVersionLS']
939
version = win32api.HIWORD(ms), win32api.LOWORD(ms), win32api.HIWORD(ls), win32api.LOWORD(ls)
940
return '.'.join(map(str, version))
941
elif MACOS:
942
plistfile = filename[0:filename.find('MacOS')] + 'Info.plist'
943
info = plistlib.readPlist(plistfile)
944
# Data in Info.plists is a bit odd, this check combo gives best information on each browser.
945
if 'firefox' in filename.lower():
946
return info['CFBundleShortVersionString']
947
if 'opera' in filename.lower():
948
return info['CFBundleVersion']
949
else:
950
return info['CFBundleShortVersionString']
951
elif LINUX:
952
if 'firefox' in filename.lower():
953
version = check_output([filename, '-v'])
954
version = version.replace('Mozilla Firefox ', '')
955
return version.strip()
956
else:
957
return ""
958
except Exception as e:
959
logv(e)
960
return ""
961
962
963
def get_browser_build_date(filename):
964
try:
965
if MACOS:
966
plistfile = filename[0:filename.find('MacOS')] + 'Info.plist'
967
info = plistlib.readPlist(plistfile)
968
# Data in Info.plists is a bit odd, this check combo gives best information on each browser.
969
if 'firefox' in filename.lower():
970
return '20' + '-'.join(x.zfill(2) for x in info['CFBundleVersion'][2:].split('.'))
971
except Exception as e:
972
logv(e)
973
974
# No exact information about the build date, so take the last modified date of the file.
975
# This is not right, but assuming that one installed the browser shortly after the update was
976
# available, it's shooting close.
977
try:
978
return time.strftime("%Y-%m-%d %H:%M:%S", time.gmtime(os.path.getmtime(filename)))
979
except Exception as e:
980
logv(e)
981
return '(unknown)'
982
983
984
def get_browser_info(filename, format_json):
985
if format_json:
986
return json.dumps({
987
'name': browser_display_name(filename),
988
'version': get_executable_version(filename),
989
'buildDate': get_browser_build_date(filename),
990
}, indent=2)
991
else:
992
return 'Browser: ' + browser_display_name(filename) + ' ' + get_executable_version(filename) + ', build ' + get_browser_build_date(filename)
993
994
995
# http://stackoverflow.com/questions/580924/python-windows-file-version-attribute
996
def win_get_file_properties(fname):
997
propNames = ('Comments', 'InternalName', 'ProductName',
998
'CompanyName', 'LegalCopyright', 'ProductVersion',
999
'FileDescription', 'LegalTrademarks', 'PrivateBuild',
1000
'FileVersion', 'OriginalFilename', 'SpecialBuild')
1001
1002
props = {'FixedFileInfo': None, 'StringFileInfo': None, 'FileVersion': None}
1003
1004
import win32api
1005
# backslash as parm returns dictionary of numeric info corresponding to VS_FIXEDFILEINFO struct
1006
fixedInfo = win32api.GetFileVersionInfo(fname, '\\')
1007
props['FixedFileInfo'] = fixedInfo
1008
props['FileVersion'] = "%d.%d.%d.%d" % (fixedInfo['FileVersionMS'] / 65536,
1009
fixedInfo['FileVersionMS'] % 65536,
1010
fixedInfo['FileVersionLS'] / 65536,
1011
fixedInfo['FileVersionLS'] % 65536)
1012
1013
# \VarFileInfo\Translation returns list of available (language, codepage)
1014
# pairs that can be used to retrieve string info. We are using only the first pair.
1015
lang, codepage = win32api.GetFileVersionInfo(fname, '\\VarFileInfo\\Translation')[0]
1016
1017
# any other must be of the form \StringfileInfo\%04X%04X\parm_name, middle
1018
# two are language/codepage pair returned from above
1019
1020
strInfo = {}
1021
for propName in propNames:
1022
strInfoPath = u'\\StringFileInfo\\%04X%04X\\%s' % (lang, codepage, propName)
1023
## print str_info
1024
strInfo[propName] = win32api.GetFileVersionInfo(fname, strInfoPath)
1025
1026
props['StringFileInfo'] = strInfo
1027
1028
return props
1029
1030
1031
def get_computer_model():
1032
try:
1033
if MACOS:
1034
try:
1035
with open(os.path.join(os.getenv("HOME"), '.emrun.hwmodel.cached'), 'r') as f:
1036
model = f.read()
1037
return model
1038
except IOError:
1039
pass
1040
1041
try:
1042
# http://apple.stackexchange.com/questions/98080/can-a-macs-model-year-be-determined-via-terminal-command
1043
serial = check_output(['system_profiler', 'SPHardwareDataType'])
1044
serial = re.search("Serial Number (.*): (.*)", serial)
1045
serial = serial.group(2).strip()[-4:]
1046
cmd = ['curl', '-s', 'http://support-sp.apple.com/sp/product?cc=' + serial]
1047
logv(str(cmd))
1048
model = check_output(cmd)
1049
model = re.search('<configCode>(.*)</configCode>', model)
1050
model = model.group(1).strip()
1051
with open(os.path.join(os.getenv("HOME"), '.emrun.hwmodel.cached'), 'w') as fh:
1052
fh.write(model) # Cache the hardware model to disk
1053
return model
1054
except Exception:
1055
hwmodel = check_output(['sysctl', 'hw.model'])
1056
hwmodel = re.search('hw.model: (.*)', hwmodel).group(1).strip()
1057
return hwmodel
1058
elif WINDOWS:
1059
manufacturer = check_output(['wmic', 'baseboard', 'get', 'manufacturer']).split('\n')[1].strip()
1060
version = check_output(['wmic', 'baseboard', 'get', 'version']).split('\n')[1].strip()
1061
product = check_output(['wmic', 'baseboard', 'get', 'product']).split('\n')[1].strip()
1062
if 'Apple' in manufacturer:
1063
return manufacturer + ' ' + version + ', ' + product
1064
else:
1065
return manufacturer + ' ' + product + ', ' + version
1066
elif LINUX:
1067
board_vendor = check_output(['cat', '/sys/devices/virtual/dmi/id/board_vendor']).strip()
1068
board_name = check_output(['cat', '/sys/devices/virtual/dmi/id/board_name']).strip()
1069
board_version = check_output(['cat', '/sys/devices/virtual/dmi/id/board_version']).strip()
1070
1071
bios_vendor = check_output(['cat', '/sys/devices/virtual/dmi/id/bios_vendor']).strip()
1072
bios_version = check_output(['cat', '/sys/devices/virtual/dmi/id/bios_version']).strip()
1073
bios_date = check_output(['cat', '/sys/devices/virtual/dmi/id/bios_date']).strip()
1074
return board_vendor + ' ' + board_name + ' ' + board_version + ', ' + bios_vendor + ' ' + bios_version + ' (' + bios_date + ')'
1075
except Exception as e:
1076
logv(str(e))
1077
return 'Generic'
1078
1079
1080
def get_os_version():
1081
bitness = ' (64bit)' if platform.machine() in ['AMD64', 'x86_64'] else ' (32bit)'
1082
try:
1083
if WINDOWS:
1084
versionHandle = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion")
1085
productName = winreg.QueryValueEx(versionHandle, "ProductName")
1086
1087
version = ''
1088
try:
1089
version = ' ' + check_output(['wmic', 'os', 'get', 'version']).split('\n')[1].strip()
1090
except Exception:
1091
pass
1092
return productName[0] + version + bitness
1093
elif MACOS:
1094
return 'macOS ' + platform.mac_ver()[0] + bitness
1095
elif LINUX:
1096
kernel_version = check_output(['uname', '-r']).strip()
1097
return ' '.join(platform.linux_distribution()) + ', linux kernel ' + kernel_version + ' ' + platform.architecture()[0] + bitness
1098
except Exception:
1099
return 'Unknown OS'
1100
1101
1102
def get_system_memory():
1103
try:
1104
if LINUX or emrun_options.android:
1105
if emrun_options.android:
1106
lines = check_output([ADB, 'shell', 'cat', '/proc/meminfo']).split('\n')
1107
else:
1108
mem = open('/proc/meminfo', 'r')
1109
lines = mem.readlines()
1110
mem.close()
1111
for i in lines:
1112
sline = i.split()
1113
if str(sline[0]) == 'MemTotal:':
1114
return int(sline[1]) * 1024
1115
elif WINDOWS:
1116
import win32api
1117
return win32api.GlobalMemoryStatusEx()['TotalPhys']
1118
elif MACOS:
1119
return int(check_output(['sysctl', '-n', 'hw.memsize']).strip())
1120
except Exception:
1121
return -1
1122
1123
1124
# Finds the given executable 'program' in PATH. Operates like the Unix tool 'which'.
1125
def which(program):
1126
def is_exe(fpath):
1127
return os.path.isfile(fpath) and os.access(fpath, os.X_OK)
1128
1129
fpath, fname = os.path.split(program)
1130
if fpath:
1131
if is_exe(program):
1132
return program
1133
else:
1134
for path in os.environ["PATH"].split(os.pathsep):
1135
path = path.strip('"')
1136
exe_file = os.path.join(path, program)
1137
if is_exe(exe_file):
1138
return exe_file
1139
1140
if WINDOWS and '.' not in fname:
1141
if is_exe(exe_file + '.exe'):
1142
return exe_file + '.exe'
1143
if is_exe(exe_file + '.cmd'):
1144
return exe_file + '.cmd'
1145
if is_exe(exe_file + '.bat'):
1146
return exe_file + '.bat'
1147
1148
return None
1149
1150
1151
def win_get_default_browser():
1152
# Look in the registry for the default system browser on Windows without relying on
1153
# 'start %1' since that method has an issue, see comment below.
1154
try:
1155
with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Classes\http\shell\open\command") as key:
1156
cmd = winreg.QueryValue(key, None)
1157
if cmd:
1158
parts = shlex.split(cmd)
1159
if len(parts):
1160
return [parts[0]]
1161
except WindowsError:
1162
logv("Unable to find default browser key in Windows registry. Trying fallback.")
1163
1164
# Fall back to 'start "" %1', which we have to treat as if user passed --serve-forever, since
1165
# for some reason, we are not able to detect when the browser closes when this is passed.
1166
#
1167
# If the first argument to 'start' is quoted, then 'start' will create a new cmd.exe window with
1168
# that quoted string as the title. If the URL contained spaces, it would be quoted by subprocess,
1169
# and if we did 'start %1', it would create a new cmd.exe window with the URL as title instead of
1170
# actually launching the browser. Therefore, we must pass a dummy quoted first argument for start
1171
# to interpret as the title. For this purpose, we use the empty string, which will be quoted
1172
# as "". See #9253 for details.
1173
return ['cmd', '/C', 'start', '']
1174
1175
1176
def find_browser(name):
1177
if WINDOWS and name == 'start':
1178
return win_get_default_browser()
1179
if MACOS and name == 'open':
1180
return [name]
1181
1182
if os.path.isfile(os.path.abspath(name)):
1183
return [name]
1184
if os.path.isfile(os.path.abspath(name) + '.exe'):
1185
return [os.path.abspath(name) + '.exe']
1186
if os.path.isfile(os.path.abspath(name) + '.cmd'):
1187
return [os.path.abspath(name) + '.cmd']
1188
if os.path.isfile(os.path.abspath(name) + '.bat'):
1189
return [os.path.abspath(name) + '.bat']
1190
1191
path_lookup = which(name)
1192
if path_lookup is not None:
1193
return [path_lookup]
1194
1195
browser_locations = []
1196
if MACOS:
1197
# Note: by default Firefox beta installs as 'Firefox.app', you must manually rename it to
1198
# FirefoxBeta.app after installation.
1199
browser_locations = [('firefox', '/Applications/Firefox.app/Contents/MacOS/firefox'),
1200
('firefox_beta', '/Applications/FirefoxBeta.app/Contents/MacOS/firefox'),
1201
('firefox_aurora', '/Applications/FirefoxAurora.app/Contents/MacOS/firefox'),
1202
('firefox_nightly', '/Applications/FirefoxNightly.app/Contents/MacOS/firefox'),
1203
('safari', '/Applications/Safari.app/Contents/MacOS/Safari'),
1204
('opera', '/Applications/Opera.app/Contents/MacOS/Opera'),
1205
('chrome', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'),
1206
('chrome_canary', '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary')]
1207
elif WINDOWS:
1208
pf_locations = ['ProgramFiles(x86)', 'ProgramFiles', 'ProgramW6432', 'LOCALAPPDATA']
1209
1210
for pf_env in pf_locations:
1211
if pf_env not in os.environ:
1212
continue
1213
program_files = os.environ[pf_env] if WINDOWS else ''
1214
1215
browser_locations += [('chrome', os.path.join(program_files, 'Google/Chrome/Application/chrome.exe')),
1216
('chrome_canary', os.path.expanduser("~/AppData/Local/Google/Chrome SxS/Application/chrome.exe")),
1217
('firefox_nightly', os.path.join(program_files, 'Nightly/firefox.exe')),
1218
('firefox_aurora', os.path.join(program_files, 'Aurora/firefox.exe')),
1219
('firefox_beta', os.path.join(program_files, 'Beta/firefox.exe')),
1220
('firefox_beta', os.path.join(program_files, 'FirefoxBeta/firefox.exe')),
1221
('firefox_beta', os.path.join(program_files, 'Firefox Beta/firefox.exe')),
1222
('firefox', os.path.join(program_files, 'Mozilla Firefox/firefox.exe')),
1223
('iexplore', os.path.join(program_files, 'Internet Explorer/iexplore.exe')),
1224
('opera', os.path.join(program_files, 'Opera/launcher.exe'))]
1225
1226
elif LINUX:
1227
browser_locations = [('firefox', os.path.expanduser('~/firefox/firefox')),
1228
('firefox_beta', os.path.expanduser('~/firefox_beta/firefox')),
1229
('firefox_aurora', os.path.expanduser('~/firefox_aurora/firefox')),
1230
('firefox_nightly', os.path.expanduser('~/firefox_nightly/firefox')),
1231
('chrome', which('google-chrome-stable')),
1232
('chrome', which('google-chrome'))]
1233
1234
for alias, browser_exe in browser_locations:
1235
if name == alias:
1236
if browser_exe is not None and os.path.isfile(browser_exe):
1237
return [browser_exe]
1238
1239
return None # Could not find the browser
1240
1241
1242
def get_android_model():
1243
manufacturer = check_output([ADB, 'shell', 'getprop', 'ro.product.manufacturer']).strip()
1244
brand = check_output([ADB, 'shell', 'getprop', 'ro.product.brand']).strip()
1245
model = check_output([ADB, 'shell', 'getprop', 'ro.product.model']).strip()
1246
board = check_output([ADB, 'shell', 'getprop', 'ro.product.board']).strip()
1247
device = check_output([ADB, 'shell', 'getprop', 'ro.product.device']).strip()
1248
name = check_output([ADB, 'shell', 'getprop', 'ro.product.name']).strip()
1249
return manufacturer + ' ' + brand + ' ' + model + ' ' + board + ' ' + device + ' ' + name
1250
1251
1252
def get_android_os_version():
1253
ver = check_output([ADB, 'shell', 'getprop', 'ro.build.version.release']).strip()
1254
apiLevel = check_output([ADB, 'shell', 'getprop', 'ro.build.version.sdk']).strip()
1255
if not apiLevel:
1256
apiLevel = check_output([ADB, 'shell', 'getprop', 'ro.build.version.sdk_int']).strip()
1257
1258
os = ''
1259
if ver:
1260
os += 'Android ' + ver + ' '
1261
if apiLevel:
1262
os += 'SDK API Level ' + apiLevel + ' '
1263
os += check_output([ADB, 'shell', 'getprop', 'ro.build.description']).strip()
1264
return os
1265
1266
1267
def list_android_browsers():
1268
apps = check_output([ADB, 'shell', 'pm', 'list', 'packages', '-f']).replace('\r\n', '\n')
1269
browsers = []
1270
for line in apps.split('\n'):
1271
line = line.strip()
1272
if line.endswith('=org.mozilla.firefox'):
1273
browsers += ['firefox']
1274
if line.endswith('=org.mozilla.firefox_beta'):
1275
browsers += ['firefox_beta']
1276
if line.endswith('=org.mozilla.fennec_aurora'):
1277
browsers += ['firefox_aurora']
1278
if line.endswith('=org.mozilla.fennec'):
1279
browsers += ['firefox_nightly']
1280
if line.endswith('=com.android.chrome'):
1281
browsers += ['chrome']
1282
if line.endswith('=com.chrome.beta'):
1283
browsers += ['chrome_beta']
1284
if line.endswith('=com.chrome.dev'):
1285
browsers += ['chrome_dev']
1286
if line.endswith('=com.chrome.canary'):
1287
browsers += ['chrome_canary']
1288
if line.endswith('=com.opera.browser'):
1289
browsers += ['opera']
1290
if line.endswith('=com.opera.mini.android'):
1291
browsers += ['opera_mini']
1292
if line.endswith('=mobi.mgeek.TunnyBrowser'):
1293
browsers += ['dolphin']
1294
1295
browsers.sort()
1296
logi('emrun has automatically found the following browsers on the connected Android device:')
1297
for browser in browsers:
1298
logi(' - ' + browser)
1299
1300
1301
def list_pc_browsers():
1302
browsers = ['firefox', 'firefox_beta', 'firefox_aurora', 'firefox_nightly', 'chrome', 'chrome_canary', 'iexplore', 'safari', 'opera']
1303
logi('emrun has automatically found the following browsers in the default install locations on the system:')
1304
logi('')
1305
for browser in browsers:
1306
browser_exe = find_browser(browser)
1307
if type(browser_exe) is list:
1308
browser_exe = browser_exe[0]
1309
if browser_exe:
1310
logi(' - ' + browser + ': ' + browser_display_name(browser_exe) + ' ' + get_executable_version(browser_exe))
1311
logi('')
1312
logi('You can pass the --browser <id> option to launch with the given browser above.')
1313
logi('Even if your browser was not detected, you can use --browser /path/to/browser/executable to launch with that browser.')
1314
1315
1316
def browser_display_name(browser):
1317
b = browser.lower()
1318
if 'iexplore' in b:
1319
return 'Microsoft Internet Explorer'
1320
if 'chrome' in b:
1321
return 'Google Chrome'
1322
if 'firefox' in b:
1323
# Try to identify firefox flavor explicitly, to help show issues where emrun would launch the wrong browser.
1324
try:
1325
product_name = win_get_file_properties(browser)['StringFileInfo']['ProductName'] if WINDOWS else 'firefox'
1326
if product_name.lower() != 'firefox':
1327
return 'Mozilla Firefox ' + product_name
1328
except Exception:
1329
pass
1330
return 'Mozilla Firefox'
1331
if 'opera' in b:
1332
return 'Opera'
1333
if 'safari' in b:
1334
return 'Apple Safari'
1335
return browser
1336
1337
1338
def subprocess_env():
1339
e = os.environ.copy()
1340
# https://bugzilla.mozilla.org/show_bug.cgi?id=745154
1341
e['MOZ_DISABLE_AUTO_SAFE_MODE'] = '1'
1342
e['MOZ_DISABLE_SAFE_MODE_KEY'] = '1' # https://bugzilla.mozilla.org/show_bug.cgi?id=653410#c9
1343
e['JIT_OPTION_asmJSAtomicsEnable'] = 'true' # https://bugzilla.mozilla.org/show_bug.cgi?id=1299359#c0
1344
return e
1345
1346
1347
# Removes a directory tree even if it was readonly, and doesn't throw exception on failure.
1348
def remove_tree(d):
1349
os.chmod(d, stat.S_IWRITE)
1350
try:
1351
def remove_readonly_and_try_again(func, path, _exc_info):
1352
if not (os.stat(path).st_mode & stat.S_IWRITE):
1353
os.chmod(path, stat.S_IWRITE)
1354
func(path)
1355
else:
1356
raise
1357
shutil.rmtree(d, onerror=remove_readonly_and_try_again)
1358
except Exception:
1359
pass
1360
1361
1362
def get_system_info(format_json):
1363
if emrun_options.android:
1364
if format_json:
1365
return json.dumps({'model': get_android_model(),
1366
'os': get_android_os_version(),
1367
'ram': get_system_memory(),
1368
'cpu': get_android_cpu_infoline(),
1369
}, indent=2)
1370
else:
1371
info = 'Model: ' + get_android_model() + '\n'
1372
info += 'OS: ' + get_android_os_version() + ' with ' + str(get_system_memory() // 1024 // 1024) + ' MB of System RAM\n'
1373
info += 'CPU: ' + get_android_cpu_infoline() + '\n'
1374
return info.strip()
1375
else:
1376
try:
1377
with open(os.path.expanduser('~/.emrun.generated.guid')) as fh:
1378
unique_system_id = fh.read().strip()
1379
except Exception:
1380
import uuid
1381
unique_system_id = str(uuid.uuid4())
1382
try:
1383
open(os.path.expanduser('~/.emrun.generated.guid'), 'w').write(unique_system_id)
1384
except Exception as e:
1385
logv(e)
1386
1387
if format_json:
1388
return json.dumps({'name': socket.gethostname(),
1389
'model': get_computer_model(),
1390
'os': get_os_version(),
1391
'ram': get_system_memory(),
1392
'cpu': get_cpu_info(),
1393
'gpu': get_gpu_info(),
1394
'uuid': unique_system_id}, indent=2)
1395
else:
1396
cpu = get_cpu_info()
1397
gpus = get_gpu_info()
1398
# http://stackoverflow.com/questions/799767/getting-name-of-windows-computer-running-python-script
1399
info = 'Computer name: ' + socket.gethostname() + '\n'
1400
info += 'Model: ' + get_computer_model() + '\n'
1401
info += 'OS: ' + get_os_version() + ' with ' + str(get_system_memory() // 1024 // 1024) + ' MB of System RAM\n'
1402
info += 'CPU: ' + cpu['model'] + ', ' + str(cpu['frequency']) + ' MHz, ' + str(cpu['physicalCores']) + ' physical cores, ' + str(cpu['logicalCores']) + ' logical cores\n'
1403
if len(gpus) == 1:
1404
info += 'GPU: ' + gpus[0]['model'] + ' with ' + str(gpus[0]['ram'] // 1024 // 1024) + " MB of VRAM\n"
1405
elif len(gpus) > 1:
1406
for i in range(len(gpus)):
1407
info += 'GPU' + str(i) + ": " + gpus[i]['model'] + ' with ' + str(gpus[i]['ram'] // 1024 // 1024) + ' MBs of VRAM\n'
1408
info += 'UUID: ' + unique_system_id
1409
return info.strip()
1410
1411
1412
# Be resilient to quotes and whitespace
1413
def unwrap(s):
1414
s = s.strip()
1415
if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")):
1416
s = s[1:-1].strip()
1417
return s
1418
1419
1420
def list_processes_by_name(exe_full_path):
1421
pids = []
1422
try:
1423
import psutil
1424
for proc in psutil.process_iter():
1425
try:
1426
pinfo = proc.as_dict(attrs=['pid', 'name', 'exe'])
1427
if pinfo['exe'].lower().replace('\\', '/') == exe_full_path.lower().replace('\\', '/'):
1428
pids.append(pinfo)
1429
except Exception:
1430
# Fail gracefully if unable to iterate over a specific process
1431
pass
1432
except Exception:
1433
# Fail gracefully if psutil not available
1434
logv('import psutil failed, unable to detect browser processes')
1435
1436
logv('Searching for processes by full path name "' + exe_full_path + '".. found ' + str(len(pids)) + ' entries')
1437
1438
return pids
1439
1440
1441
usage_str = """\
1442
emrun [emrun_options] filename.html -- [html_cmdline_options]
1443
1444
where emrun_options specifies command line options for emrun itself, whereas
1445
html_cmdline_options specifies startup arguments to the program.
1446
1447
If you are seeing "unrecognized arguments" when trying to pass
1448
arguments to your page, remember to add `--` between arguments
1449
to emrun itself and arguments to your page.
1450
"""
1451
1452
1453
def parse_args(args):
1454
parser = argparse.ArgumentParser(usage=usage_str)
1455
1456
parser.add_argument('--kill-start', action='store_true',
1457
help='If true, any previously running instances of '
1458
'the target browser are killed before starting.')
1459
1460
parser.add_argument('--kill-exit', action='store_true',
1461
help='If true, the spawned browser process is forcibly '
1462
'killed when it calls exit(). Note: Using this '
1463
'option may require explicitly passing the option '
1464
'--browser=/path/to/browser, to avoid emrun being '
1465
'detached from the browser process it spawns.')
1466
1467
parser.add_argument('--no-server', dest='run_server', action='store_false',
1468
default=True,
1469
help='If specified, a HTTP web server is not launched '
1470
'to host the page to run.')
1471
1472
parser.add_argument('--no-browser', dest='run_browser', action='store_false',
1473
default=True,
1474
help='If specified, emrun will not launch a web browser '
1475
'to run the page.')
1476
1477
parser.add_argument('--no-emrun-detect', action='store_true',
1478
help='If specified, skips printing the warning message '
1479
'if html page is detected to not have been built '
1480
'with --emrun linker flag.')
1481
1482
parser.add_argument('--serve-after-close', action='store_true',
1483
help='If true, serves the web page even after the '
1484
'application quits by user closing the web page.')
1485
1486
parser.add_argument('--serve-after-exit', action='store_true',
1487
help='If true, serves the web page even after the '
1488
'application quits by a call to exit().')
1489
1490
parser.add_argument('--serve-root',
1491
help='If set, specifies the root path that the emrun '
1492
'web server serves. If not specified, the directory '
1493
'where the target .html page lives in is served.')
1494
1495
parser.add_argument('--verbose', action='store_true',
1496
help='Enable verbose logging from emrun internal operation.')
1497
1498
parser.add_argument('--hostname', default=default_webserver_hostname,
1499
help='Specifies the hostname the server runs in.')
1500
1501
parser.add_argument('--port', default=default_webserver_port, type=int,
1502
help='Specifies the port the server runs in.')
1503
1504
parser.add_argument('--log-stdout',
1505
help='Specifies a log filename where the browser process '
1506
'stdout data will be appended to.')
1507
1508
parser.add_argument('--log-stderr',
1509
help='Specifies a log filename where the browser process stderr data will be appended to.')
1510
1511
parser.add_argument('--silence-timeout', type=int, default=0,
1512
help='If no activity is received in this many seconds, '
1513
'the browser process is assumed to be hung, and the web '
1514
'server is shut down and the target browser killed. '
1515
'Disabled by default.')
1516
1517
parser.add_argument('--timeout', type=int, default=0,
1518
help='If the browser process does not quit or the page '
1519
'exit() in this many seconds, the browser is assumed '
1520
'to be hung, and the web server is shut down and the '
1521
'target browser killed. Disabled by default.')
1522
1523
parser.add_argument('--timeout-returncode', type=int, default=99999,
1524
help='Sets the exit code that emrun reports back to '
1525
'caller in the case that a page timeout occurs. '
1526
'Default: 99999.')
1527
1528
parser.add_argument('--list-browsers', action='store_true',
1529
help='Prints out all detected browser that emrun is able '
1530
'to use with the --browser command and exits.')
1531
1532
parser.add_argument('--browser',
1533
help='Specifies the browser executable to run the web page in.')
1534
1535
parser.add_argument('--browser-args', default='',
1536
help='Specifies the arguments to the browser executable.')
1537
1538
parser.add_argument('--android', action='store_true',
1539
help='Launches the page in a browser of an Android '
1540
'device connected to an USB on the local system. (via adb)')
1541
1542
parser.add_argument('--android-tunnel', action='store_true',
1543
help='Expose the port directly to the Android device '
1544
'and connect to it as localhost, establishing '
1545
'cross origin isolation. Implies --android. A '
1546
'reverse socket connection is created by adb '
1547
'reverse, and remains after emrun terminates (it '
1548
'can be removed by adb reverse --remove).')
1549
1550
parser.add_argument('--system-info', action='store_true',
1551
help='Prints information about the current system at startup.')
1552
1553
parser.add_argument('--browser-info', action='store_true',
1554
help='Prints information about the target browser to launch at startup.')
1555
1556
parser.add_argument('--json', action='store_true',
1557
help='If specified, --system-info and --browser-info are '
1558
'outputted in JSON format.')
1559
1560
parser.add_argument('--safe-firefox-profile', action='store_true',
1561
help='If true, the browser is launched into a new clean '
1562
'Firefox profile that is suitable for unattended '
1563
'automated runs. (If target browser != Firefox, '
1564
'this parameter is ignored)')
1565
1566
parser.add_argument('--private-browsing', action='store_true',
1567
help='If specified, opens browser in private/incognito mode.')
1568
1569
parser.add_argument('--dump-out-directory', default='dump_out', type=str,
1570
help='If specified, overrides the directory for dump files using emrun_file_dump method.')
1571
1572
parser.add_argument('serve', nargs='?', default='')
1573
1574
parser.add_argument('cmdlineparams', nargs='*')
1575
1576
# Support legacy argument names with `_` in them (but don't
1577
# advertize these in the --help message).
1578
for i, a in enumerate(args):
1579
if a == '--':
1580
break
1581
if a.startswith('--') and '_' in a:
1582
# Only replace '_' in that argument name, not that its value
1583
parts = a.split('=')
1584
parts[0] = parts[0].replace('_', '-')
1585
args[i] = '='.join(parts)
1586
1587
return parser.parse_args(args)
1588
1589
1590
def run(args): # noqa: C901, PLR0912, PLR0915
1591
"""Future modifications should consider refactoring to reduce complexity.
1592
1593
* The McCabe cyclomatiic complexity is currently 74 vs 10 recommended.
1594
* There are currently 86 branches vs 12 recommended.
1595
* There are currently 202 statements vs 50 recommended.
1596
1597
To revalidate these numbers, run `ruff check --select=C901,PLR091`.
1598
"""
1599
global browser_process, browser_exe, processname_killed_atexit, emrun_options, emrun_not_enabled_nag_printed
1600
1601
options = emrun_options = parse_args(args)
1602
1603
if options.android_tunnel:
1604
options.android = True
1605
1606
if options.android:
1607
global ADB
1608
ADB = which('adb')
1609
if not ADB:
1610
loge("Could not find the adb tool. Install Android SDK and add the directory of adb to PATH.")
1611
return 1
1612
1613
if not options.browser and not options.android:
1614
if WINDOWS:
1615
options.browser = 'start'
1616
elif LINUX:
1617
options.browser = which('xdg-open')
1618
if not options.browser:
1619
options.browser = 'firefox'
1620
elif MACOS:
1621
options.browser = 'open'
1622
1623
if options.list_browsers:
1624
if options.android:
1625
list_android_browsers()
1626
else:
1627
list_pc_browsers()
1628
return
1629
1630
if not options.serve and (options.system_info or options.browser_info):
1631
# Don't run if only --system-info or --browser-info was passed.
1632
options.run_server = options.run_browser = False
1633
1634
if not options.serve and (options.run_server or options.run_browser):
1635
logi(usage_str)
1636
logi('')
1637
logi('Type emrun --help for a detailed list of available options.')
1638
return
1639
1640
if options.serve:
1641
file_to_serve = options.serve
1642
else:
1643
file_to_serve = '.'
1644
file_to_serve_is_url = file_to_serve.startswith(('file://', 'http://', 'https://'))
1645
1646
if options.serve_root:
1647
serve_dir = os.path.abspath(options.serve_root)
1648
else:
1649
if file_to_serve == '.' or file_to_serve_is_url:
1650
serve_dir = os.path.abspath('.')
1651
else:
1652
if file_to_serve.endswith(('/', '\\')) or os.path.isdir(file_to_serve):
1653
serve_dir = file_to_serve
1654
else:
1655
serve_dir = os.path.dirname(os.path.abspath(file_to_serve))
1656
if file_to_serve_is_url:
1657
url = file_to_serve
1658
else:
1659
url = os.path.relpath(os.path.abspath(file_to_serve), serve_dir)
1660
1661
os.chdir(serve_dir)
1662
if options.run_server:
1663
if options.run_browser:
1664
logv('Web server root directory: ' + os.path.abspath('.'))
1665
else:
1666
logi('Web server root directory: ' + os.path.abspath('.'))
1667
logv('Starting web server: http://%s:%i/' % (options.hostname, options.port))
1668
httpd = HTTPWebServer((options.hostname, options.port), HTTPHandler)
1669
# to support binding to port zero we must allow the server to open to socket then retrieve the final port number
1670
options.port = httpd.socket.getsockname()[1]
1671
1672
if not file_to_serve_is_url:
1673
if len(options.cmdlineparams):
1674
url += '?' + '&'.join(options.cmdlineparams)
1675
if options.android_tunnel:
1676
hostname = 'localhost'
1677
elif options.android:
1678
hostname = socket.gethostbyname(socket.gethostname())
1679
else:
1680
hostname = options.hostname
1681
# create url for browser after opening the server so we have the final port number in case we are binding to port 0
1682
url = 'http://' + hostname + ':' + str(options.port) + '/' + url
1683
1684
if options.android:
1685
if options.run_browser or options.browser_info:
1686
if not options.browser:
1687
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.")
1688
return 1
1689
elif options.browser == 'firefox':
1690
browser_app = 'org.mozilla.firefox/org.mozilla.gecko.BrowserApp'
1691
elif options.browser in {'firefox_nightly', 'fenix'}:
1692
browser_app = 'org.mozilla.fenix/org.mozilla.gecko.BrowserApp'
1693
elif options.browser == 'chrome':
1694
browser_app = 'com.android.chrome/com.google.android.apps.chrome.Main'
1695
elif options.browser == 'chrome_beta':
1696
browser_app = 'com.chrome.beta/com.google.android.apps.chrome.Main'
1697
elif options.browser == 'chrome_dev':
1698
browser_app = 'com.chrome.dev/com.google.android.apps.chrome.Main'
1699
elif options.browser == 'chrome_canary':
1700
browser_app = 'com.chrome.canary/com.google.android.apps.chrome.Main'
1701
elif '.' in options.browser and '/' in options.browser:
1702
# Browser command line contains both '.' and '/', so it looks like a string of form 'package/activity', use that
1703
# as the browser.
1704
browser_app = options.browser
1705
else:
1706
loge("Don't know how to launch browser " + options.browser + ' on Android!')
1707
return 1
1708
# To add support for a new Android browser in the list above:
1709
# 1. Install the browser to Android phone, connect it via adb to PC.
1710
# 2. Type 'adb shell pm list packages -f' to locate the package name of that application.
1711
# 3. Type 'adb pull <packagename>.apk' to copy the apk of that application to PC.
1712
# 4. Type 'aapt d xmltree <packagename>.apk AndroidManifest.xml > manifest.txt' to extract the manifest from the package.
1713
# 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'
1714
1715
if options.android_tunnel:
1716
subprocess.check_call([ADB, 'reverse', 'tcp:' + str(options.port), 'tcp:' + str(options.port)])
1717
1718
url = url.replace('&', '\\&')
1719
browser = [ADB, 'shell', 'am', 'start', '-a', 'android.intent.action.VIEW', '-n', browser_app, '-d', url]
1720
processname_killed_atexit = browser_app[:browser_app.find('/')]
1721
else: # Launching a web page on local system.
1722
if options.browser:
1723
options.browser = unwrap(options.browser)
1724
1725
if options.run_browser or options.browser_info:
1726
browser = find_browser(str(options.browser))
1727
if not browser:
1728
loge('Unable to find browser "' + str(options.browser) + '"! Check the correctness of the passed --browser=xxx parameter!')
1729
return 1
1730
browser_exe = browser[0]
1731
browser_args = shlex.split(unwrap(options.browser_args))
1732
1733
if MACOS and ('safari' in browser_exe.lower() or browser_exe == 'open'):
1734
# Safari has a bug that a command line 'Safari http://page.com' does
1735
# not launch that page, but instead launches 'file:///http://page.com'.
1736
# To remedy this, must use the open -a command to run Safari, but
1737
# unfortunately this will end up spawning Safari process detached from
1738
# emrun.
1739
browser = ['open', '-a', 'Safari'] + (browser[1:] if len(browser) > 1 else [])
1740
browser_exe = '/Applications/Safari.app/Contents/MacOS/Safari'
1741
processname_killed_atexit = 'Safari'
1742
elif 'chrome' in browser_exe.lower():
1743
processname_killed_atexit = 'chrome'
1744
browser_args += ['--enable-nacl', '--enable-pnacl', '--disable-restore-session-state', '--enable-webgl',
1745
'--no-default-browser-check', '--no-first-run', '--allow-file-access-from-files', '--password-store=basic']
1746
if options.private_browsing:
1747
browser_args += ['--incognito']
1748
# if not options.run_server:
1749
# browser_args += ['--disable-web-security']
1750
elif 'firefox' in browser_exe.lower():
1751
processname_killed_atexit = 'firefox'
1752
elif 'iexplore' in browser_exe.lower():
1753
processname_killed_atexit = 'iexplore'
1754
if options.private_browsing:
1755
browser_args += ['-private']
1756
elif 'opera' in browser_exe.lower():
1757
processname_killed_atexit = 'opera'
1758
1759
# In Windows cmdline, & character delimits multiple commands, so must
1760
# use ^ to escape them.
1761
if browser_exe == 'cmd':
1762
url = url.replace('&', '^&')
1763
url = url.replace('0.0.0.0', 'localhost')
1764
browser += browser_args + [url]
1765
1766
if options.kill_start:
1767
pname = processname_killed_atexit
1768
kill_browser_process()
1769
processname_killed_atexit = pname
1770
1771
# Copy the profile over to Android.
1772
if options.android and options.safe_firefox_profile:
1773
profile_dir = create_emrun_safe_firefox_profile()
1774
1775
def run(cmd):
1776
logi(str(cmd))
1777
subprocess.check_call(cmd)
1778
1779
try:
1780
run(['adb', 'shell', 'rm', '-rf', '/mnt/sdcard/safe_firefox_profile'])
1781
run(['adb', 'shell', 'mkdir', '/mnt/sdcard/safe_firefox_profile'])
1782
run(['adb', 'push', os.path.join(profile_dir, 'prefs.js'), '/mnt/sdcard/safe_firefox_profile/prefs.js'])
1783
except Exception as e:
1784
loge('Creating Firefox profile prefs.js file to internal storage in /mnt/sdcard failed with error ' + str(e) + '!')
1785
loge('Try running without --safe-firefox-profile flag if unattended execution mode is not important, or')
1786
loge('enable rooted debugging on the Android device to allow adb to write files to /mnt/sdcard.')
1787
browser += ['--es', 'args', '"--profile /mnt/sdcard/safe_firefox_profile"']
1788
1789
# Create temporary Firefox profile to run the page with. This is important to
1790
# run after kill_browser_process()/kill_start op above, since that cleans up
1791
# the temporary profile if one exists.
1792
if processname_killed_atexit == 'firefox' and options.safe_firefox_profile and options.run_browser and not options.android:
1793
profile_dir = create_emrun_safe_firefox_profile()
1794
1795
browser += ['-no-remote', '--profile', profile_dir.replace('\\', '/')]
1796
1797
if options.system_info:
1798
logi('Time of run: ' + time.strftime("%x %X"))
1799
logi(get_system_info(format_json=options.json))
1800
1801
if options.browser_info:
1802
if options.android:
1803
if options.json:
1804
logi(json.dumps({'browser': 'Android ' + browser_app}, indent=2))
1805
else:
1806
logi('Browser: Android ' + browser_app)
1807
else:
1808
logi(get_browser_info(browser_exe, format_json=options.json))
1809
1810
# Suppress run warning if requested.
1811
if options.no_emrun_detect:
1812
emrun_not_enabled_nag_printed = True
1813
1814
if options.log_stdout:
1815
global browser_stdout_handle
1816
browser_stdout_handle = open(options.log_stdout, 'a')
1817
if options.log_stderr:
1818
global browser_stderr_handle
1819
if options.log_stderr == options.log_stdout:
1820
browser_stderr_handle = browser_stdout_handle
1821
else:
1822
browser_stderr_handle = open(options.log_stderr, 'a')
1823
if options.run_browser:
1824
logv("Starting browser: %s" % ' '.join(browser))
1825
# if browser[0] == 'cmd':
1826
# Workaround an issue where passing 'cmd /C start' is not able to detect
1827
# when the user closes the page.
1828
# serve_forever = True
1829
if browser_exe:
1830
global previous_browser_processes
1831
logv(browser_exe)
1832
previous_browser_processes = list_processes_by_name(browser_exe)
1833
for p in previous_browser_processes:
1834
logv('Before spawning web browser, found a running ' + os.path.basename(browser_exe) + ' browser process id: ' + str(p['pid']))
1835
browser_process = subprocess.Popen(browser, env=subprocess_env())
1836
logv('Launched browser process with pid=' + str(browser_process.pid))
1837
if options.kill_exit:
1838
atexit.register(kill_browser_process)
1839
# For Android automation, we execute adb, so this process does not
1840
# represent a browser and no point killing it.
1841
if options.android:
1842
browser_process = None
1843
1844
if browser_process:
1845
premature_quit_code = browser_process.poll()
1846
if premature_quit_code is not None:
1847
options.serve_after_close = True
1848
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.')
1849
if not options.browser:
1850
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.')
1851
1852
if options.run_server:
1853
try:
1854
httpd.serve_forever()
1855
except KeyboardInterrupt:
1856
pass
1857
httpd.server_close()
1858
1859
logv('Closed web server.')
1860
1861
if options.run_browser:
1862
if options.kill_exit:
1863
kill_browser_process()
1864
else:
1865
if is_browser_process_alive():
1866
logv('Not terminating browser process, pass --kill-exit to terminate the browser when it calls exit().')
1867
# If we have created a temporary Firefox profile, we would really really
1868
# like to wait until the browser closes, or otherwise we'll just have to
1869
# litter temp files and keep the temporary profile alive. It is possible
1870
# here that the browser is cooperatively shutting down, but has not yet
1871
# had time to do so, so wait for a short while.
1872
if temp_firefox_profile_dir is not None:
1873
time.sleep(3)
1874
1875
if not is_browser_process_alive():
1876
# Browser is no longer running, make sure to clean up the temp Firefox
1877
# profile, if we created one.
1878
delete_emrun_safe_firefox_profile()
1879
1880
return page_exit_code
1881
1882
1883
def main(args):
1884
returncode = run(args)
1885
logv('emrun quitting with process exit code ' + str(returncode))
1886
if temp_firefox_profile_dir is not None:
1887
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.')
1888
return returncode
1889
1890
1891
if __name__ == '__main__':
1892
sys.exit(main(sys.argv[1:]))
1893
1894