diff options
Diffstat (limited to 'tools/ffdb.py')
-rwxr-xr-x | tools/ffdb.py | 276 |
1 files changed, 246 insertions, 30 deletions
diff --git a/tools/ffdb.py b/tools/ffdb.py index c22fd9db..497f0162 100755 --- a/tools/ffdb.py +++ b/tools/ffdb.py @@ -1,7 +1,8 @@ #!/usr/bin/env python -import socket, json, sys, uuid, datetime, time, logging, cgi, zipfile, os, tempfile, atexit, subprocess +import socket, json, sys, uuid, datetime, time, logging, cgi, zipfile, os, tempfile, atexit, subprocess, re, base64, struct, imghdr +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 @@ -18,7 +19,13 @@ def sizeof_fmt(num): return "%3.1f%s" % (num, 'TB') def zipdir(path, zipfilename): - zipf = zipfile.ZipFile(zipfilename, 'w') + 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: @@ -29,9 +36,10 @@ def zipdir(path, zipfilename): (root, file) = tuple filename = os.path.join(root, file) filesize = os.path.getsize(filename) - print 'Compressing ' + str(n) + '/' + str(len(files_to_compress)) + ': "' + os.path.relpath(filename, path) + '" (' + sizeof_fmt(filesize) + ')...' + 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)) + zipf.write(os.path.join(root, file), path_in_archive) zipf.close() print 'Done. ' @@ -46,16 +54,27 @@ def format_html(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)) + 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(): +def read_b2g_response(print_errors_to_console = True): global read_queue, b2g_socket - read_queue += b2g_socket.recv(65536*2) + try: + read_queue += b2g_socket.recv(65536*2) + except KeyboardInterrupt: + print ' Aborted by user' + sys.exit(1) + except Exception, 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: + raise + payload = '' while ':' in read_queue: semicolon = read_queue.index(':') payload_len = int(read_queue[:semicolon]) @@ -66,10 +85,13 @@ def read_b2g_response(): 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 >> sys.stderr, 'Received error "' + payload['error'] + '"! Reason: ' + payload['message'] 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 = {}): +def send_b2g_cmd(to, cmd, data = {}, print_errors_to_console = True): global b2g_socket msg = { 'to': to, 'type': cmd} msg = dict(msg.items() + data.items()) @@ -78,7 +100,7 @@ def send_b2g_cmd(to, cmd, data = {}): msg = str(len(msg))+':'+msg logv('Sending cmd:' + cmd + ' to:' + to) b2g_socket.sendall(msg) - return read_b2g_response() + return read_b2g_response(print_errors_to_console) def escape_bytes(b): return str(b) @@ -102,7 +124,16 @@ def send_b2g_data_chunk(to, data_blob): 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(): @@ -130,8 +161,60 @@ def print_applist(applist, running_app_manifests, print_removable): num_printed += 1 return num_printed +def adb_devices(): + try: + devices = subprocess.check_output([ADB, 'devices']) + devices = devices.strip().split('\n')[1:] + devices = map(lambda x: x.strip().split('\t'), devices) + return devices + except Exception, e: + return [] + +def b2g_get_prefs_filename(): + return subprocess.check_output([ADB, 'shell', 'echo', '-n', '/data/b2g/mozilla/*.default/prefs.js']) + +def b2g_get_prefs_data(): + return subprocess.check_output([ADB, 'shell', 'cat', '/data/b2g/mozilla/*.default/prefs.js']) + +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('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('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 main(): - global b2g_socket, webappsActorName + global b2g_socket, webappsActorName, 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. @@ -148,22 +231,74 @@ def main(): 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. 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. + 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. + + 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 >> sys.stderr, "Missing value for option " + sys.argv[i] +'!' + 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 = filter(lambda x: len(x) > 0, sys.argv) + + # 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) try: b2g_socket.connect((HOST, PORT)) except Exception, e: if e[0] == 61: # Connection refused - if HOST == 'localhost' or HOST == '127.0.0.1': - cmd = ['adb', 'forward', 'tcp:'+str(PORT), 'localfilesystem:/data/local/debugger-socket'] + 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 device debugger socket at address ' + HOST + ':' + str(PORT) + '!' + 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) @@ -187,6 +322,9 @@ def main(): logv('Connected. Handshake: ' + str(handshake)) data = send_b2g_cmd('root', 'listTabs') + if not 'deviceActor' 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'] @@ -241,28 +379,35 @@ def main(): print 'Uploading application package "' + target_app_path + '"...' print 'Size of compressed package: ' + sizeof_fmt(os.path.getsize(target_app_path)) + '.' - uploadResponse = send_b2g_cmd(webappsActorName, 'uploadPackage') - packageUploadActor = uploadResponse['actor'] app_file = open(target_app_path, 'rb') data = app_file.read() file_size = len(data) - chunk_size = 4*1024*1024 - i = 0 + + uploadResponse = send_b2g_cmd(webappsActorName, 'uploadPackage', { 'bulk': 'true'}, print_errors_to_console = False) # This may fail if on old device. start_time = time.time() - 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) - send_b2g_cmd(webappsActorName, 'install', { 'appId': str(uuid.uuid4()), 'upload': packageUploadActor }) + 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) + send_b2g_cmd(webappsActorName, 'install', { 'appId': str(uuid.uuid4()), '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) @@ -330,6 +475,77 @@ def main(): log_b2g_message(msg) else: print 'Application "' + sys.argv[2] + '" is not running!' + elif sys.argv[1] == 'screenshot': + if len(sys.argv) >= 3: + filename = sys.argv[2] + if not filename.endswith('.png'): + print >> sys.stderr, "Writing screenshots only to .png files are supported!" + sys.exit(1) + else: + filename = time.strftime("screen_%Y%m%d_%H%M%S.png", time.gmtime()) + + data_reply = send_b2g_cmd(deviceActorName, 'screenshotToDataURL') + data = data_reply['value'] + data_get_actor = data['actor'] + data_len = int(data['length']) + data_str = data['initial'] + delim = re.search(",", data_str).start() + data_format = data_str[:delim] + if data_format != "data:image/png;base64": + print >> sys.stderr, "Error: Received screenshot from device in an unexpected format '" + data_format + "'!" + sys.exit(1) + data = data_str[delim+1:] + chunk_size = 65000 + pos = len(data_str) + 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 >> sys.stderr, 'Error! Expected to receive ' + str(bytes_to_read) + ' bytes of image data, but got ' + str(len(data_reply['substring'])) + ' bytes instead!' + 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! + 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 >> sys.stderr, "Wrote " + sizeof_fmt(len(binary_data)) + " to file '" + filename + "', but the contents may be corrupted!" + else: + print "Wrote " + sizeof_fmt(len(binary_data)) + " to file '" + filename + "' (" + str(width) + 'x' + str(height) + ' pixels).' + 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) else: print "Unknown command '" + sys.argv[1] + "'! Pass --help for instructions." |