#!/usr/bin/env python

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('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 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) > 0]

  # 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)
