#!/usr/bin/env python
import socket, json, sys, uuid, datetime, time, logging, cgi, zipfile, os, tempfile, atexit, subprocess
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):
zipf = zipfile.ZipFile(zipfilename, 'w')
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)
print 'Compressing ' + str(n) + '/' + str(len(files_to_compress)) + ': "' + os.path.relpath(filename, path) + '" (' + sizeof_fmt(filesize) + ')...'
n += 1
zipf.write(os.path.join(root, file))
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', '
').replace('\n', '
')
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))
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():
global read_queue, b2g_socket
read_queue += b2g_socket.recv(65536*2)
while ':' in read_queue:
semicolon = read_queue.index(':')
payload_len = int(read_queue[:semicolon])
if semicolon+1+payload_len > len(read_queue):
read_queue += b2g_socket.recv(65536*2)
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)
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 = {}):
global b2g_socket
msg = { 'to': to, 'type': cmd}
msg = dict(msg.items() + 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()
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
b2g_socket.sendall(message)
# 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 main():
global b2g_socket, webappsActorName
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 , 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 : Starts the given application. If already running, brings to front.
close : Terminates the execution of the given application.
uninstall : Removes the given application from the device.
install : Uploads and installs a packaged app that resides in the given local directory.
may either refer to a directory containing a packaged app, or to a prepackaged zip file.
log [--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 : Opens the given web page in the B2G browser.
In the above, whenever a command requires an to be specified, either the human-readable name,
localId or manifestURL of the application can be used.'''
sys.exit(0)
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']
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) + '!'
sys.exit(1)
try:
retcode = subprocess.check_call(cmd)
except Exception, 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, 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')
deviceActorName = data['deviceActor']
logv('deviceActor: ' + deviceActorName)
webappsActorName = data['webappsActor']
logv('webappsActor: ' + webappsActorName)
send_b2g_cmd(deviceActorName, 'getDescription')
send_b2g_cmd(deviceActorName, 'getRawPermissionsTable')
apps = b2g_get_appslist()
if sys.argv[1] == 'list':
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 = filter(lambda x: x['manifestURL'] in running_app_manifests, apps)
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 not '--all' 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] + ' '
return 1
for app in apps:
if str(app['localId']) == sys.argv[2] or app['name'] == sys.argv[2] or app['manifestURL'] == sys.argv[2]:
send_b2g_cmd(webappsActorName, sys.argv[1], { 'manifestURL': app['manifestURL'] })
return 0
print 'Error! Application "' + sys.argv[2] + '" was not found! Use the \'list\' command to find installed applications.'
return 1
elif sys.argv[1] == 'install':
if len(sys.argv) < 3:
print 'Error! No application path given! Usage: ' + sys.argv[0] + ' ' + sys.argv[1] + ' '
return 1
target_app_path = sys.argv[2]
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)) + '.'
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
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 })
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)
elif sys.argv[1] == 'navigate':
if len(sys.argv) < 3:
print 'Error! No URL given! Usage: ' + sys.argv[0] + ' ' + sys.argv[1] + ' '
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':
appActor = ''
for app in apps:
if str(app['localId']) == sys.argv[2] or app['name'] == sys.argv[2] or app['manifestURL'] == sys.argv[2]:
appActor = send_b2g_cmd(webappsActorName, 'getAppActor', { 'manifestURL': app['manifestURL'] })
break
if 'actor' in appActor:
consoleActor = appActor['actor']['consoleActor']
if '-c' in sys.argv or '-clear' in sys.argv or '--clear' in sys.argv:
send_b2g_cmd(consoleActor, 'clearMessagesCache')
print 'Cleared message log.'
sys.exit(0)
msgs = send_b2g_cmd(consoleActor, 'startListeners', { 'listeners': ['PageError','ConsoleAPI','NetworkActivity','FileActivity'] })
def log_b2g_message(msg):
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
BOLD = "\033[1m"
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 m['level'] == 'log':
color = 'I/'
elif m['level'] == 'warn':
color = WARNING + 'W/'
elif m['level'] == 'error':
color = FAIL + 'E/'
else:
color = m['level'] + '/'
print color + str(m['functionName']) + '@' + str(m['filename']) + ':' + str(m['lineNumber']) + ': ' + str(arg) + ENDC
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!'
else:
print "Unknown command '" + sys.argv[1] + "'! Pass --help for instructions."
b2g_socket.close()
return 0
if __name__ == '__main__':
returncode = main()
logv('ffdb.py quitting with process exit code ' + str(returncode))
sys.exit(returncode)