| #!/usr/bin/env python |
| # Copyright 2014 The Emscripten Authors. All rights reserved. |
| # Emscripten is available under two separate licenses, the MIT license and the |
| # University of Illinois/NCSA Open Source License. Both these licenses can be |
| # found in the LICENSE file. |
| |
| from __future__ import print_function |
| import socket, json, sys, uuid, datetime, time, logging, cgi, zipfile, os, tempfile, atexit, subprocess, re, base64, struct, imghdr |
| |
| WINDOWS = sys.platform == 'win32' |
| if WINDOWS: |
| import ctypes |
| stdout_handle = ctypes.windll.kernel32.GetStdHandle(-11) |
| |
| LOG_FORMAT = 'short' # Either 'bare', 'short', or 'long' |
| ADB = 'adb' # Path to the adb executable |
| LOG_VERBOSE = False # Verbose printing enabled with --verbose |
| HOST = 'localhost' # The remote host to connect to the B2G device |
| PORT = 6000 # The port on the host on which the B2G device listens on |
| b2g_socket = None # Python socket object for the active connection to the B2G device |
| read_queue = '' # Inbound queue of partial data read so far from the device |
| |
| webappsActorName = None |
| |
| def sizeof_fmt(num): |
| for x in ['bytes','KB','MB','GB']: |
| if num < 1024.0: |
| return "%3.1f%s" % (num, x) |
| num /= 1024.0 |
| return "%3.1f%s" % (num, 'TB') |
| |
| def zipdir(path, zipfilename): |
| try: |
| import zlib |
| zip_mode = zipfile.ZIP_DEFLATED |
| except: |
| zip_mode = zipfile.ZIP_STORED |
| |
| zipf = zipfile.ZipFile(zipfilename, 'w', zip_mode) |
| files_to_compress = [] |
| for root, dirs, files in os.walk(path): |
| for file in files: |
| files_to_compress += [(root, file)] |
| |
| n = 1 |
| for tuple in files_to_compress: |
| (root, file) = tuple |
| filename = os.path.join(root, file) |
| filesize = os.path.getsize(filename) |
| path_in_archive = os.path.relpath(filename, path) |
| print('Compressing ' + str(n) + '/' + str(len(files_to_compress)) + ': "' + path_in_archive + '" (' + sizeof_fmt(filesize) + ')...') |
| n += 1 |
| zipf.write(os.path.join(root, file), path_in_archive) |
| zipf.close() |
| print('Done. ') |
| |
| # Returns given log message formatted to be outputted on a HTML page. |
| def format_html(msg): |
| if not msg.endswith('\n'): |
| msg += '\n' |
| msg = cgi.escape(msg) |
| msg = msg.replace('\r\n', '<br />').replace('\n', '<br />') |
| return msg |
| |
| # Prints a verbose log message to stdout channel. Only shown if run with --verbose. |
| def logv(msg): |
| if LOG_VERBOSE: |
| sys.stdout.write(format_html(msg) + '\n') |
| sys.stdout.flush() |
| |
| # Reads data from the socket, and tries to parse what we have got so far as a JSON message. |
| # The messages are of form "bytelength:{jsondict}", where bytelength tells how many bytes |
| # there are in the data that comes after the colon. |
| # Returns a JSON dictionary of the received message. |
| def read_b2g_response(print_errors_to_console = True): |
| global read_queue, b2g_socket |
| payload = '' |
| while True: |
| semicolon = float('Inf') |
| payload_len = float('Inf') |
| try: |
| semicolon = read_queue.index(':') |
| payload_len = int(read_queue[:semicolon]) |
| except: |
| pass |
| if semicolon+1+payload_len > len(read_queue): |
| try: |
| read_queue += b2g_socket.recv(4096) |
| except socket.timeout as e: |
| pass # We simulate blocking sockets with looping over reads that time out, since on Windows, the user cannot press Ctrl-C to break on blocking sockets. |
| except Exception as e: |
| if e[0] == 57: # Socket is not connected |
| print('Error! Failed to receive data from the device: socket is not connected!') |
| sys.exit(1) |
| else: |
| print('Got exception ' + str(e)) |
| raise |
| continue |
| payload = read_queue[semicolon+1:semicolon+1+payload_len] |
| read_queue = read_queue[semicolon+1+payload_len:] |
| logv('Read a message of size ' + str(payload_len) + 'b from socket.') |
| payload = json.loads(payload) |
| # Log received errors immediately to console |
| if print_errors_to_console and 'error' in payload: |
| print('Received error "' + payload['error'] + '"! Reason: ' + payload['message'], file=sys.stderr) |
| else: |
| break |
| return payload |
| |
| # Sends a command to the B2G device and waits for the response and returns it as a JSON dict. |
| def send_b2g_cmd(to, cmd, data = {}, print_errors_to_console = True): |
| global b2g_socket |
| msg = { 'to': to, 'type': cmd} |
| msg = dict(list(msg.items()) + list(data.items())) |
| msg = json.dumps(msg, encoding='latin-1') |
| msg = msg.replace('\\\\', '\\') |
| msg = str(len(msg))+':'+msg |
| logv('Sending cmd:' + cmd + ' to:' + to) |
| b2g_socket.sendall(msg) |
| return read_b2g_response(print_errors_to_console) |
| |
| def escape_bytes(b): |
| return str(b) |
| |
| # Sends a data fragment of a packaged app upload. This is a special-case version of the send_b2g_cmd |
| # command optimized for performance. |
| def send_b2g_data_chunk(to, data_blob): |
| byte_str = [] |
| e = '\u0000' |
| # '"' == 34 |
| # '\' == 92 |
| i = 0 |
| while i < len(data_blob): |
| o = ord(data_blob[i]) |
| # if o == 34 or o == 92 or o >= 128 or o <= 32:#o <= 32 or o >= 36:# or o == ord('\\'): |
| if o <= 34 or o >= 128 or o == 92: |
| c = hex(o)[2:] |
| byte_str += e[:-len(c)] + c |
| else: |
| byte_str += data_blob[i] |
| i += 1 |
| message = '{"to":"'+to+'","type":"chunk","chunk":"' + ''.join(byte_str) + '"}' |
| message = str(len(message)) + ':' + message |
| logv('{"to":"'+to+'","type":"chunk","chunk":"<data>"}') |
| b2g_socket.sendall(message) |
| return read_b2g_response() |
| |
| def send_b2g_bulk_data(to, data_blob): |
| message = 'bulk ' + to + ' stream ' + str(len(data_blob)) + ':' |
| logv(message) |
| b2g_socket.sendall(message) |
| b2g_socket.sendall(data_blob) |
| # It seems that B2G doesn't send any response JSON back after a bulk transfer is finished, so no read_b2g_response() here. |
| |
| # Queries the device for a list of all installed apps. |
| def b2g_get_appslist(): |
| global webappsActorName |
| apps = send_b2g_cmd(webappsActorName, 'getAll') |
| return apps['apps'] |
| |
| # Queries the device for a list of all currently running apps. |
| def b2g_get_runningapps(): |
| global webappsActorName |
| apps = send_b2g_cmd(webappsActorName, 'listRunningApps') |
| return apps['apps'] # Returns manifestURLs of all running apps |
| |
| def print_applist(applist, running_app_manifests, print_removable): |
| num_printed = 0 |
| for app in applist: |
| if print_removable or app['removable']: # Print only removable apps unless --all is specified, skip the built-in apps that can't be uninstalled. |
| if 'manifest' in app and 'version' in app['manifest']: |
| version = " version '" + app['manifest']['version'] + "'" |
| else: |
| version = '' |
| if app['manifestURL'] in running_app_manifests: |
| version += ' RUNNING' |
| print(' ' + str(app['localId']) + ': "' + app['name'] + '"' + version) |
| num_printed += 1 |
| return num_printed |
| |
| def adb_devices(): |
| try: |
| devices = run_process([ADB, 'devices'], check=True, stdout=subprocess.PIPE).stdout |
| devices = devices.strip().split('\n')[1:] |
| devices = [x.strip().split('\t') for x in devices] |
| return devices |
| except Exception as e: |
| return [] |
| |
| def b2g_get_prefs_filename(): |
| return run_process([ADB, 'shell', 'echo', '-n', '/data/b2g/mozilla/*.default/prefs.js'], check=True, stdout=subprocess.PIPE).stdout |
| |
| def b2g_get_prefs_data(): |
| return run_process([ADB, 'shell', 'cat', '/data/b2g/mozilla/*.default/prefs.js'], check=True, stdout=subprocess.PIPE).stdout |
| |
| def b2g_get_pref(sub): |
| prefs_data = b2g_get_prefs_data().split('\n') |
| # Filter to find all prefs that have the substring 'sub' in them. |
| r = re.compile(r'user_pref\w*\(\w*"([^"]*)"\w*,\w*([^\)]*)') |
| for line in prefs_data: |
| m = r.match(line) |
| if m and (sub is None or sub in m.group(1)): |
| print(m.group(1) + ': ' + m.group(2).strip()) |
| |
| def b2g_set_pref(pref, value): |
| prefs_data = b2g_get_prefs_data().split('\n') |
| # Remove any old value of this pref. |
| r = re.compile(r'user_pref\w*\(\w*"([^"]*)"\w*,\w*([^\)]*)') |
| new_prefs_data = [] |
| for line in prefs_data: |
| m = r.match(line) |
| if not m or m.group(1) != pref: |
| new_prefs_data += [line] |
| |
| if value != None: |
| print('Setting pref "' + pref + '" = ' + value) |
| new_prefs_data += ['user_pref("' + pref + '", ' + value + ');'] |
| else: |
| print('Unsetting pref "' + pref + '"') |
| (oshandle, tempfilename) = tempfile.mkstemp(suffix='.js', prefix='ffdb_temp_') |
| os.write(oshandle, '\n'.join(new_prefs_data)); |
| |
| # Write the new pref |
| subprocess.check_output([ADB, 'shell', 'stop', 'b2g']) |
| subprocess.check_output([ADB, 'push', tempfilename, b2g_get_prefs_filename()]) |
| subprocess.check_output([ADB, 'shell', 'start', 'b2g']) |
| print('Rebooting phone...') |
| |
| def delete_temp_file(): |
| os.remove(tempfilename) |
| atexit.register(delete_temp_file) |
| |
| def get_packaged_app_manifest(target_app_path): |
| if os.path.isdir(target_app_path): |
| manifest_file = os.path.join(target_app_path, 'manifest.webapp') |
| if not os.path.isfile(manifest_file): |
| print("Error: Failed to find FFOS packaged app manifest file '" + manifest_file + "'! That directory does not contain a packaged app?") |
| sys.exit(1) |
| return json.loads(open(manifest_file, 'r').read()) |
| elif target_app_path.endswith('.zip') and os.path.isfile(target_app_path): |
| try: |
| z = zipfile.ZipFile(target_app_path, "r") |
| bytes = z.read('manifest.webapp') |
| except Exception as e: |
| print("Error: Failed to read FFOS packaged app manifest file 'manifest.webapp' in zip file '" + target_app_path + "'! Error: " + str(e)) |
| sys.exit(1) |
| return None |
| return json.loads(str(bytes)) |
| else: |
| print("Error: Path '" + target_app_path + "' is neither a directory or a .zip file to represent the location of a FFOS packaged app!") |
| sys.exit(1) |
| return None |
| |
| def b2g_install(target_app_path): |
| if os.path.isdir(target_app_path): |
| print('Zipping up the contents of directory "' + target_app_path + '"...') |
| (oshandle, tempzip) = tempfile.mkstemp(suffix='.zip', prefix='ffdb_temp_') |
| zipdir(target_app_path, tempzip) |
| target_app_path = tempzip |
| # Remember to delete the temporary package after we quit. |
| def delete_temp_file(): |
| os.remove(tempzip) |
| atexit.register(delete_temp_file) |
| |
| print('Uploading application package "' + target_app_path + '"...') |
| print('Size of compressed package: ' + sizeof_fmt(os.path.getsize(target_app_path)) + '.') |
| app_file = open(target_app_path, 'rb') |
| data = app_file.read() |
| file_size = len(data) |
| |
| uploadResponse = send_b2g_cmd(webappsActorName, 'uploadPackage', { 'bulk': 'true'}, print_errors_to_console = False) # This may fail if on old device. |
| start_time = time.time() |
| if 'actor' in uploadResponse and 'BulkActor' in uploadResponse['actor']: # New B2G 2.0 hotness: binary data transfer |
| packageUploadActor = uploadResponse['actor'] |
| send_b2g_bulk_data(packageUploadActor, data) |
| else: # Old B2G 1.4 and older, serialize binary data in JSON text strings (SLOW!) |
| print('Bulk upload is not supported, uploading binary data with old slow format. Consider flashing your device to FFOS 2.0 or newer to enjoy faster upload speeds.') |
| uploadResponse = send_b2g_cmd(webappsActorName, 'uploadPackage') |
| packageUploadActor = uploadResponse['actor'] |
| chunk_size = 4*1024*1024 |
| i = 0 |
| while i < file_size: |
| chunk = data[i:i+chunk_size] |
| |
| send_b2g_data_chunk(packageUploadActor, chunk) |
| i += chunk_size |
| bytes_uploaded = min(i, file_size) |
| cur_time = time.time() |
| secs_elapsed = cur_time - start_time |
| percentage_done = bytes_uploaded * 1.0 / file_size |
| total_time = secs_elapsed / percentage_done |
| time_left = total_time - secs_elapsed |
| print(sizeof_fmt(bytes_uploaded) + " uploaded, {:5.1f} % done.".format(percentage_done*100.0) + ' Elapsed: ' + str(int(secs_elapsed)) + ' seconds. Time left: ' + str(datetime.timedelta(seconds=int(time_left))) + '. Data rate: {:5.2f} KB/second.'.format(bytes_uploaded / 1024.0 / secs_elapsed)) |
| |
| app_local_id = str(uuid.uuid4()) |
| reply = send_b2g_cmd(webappsActorName, 'install', { 'appId': app_local_id, 'upload': packageUploadActor }) |
| cur_time = time.time() |
| secs_elapsed = cur_time - start_time |
| print('Upload of ' + sizeof_fmt(file_size) + ' finished. Total time elapsed: ' + str(int(secs_elapsed)) + ' seconds. Data rate: {:5.2f} KB/second.'.format(file_size / 1024.0 / secs_elapsed)) |
| if 'appId' not in reply: |
| print('Error: Application install failed! ' + str(reply)) |
| sys.exit() |
| return reply['appId'] |
| |
| def b2g_app_command(app_command, app_name, print_errors_to_console = True): |
| apps = b2g_get_appslist() |
| for app in apps: |
| if str(app['localId']) == app_name or app['name'] == app_name or app['manifestURL'] == app_name or app['id'] == app_name: |
| send_b2g_cmd(webappsActorName, app_command, { 'manifestURL': app['manifestURL'] }) |
| return 0 |
| if print_errors_to_console: |
| print('Error! Application "' + app_name + '" was not found! Use the \'list\' command to find installed applications.') |
| return 1 |
| |
| def b2g_memory(app_name): |
| apps = b2g_get_appslist() |
| appActor = '' |
| for app in apps: |
| if str(app['localId']) == app_name or app['name'] == app_name or app['manifestURL'] == app_name or app['id'] == app_name: |
| appActor = send_b2g_cmd(webappsActorName, 'getAppActor', { 'manifestURL': app['manifestURL'] }) |
| break |
| if 'actor' in appActor: |
| memoryActor = appActor['actor']['memoryActor'] |
| measure = send_b2g_cmd(memoryActor, 'measure') |
| for k,v in measure.items(): |
| if k != 'from': |
| if k in ['otherSize', 'jsStringsSize', 'jsObjectsSize', 'styleSize', 'jsOtherSize', 'domSize', 'total']: # These are formatted in bytes |
| print(k + ': ' + sizeof_fmt(v)) |
| else: |
| print(k + ': ' + str(v)) |
| |
| def b2g_log(app_name, clear=False): |
| global LOG_FORMAT |
| apps = b2g_get_appslist() |
| appActor = '' |
| for app in apps: |
| if str(app['localId']) == app_name or app['name'] == app_name or app['manifestURL'] == app_name or app['id'] == app_name: |
| appActor = send_b2g_cmd(webappsActorName, 'getAppActor', { 'manifestURL': app['manifestURL'] }) |
| break |
| if 'actor' in appActor: |
| consoleActor = appActor['actor']['consoleActor'] |
| |
| if clear: |
| send_b2g_cmd(consoleActor, 'clearMessagesCache') |
| print('Cleared message log.') |
| return 0 |
| |
| msgs = send_b2g_cmd(consoleActor, 'startListeners', { 'listeners': ['PageError','ConsoleAPI','NetworkActivity','FileActivity'] }) |
| |
| if WINDOWS: |
| WARNING = 14 # FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_INTENSITY |
| FAIL = 12 # FOREGROUND_RED | FOREGROUND_INTENSITY |
| INFO = 7 # FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE |
| ENDC = '' |
| BOLD = '' |
| else: |
| WARNING = '\033[93m' |
| FAIL = '\033[91m' |
| INFO = ENDC = '\033[0m' |
| BOLD = "\033[1m" |
| |
| def set_color(string, color): |
| if WINDOWS: |
| ctypes.windll.kernel32.SetConsoleTextAttribute(stdout_handle, color) |
| return string |
| else: |
| return color + string + ENDC |
| |
| def reset_color(): |
| if WINDOWS: |
| ctypes.windll.kernel32.SetConsoleTextAttribute(stdout_handle, INFO) |
| |
| def log_b2g_message(msg): |
| msgs = [] |
| if 'type' in msg and msg['type'] == 'consoleAPICall': |
| msgs = [msg['message']] |
| elif 'messages' in msg: |
| msgs = msg['messages'] |
| |
| for m in msgs: |
| args = m['arguments'] |
| |
| for arg in args: |
| if LOG_FORMAT == 'long': |
| text = str(m['functionName']) + '@' + str(m['filename']) + ':' + str(m['lineNumber']) + ': ' + str(arg) |
| elif LOG_FORMAT == 'bare': |
| text = str(arg) |
| else: # Default to 'short' |
| text = str(m['functionName']) + '@' + os.path.basename(str(m['filename'])) + ':' + str(m['lineNumber']) + ': ' + str(arg) |
| |
| if m['level'] == 'log': |
| color = INFO |
| channel = 'I/' |
| elif m['level'] == 'warn': |
| color = WARNING |
| channel = 'W/' |
| elif m['level'] == 'error': |
| color = FAIL |
| channel = 'E/' |
| else: |
| color = INFO |
| channel = m['level'] + '/' |
| |
| text = set_color(channel + text, color) |
| print(text) |
| reset_color() |
| |
| msgs = send_b2g_cmd(consoleActor, 'getCachedMessages', { 'messageTypes': ['PageError', 'ConsoleAPI'] }) |
| log_b2g_message(msgs) |
| |
| while True: |
| msg = read_b2g_response() |
| log_b2g_message(msg) |
| else: |
| print('Application "' + sys.argv[2] + '" is not running!') |
| |
| def b2g_screenshot(filename): |
| global deviceActorName |
| data_reply = send_b2g_cmd(deviceActorName, 'screenshotToDataURL') |
| data = data_reply['value'] |
| if not isinstance(data, (type(u''), bytes)): # The device is sending the screenshot in multiple fragments since it's too long to fit in one message? |
| data_get_actor = data['actor'] |
| data_len = int(data['length']) |
| data = data['initial'] |
| chunk_size = 65000 |
| pos = len(data) |
| # Pull and assemble individual screenshot fragments. |
| while pos < data_len: |
| bytes_to_read = min(data_len - pos, chunk_size) |
| data_reply = send_b2g_cmd(data_get_actor, 'substring', { 'start': str(pos), 'end': str(pos + bytes_to_read) }) |
| if len(data_reply['substring']) != bytes_to_read: |
| print('Error! Expected to receive ' + str(bytes_to_read) + ' bytes of image data, but got ' + str(len(data_reply['substring'])) + ' bytes instead!', file=sys.stderr) |
| sys.exit(1) |
| data += data_reply['substring'] |
| pos += bytes_to_read |
| send_b2g_cmd(data_get_actor, 'release') # We need to explicitly free the screenshot image string from the device, or the Devtools connection leaks resources! |
| |
| # Expected format is "data:image/png;base64,<base64data>" |
| delim = re.search(",", data).start() |
| data_format = data[:delim] |
| if data_format != "data:image/png;base64": |
| print("Error: Received screenshot from device in an unexpected format '" + data_format + "'!", file=sys.stderr) |
| sys.exit(1) |
| data = data[delim+1:] |
| |
| binary_data = base64.b64decode(data) |
| open(filename, 'wb').write(binary_data) |
| |
| def get_png_image_size(filename): |
| fhandle = open(filename, 'rb') |
| head = fhandle.read(24) |
| if len(head) != 24: |
| return (-1, -1) |
| check = struct.unpack('>i', head[4:8])[0] |
| if check != 0x0d0a1a0a: |
| return (-1, -1) |
| return struct.unpack('>ii', head[16:24]) |
| |
| width, height = get_png_image_size(filename) |
| if width <= 0 or height <= 0: |
| print("Wrote " + sizeof_fmt(len(binary_data)) + " to file '" + filename + "', but the contents may be corrupted!", file=sys.stderr) |
| else: |
| print("Wrote " + sizeof_fmt(len(binary_data)) + " to file '" + filename + "' (" + str(width) + 'x' + str(height) + ' pixels).') |
| |
| def b2g_get_description(desc): |
| global deviceActorName |
| data_reply = send_b2g_cmd(deviceActorName, 'getDescription') |
| # First try an exact match to requested desc |
| if desc and desc in data_reply['value']: |
| print(desc + ': ' + str(data_reply['value'][desc])) |
| else: # Print all with case-insensitive substring search |
| for k,v in data_reply['value'].items(): |
| if not desc or desc.lower() in k.lower(): |
| print(k + ': ' + str(v)) |
| |
| def main(): |
| global b2g_socket, webappsActorName, deviceActorName, HOST, PORT, VERBOSE, ADB |
| if len(sys.argv) < 2 or '--help' in sys.argv or 'help' in sys.argv or '-v' in sys.argv: |
| print('''Firefox OS Debug Bridge, a tool for automating FFOS device tasks from the command line. |
| |
| Usage: ffdb.py <command>, where command is one of: |
| |
| list [--running] [--all]: Prints out the user applications installed on the device. |
| If --running is passed, only the currently opened apps are shown. |
| If --all is specified, then also uninstallable system applications are listed. |
| launch <app> [--log]: Starts the given application. If already running, brings to front. If the --log option is passed, ffdb will |
| start persistently logging the execution of the given application. |
| close <app>: Terminates the execution of the given application. |
| uninstall <app>: Removes the given application from the device. |
| install <path> [--run] [--log]: Uploads and installs a packaged app that resides in the given local directory. |
| <path> may either refer to a directory containing a packaged app, or to a prepackaged zip file. |
| If the --run option is passed, the given application is immediately launched after the installation finishes. |
| If the --log option is passed, ffdb will start persistently logging the execution of the installed application. |
| log <app> [--clear]: Starts a persistent log listener that reads web console messages from the given application. |
| If --clear is passed, the message log for that application is cleared instead. |
| memory <app>: Dumps a memory usage summary for the given application. |
| navigate <url>: Opens the given web page in the B2G browser. |
| screenshot [filename.png]: Takes a screenshot of the current contents displayed on the device. If an optional |
| filename is specified, the screenshot is saved to that file. Otherwise the filename |
| will be autogenerated. |
| get [pref]: Fetches the value of the given developer pref option from the FFOS device and prints it to console. The parameter pref |
| is optional and may either be the full name of a pref, or a substring to search for. All matching prefs will be printed. |
| If no pref parameter is given, all prefs are printed. |
| NOTE: This function (currently at least) only reports prefs that have been explicitly set and don't have their default value. |
| set <pref> <value>: Writes the given pref option to the FFOS device and restarts the B2G process on it for the change to take effect. |
| unset <pref>: Removes the given pref option from the FFOS device and restarts the B2G process on it for the change to take effect. |
| |
| hide-prompt: Permanently removes the remote debugging connection dialog from showing up, and reboots the phone. This command is |
| provided for conveniency, and is the same as calling './ffdb.py set devtools.debugger.prompt-connection false' |
| restore-prompt: Restores the remote debugging connection dialog prompt to its default state. |
| |
| desc [desc]: Fetches the value of the given device description field. These fields are read-only and describe the current system. |
| If the optional desc parameter is omitted, all device descriptions are printed. Otherwise the given description is |
| printed if it is an exact match, or all descriptions containing desc as the substring are printed. |
| |
| Options: Additionally, the following options may be passed to control FFDB execution: |
| |
| --host <hostname>: Specifies the target network address to connect to. Default: 'localhost'. |
| --port <number>: Specifies the network port to connect to. Default: 6000. |
| --verbose: Enables verbose printing, mostly useful for debugging. |
| --simulator: Signal that we will be connecting to a FFOS simulator and not a real device. |
| |
| In the above, whenever a command requires an <app> to be specified, either the human-readable name, |
| localId or manifestURL of the application can be used.''') |
| |
| sys.exit(0) |
| |
| connect_to_simulator = False |
| |
| options_with_value = ['--host', '--port'] |
| options = options_with_value + ['--verbose', '--simulator'] |
| # Process options |
| for i in range(0, len(sys.argv)): |
| if sys.argv[i] in options_with_value: |
| if i+1 >= sys.argv or sys.argv[i+1].startswith('-'): |
| print("Missing value for option " + sys.argv[i] +'!', file=sys.stderr) |
| sys.exit(1) |
| if sys.argv[i] == '--host': |
| HOST = sys.argv[i+1] |
| elif sys.argv[i] == '--port': |
| PORT = int(sys.argv[i+1]) |
| elif sys.argv[i] == '--verbose': |
| VERBOSE = True |
| elif sys.argv[i] == '--simulator': |
| connect_to_simulator = True |
| |
| # Clear the processed options so that parsing the commands below won't trip up on these. |
| if sys.argv[i] in options: sys.argv[i] = '' |
| if sys.argv[i] in options_with_value: sys.argv[i+1] = '' |
| |
| sys.argv = [x for x in sys.argv if len(x)] |
| |
| # Double-check that the device is found via adb: |
| if (HOST == 'localhost' or HOST == '127.0.0.1') and not connect_to_simulator: |
| devices = adb_devices() |
| if len(devices) == 0: |
| print('Error! Failed to connect to B2G device debugger socket at address ' + HOST + ':' + str(PORT) + ' and no devices were detected via adb. Please double-check the following and try again: ') |
| print(' 1) The device is powered on and connected to the computer with an USB cable.') |
| print(' 2) ADB and DevTools debugging is enabled on the device. (Settings -> Developer -> Debugging via USB: "ADB and DevTools"') |
| print(' 3) The device is listed when you run "adb devices" on the command line.') |
| print(' 4) When launching ffdb, remember to acknowledge the "incoming debug connection" dialog if it pops up on the device.') |
| sys.exit(1) |
| b2g_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| if WINDOWS: |
| # Python Windows issue: user cannot press Ctrl-C to abort from a socket .recv() Therefore simulate blocking sockets with looping over reads that time out. |
| b2g_socket.settimeout(0.5) |
| try: |
| b2g_socket.connect((HOST, PORT)) |
| except Exception as e: |
| if e[0] == 61 or e[0] == 107 or e[0] == 111: # 61 == Connection refused and 107+111 == Transport endpoint is not connected |
| if (HOST == 'localhost' or HOST == '127.0.0.1') and not connect_to_simulator: |
| cmd = [ADB, 'forward', 'tcp:'+str(PORT), 'localfilesystem:/data/local/debugger-socket'] |
| print('Connection to ' + HOST + ':' + str(PORT) + ' refused, attempting to forward device debugger-socket to local address by calling ' + str(cmd) + ':') |
| else: |
| print('Error! Failed to connect to B2G ' + ('simulator' if connect_to_simulator else 'device') + ' debugger socket at address ' + HOST + ':' + str(PORT) + '!') |
| sys.exit(1) |
| try: |
| retcode = subprocess.check_call(cmd) |
| except Exception as e: |
| print('Error! Failed to execute adb: ' + str(e)) |
| print("Check that the device is connected properly, call 'adb devices' to list the detected devices.") |
| sys.exit(1) |
| if retcode is not 0: |
| print('Error! Failed to connect to B2G device and executing adb failed with return code ' + retcode + '!') |
| sys.exit(1) |
| time.sleep(3) |
| # Try again: |
| try: |
| b2g_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| b2g_socket.connect((HOST, PORT)) |
| except Exception as e: |
| print('Error! Failed to connect to B2G device debugger socket at address ' + HOST + ':' + str(PORT) + '!') |
| sys.exit(1) |
| |
| handshake = read_b2g_response() |
| logv('Connected. Handshake: ' + str(handshake)) |
| |
| data = send_b2g_cmd('root', 'listTabs') |
| if 'deviceActor' not in data: |
| print('Error! Debugging connection was not available. Make sure that the "Remote debugging" developer option on the device is set to "ADB and Devtools".') |
| sys.exit(1) |
| deviceActorName = data['deviceActor'] |
| logv('deviceActor: ' + deviceActorName) |
| webappsActorName = data['webappsActor'] |
| logv('webappsActor: ' + webappsActorName) |
| |
| send_b2g_cmd(deviceActorName, 'getDescription') |
| send_b2g_cmd(deviceActorName, 'getRawPermissionsTable') |
| |
| if sys.argv[1] == 'list': |
| apps = b2g_get_appslist() |
| running_app_manifests = b2g_get_runningapps() |
| printed_apps = apps |
| print_only_running = '--running' in sys.argv and not '--all' in sys.argv |
| if print_only_running: # Print running apps only? |
| print('Running applications by id:') |
| printed_apps = [x for x in apps if x['manifestURL'] in running_app_manifests] |
| else: |
| print('Installed applications by id:') |
| num_printed = print_applist(printed_apps, running_app_manifests, '--all' in sys.argv or print_only_running) |
| if num_printed == 0: |
| if print_only_running: |
| print(' No applications running.') |
| else: |
| print(' No applications installed.') |
| if '--all' not in sys.argv and not print_only_running: |
| print('Not showing built-in apps that cannot be uninstalled. Pass --all to include those in the listing.') |
| elif sys.argv[1] == 'launch' or sys.argv[1] == 'close' or sys.argv[1] == 'uninstall' or sys.argv[1] == 'getAppActor': |
| if len(sys.argv) < 3: |
| print('Error! No application name given! Usage: ' + sys.argv[0] + ' ' + sys.argv[1] + ' <app>') |
| return 1 |
| ret = b2g_app_command(sys.argv[1], sys.argv[2]) |
| if ret == 0 and '--log' in sys.argv: |
| b2g_log(sys.argv[2]) |
| elif sys.argv[1] == 'install': |
| if len(sys.argv) < 3: |
| print('Error! No application path given! Usage: ' + sys.argv[0] + ' ' + sys.argv[1] + ' <path>') |
| return 1 |
| target_app_path = sys.argv[2] |
| # Kill and uninstall old running app execution before starting. |
| if '--run' in sys.argv: |
| app_manifest = get_packaged_app_manifest(target_app_path) |
| b2g_app_command('close', app_manifest['name'], print_errors_to_console=False) |
| b2g_app_command('uninstall', app_manifest['name'], print_errors_to_console=False) |
| # Upload package |
| app_id = b2g_install(target_app_path) |
| # Launch it immediately if requested. |
| if '--run' in sys.argv: |
| b2g_app_command('launch', app_id) |
| # Don't quit, but keep logging the app if requested. |
| if '--log' in sys.argv: |
| b2g_log(app_id) |
| elif sys.argv[1] == 'navigate': |
| if len(sys.argv) < 3: |
| print('Error! No URL given! Usage: ' + sys.argv[0] + ' ' + sys.argv[1] + ' <url>') |
| return 1 |
| browserActor = '' |
| for app in apps: |
| if app['name'] == 'Browser': |
| browserActor = send_b2g_cmd(webappsActorName, 'getAppActor', { 'manifestURL': app['manifestURL'] }) |
| break |
| if 'actor' in browserActor: |
| browserActor = browserActor['actor']['actor'] |
| send_b2g_cmd(browserActor, 'navigateTo', { 'url': sys.argv[2]}) |
| else: |
| print('Web browser is not running!') |
| elif sys.argv[1] == 'log': |
| if len(sys.argv) < 3: |
| print('Error! No application name given! Usage: ' + sys.argv[0] + ' ' + sys.argv[1] + ' <app>') |
| return 1 |
| clear = '-c' in sys.argv or '-clear' in sys.argv or '--clear' in sys.argv |
| b2g_log(sys.argv[2], clear) |
| elif sys.argv[1] == 'memory': |
| b2g_memory(sys.argv[2]) |
| elif sys.argv[1] == 'screenshot': |
| if len(sys.argv) >= 3: |
| filename = sys.argv[2] |
| if not filename.endswith('.png'): |
| print("Writing screenshots only to .png files are supported!", file=sys.stderr) |
| sys.exit(1) |
| else: |
| filename = time.strftime("screen_%Y%m%d_%H%M%S.png", time.gmtime()) |
| |
| b2g_screenshot(filename) |
| elif sys.argv[1] == 'get': |
| b2g_get_pref(sys.argv[2] if len(sys.argv) >= 3 else None) |
| elif sys.argv[1] == 'set': |
| if len(sys.argv) < 3: |
| print('Error! No pref name to set given! Usage: ' + sys.argv[0] + ' ' + sys.argv[1] + ' <pref> <value>') |
| sys.exit(1) |
| if len(sys.argv) < 4: |
| print('Error! No value given to set! Usage: ' + sys.argv[0] + ' ' + sys.argv[1] + ' <pref> <value>') |
| sys.exit(1) |
| if len(sys.argv) > 4: |
| print('Error! Too many arguments given (' + str(sys.argv) + '), need exactly four! Usage: ' + sys.argv[0] + ' ' + sys.argv[1] + ' <pref> <value>') |
| sys.exit(1) |
| b2g_set_pref(sys.argv[2], sys.argv[3]) |
| elif sys.argv[1] == 'unset': |
| if len(sys.argv) < 3: |
| print('Error! No pref name given! Usage: ' + sys.argv[0] + ' ' + sys.argv[1] + ' <pref>') |
| sys.exit(1) |
| b2g_set_pref(sys.argv[2], None) |
| elif sys.argv[1] == 'hide-prompt': |
| b2g_set_pref('devtools.debugger.prompt-connection', 'false') |
| elif sys.argv[1] == 'restore-prompt': |
| b2g_set_pref('devtools.debugger.prompt-connection', None) |
| elif sys.argv[1] == 'desc': |
| b2g_get_description(sys.argv[2] if len(sys.argv) >= 3 else None) |
| else: |
| print("Unknown command '" + sys.argv[1] + "'! Pass --help for instructions.") |
| |
| b2g_socket.close() |
| return 0 |
| |
| if __name__ == '__main__': |
| try: |
| returncode = main() |
| logv('ffdb.py quitting with process exit code ' + str(returncode)) |
| sys.exit(returncode) |
| except KeyboardInterrupt: |
| print(('^C' if WINDOWS else '') + ' Aborted by user') |
| sys.exit(1) |