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