summaryrefslogtreecommitdiff
path: root/tools/ffdb.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/ffdb.py')
-rwxr-xr-xtools/ffdb.py276
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."