Home | History | Annotate | Download | only in linux
      1 #!/usr/bin/python
      2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      3 # Use of this source code is governed by a BSD-style license that can be
      4 # found in the LICENSE file.
      5 
      6 # Virtual Me2Me implementation.  This script runs and manages the processes
      7 # required for a Virtual Me2Me desktop, which are: X server, X desktop
      8 # session, and Host process.
      9 # This script is intended to run continuously as a background daemon
     10 # process, running under an ordinary (non-root) user account.
     11 
     12 import atexit
     13 import errno
     14 import fcntl
     15 import getpass
     16 import grp
     17 import hashlib
     18 import json
     19 import logging
     20 import optparse
     21 import os
     22 import pipes
     23 import platform
     24 import psutil
     25 import platform
     26 import signal
     27 import socket
     28 import subprocess
     29 import sys
     30 import tempfile
     31 import time
     32 import uuid
     33 
     34 LOG_FILE_ENV_VAR = "CHROME_REMOTE_DESKTOP_LOG_FILE"
     35 
     36 # This script has a sensible default for the initial and maximum desktop size,
     37 # which can be overridden either on the command-line, or via a comma-separated
     38 # list of sizes in this environment variable.
     39 DEFAULT_SIZES_ENV_VAR = "CHROME_REMOTE_DESKTOP_DEFAULT_DESKTOP_SIZES"
     40 
     41 # By default, provide a maximum size that is large enough to support clients
     42 # with large or multiple monitors. This is a comma-separated list of
     43 # resolutions that will be made available if the X server supports RANDR. These
     44 # defaults can be overridden in ~/.profile.
     45 DEFAULT_SIZES = "1600x1200,3840x2560"
     46 
     47 # If RANDR is not available, use a smaller default size. Only a single
     48 # resolution is supported in this case.
     49 DEFAULT_SIZE_NO_RANDR = "1600x1200"
     50 
     51 SCRIPT_PATH = sys.path[0]
     52 
     53 IS_INSTALLED = (os.path.basename(sys.argv[0]) != 'linux_me2me_host.py')
     54 
     55 if IS_INSTALLED:
     56   HOST_BINARY_NAME = "chrome-remote-desktop-host"
     57 else:
     58   HOST_BINARY_NAME = "remoting_me2me_host"
     59 
     60 CHROME_REMOTING_GROUP_NAME = "chrome-remote-desktop"
     61 
     62 HOME_DIR = os.environ["HOME"]
     63 CONFIG_DIR = os.path.join(HOME_DIR, ".config/chrome-remote-desktop")
     64 SESSION_FILE_PATH = os.path.join(HOME_DIR, ".chrome-remote-desktop-session")
     65 SYSTEM_SESSION_FILE_PATH = "/etc/chrome-remote-desktop-session"
     66 
     67 X_LOCK_FILE_TEMPLATE = "/tmp/.X%d-lock"
     68 FIRST_X_DISPLAY_NUMBER = 20
     69 
     70 # Amount of time to wait between relaunching processes.
     71 SHORT_BACKOFF_TIME = 5
     72 LONG_BACKOFF_TIME = 60
     73 
     74 # How long a process must run in order not to be counted against the restart
     75 # thresholds.
     76 MINIMUM_PROCESS_LIFETIME = 60
     77 
     78 # Thresholds for switching from fast- to slow-restart and for giving up
     79 # trying to restart entirely.
     80 SHORT_BACKOFF_THRESHOLD = 5
     81 MAX_LAUNCH_FAILURES = SHORT_BACKOFF_THRESHOLD + 10
     82 
     83 # Globals needed by the atexit cleanup() handler.
     84 g_desktops = []
     85 g_host_hash = hashlib.md5(socket.gethostname()).hexdigest()
     86 
     87 
     88 def is_supported_platform():
     89   # Always assume that the system is supported if the config directory or
     90   # session file exist.
     91   if (os.path.isdir(CONFIG_DIR) or os.path.isfile(SESSION_FILE_PATH) or
     92       os.path.isfile(SYSTEM_SESSION_FILE_PATH)):
     93     return True
     94 
     95   # The host has been tested only on Ubuntu.
     96   distribution = platform.linux_distribution()
     97   return (distribution[0]).lower() == 'ubuntu'
     98 
     99 
    100 def get_randr_supporting_x_server():
    101   """Returns a path to an X server that supports the RANDR extension, if this
    102   is found on the system. Otherwise returns None."""
    103   try:
    104     xvfb = "/usr/bin/Xvfb-randr"
    105     if not os.path.exists(xvfb):
    106       xvfb = locate_executable("Xvfb-randr")
    107     return xvfb
    108   except Exception:
    109     return None
    110 
    111 
    112 class Config:
    113   def __init__(self, path):
    114     self.path = path
    115     self.data = {}
    116     self.changed = False
    117 
    118   def load(self):
    119     """Loads the config from file.
    120 
    121     Raises:
    122       IOError: Error reading data
    123       ValueError: Error parsing JSON
    124     """
    125     settings_file = open(self.path, 'r')
    126     self.data = json.load(settings_file)
    127     self.changed = False
    128     settings_file.close()
    129 
    130   def save(self):
    131     """Saves the config to file.
    132 
    133     Raises:
    134       IOError: Error writing data
    135       TypeError: Error serialising JSON
    136     """
    137     if not self.changed:
    138       return
    139     old_umask = os.umask(0066)
    140     try:
    141       settings_file = open(self.path, 'w')
    142       settings_file.write(json.dumps(self.data, indent=2))
    143       settings_file.close()
    144       self.changed = False
    145     finally:
    146       os.umask(old_umask)
    147 
    148   def save_and_log_errors(self):
    149     """Calls self.save(), trapping and logging any errors."""
    150     try:
    151       self.save()
    152     except (IOError, TypeError) as e:
    153       logging.error("Failed to save config: " + str(e))
    154 
    155   def get(self, key):
    156     return self.data.get(key)
    157 
    158   def __getitem__(self, key):
    159     return self.data[key]
    160 
    161   def __setitem__(self, key, value):
    162     self.data[key] = value
    163     self.changed = True
    164 
    165   def clear(self):
    166     self.data = {}
    167     self.changed = True
    168 
    169 
    170 class Authentication:
    171   """Manage authentication tokens for Chromoting/xmpp"""
    172 
    173   def __init__(self):
    174     self.login = None
    175     self.oauth_refresh_token = None
    176 
    177   def copy_from(self, config):
    178     """Loads the config and returns false if the config is invalid."""
    179     try:
    180       self.login = config["xmpp_login"]
    181       self.oauth_refresh_token = config["oauth_refresh_token"]
    182     except KeyError:
    183       return False
    184     return True
    185 
    186   def copy_to(self, config):
    187     config["xmpp_login"] = self.login
    188     config["oauth_refresh_token"] = self.oauth_refresh_token
    189 
    190 
    191 class Host:
    192   """This manages the configuration for a host."""
    193 
    194   def __init__(self):
    195     self.host_id = str(uuid.uuid1())
    196     self.host_name = socket.gethostname()
    197     self.host_secret_hash = None
    198     self.private_key = None
    199 
    200   def copy_from(self, config):
    201     try:
    202       self.host_id = config["host_id"]
    203       self.host_name = config["host_name"]
    204       self.host_secret_hash = config.get("host_secret_hash")
    205       self.private_key = config["private_key"]
    206     except KeyError:
    207       return False
    208     return True
    209 
    210   def copy_to(self, config):
    211     config["host_id"] = self.host_id
    212     config["host_name"] = self.host_name
    213     config["host_secret_hash"] = self.host_secret_hash
    214     config["private_key"] = self.private_key
    215 
    216 
    217 class Desktop:
    218   """Manage a single virtual desktop"""
    219 
    220   def __init__(self, sizes):
    221     self.x_proc = None
    222     self.session_proc = None
    223     self.host_proc = None
    224     self.child_env = None
    225     self.sizes = sizes
    226     self.pulseaudio_pipe = None
    227     self.server_supports_exact_resize = False
    228     self.host_ready = False
    229     self.ssh_auth_sockname = None
    230     g_desktops.append(self)
    231 
    232   @staticmethod
    233   def get_unused_display_number():
    234     """Return a candidate display number for which there is currently no
    235     X Server lock file"""
    236     display = FIRST_X_DISPLAY_NUMBER
    237     while os.path.exists(X_LOCK_FILE_TEMPLATE % display):
    238       display += 1
    239     return display
    240 
    241   def _init_child_env(self):
    242     # Create clean environment for new session, so it is cleanly separated from
    243     # the user's console X session.
    244     self.child_env = {}
    245 
    246     for key in [
    247         "HOME",
    248         "LANG",
    249         "LOGNAME",
    250         "PATH",
    251         "SHELL",
    252         "USER",
    253         "USERNAME",
    254         LOG_FILE_ENV_VAR]:
    255       if os.environ.has_key(key):
    256         self.child_env[key] = os.environ[key]
    257 
    258     # Ensure that the software-rendering GL drivers are loaded by the desktop
    259     # session, instead of any hardware GL drivers installed on the system.
    260     self.child_env["LD_LIBRARY_PATH"] = (
    261         "/usr/lib/%(arch)s-linux-gnu/mesa:"
    262         "/usr/lib/%(arch)s-linux-gnu/dri:"
    263         "/usr/lib/%(arch)s-linux-gnu/gallium-pipe" %
    264         { "arch": platform.machine() })
    265 
    266     # Read from /etc/environment if it exists, as it is a standard place to
    267     # store system-wide environment settings. During a normal login, this would
    268     # typically be done by the pam_env PAM module, depending on the local PAM
    269     # configuration.
    270     env_filename = "/etc/environment"
    271     try:
    272       with open(env_filename, "r") as env_file:
    273         for line in env_file:
    274           line = line.rstrip("\n")
    275           # Split at the first "=", leaving any further instances in the value.
    276           key_value_pair = line.split("=", 1)
    277           if len(key_value_pair) == 2:
    278             key, value = tuple(key_value_pair)
    279             # The file stores key=value assignments, but the value may be
    280             # quoted, so strip leading & trailing quotes from it.
    281             value = value.strip("'\"")
    282             self.child_env[key] = value
    283     except IOError:
    284       logging.info("Failed to read %s, skipping." % env_filename)
    285 
    286   def _setup_pulseaudio(self):
    287     self.pulseaudio_pipe = None
    288 
    289     # pulseaudio uses UNIX sockets for communication. Length of UNIX socket
    290     # name is limited to 108 characters, so audio will not work properly if
    291     # the path is too long. To workaround this problem we use only first 10
    292     # symbols of the host hash.
    293     pulse_path = os.path.join(CONFIG_DIR,
    294                               "pulseaudio#%s" % g_host_hash[0:10])
    295     if len(pulse_path) + len("/native") >= 108:
    296       logging.error("Audio will not be enabled because pulseaudio UNIX " +
    297                     "socket path is too long.")
    298       return False
    299 
    300     sink_name = "chrome_remote_desktop_session"
    301     pipe_name = os.path.join(pulse_path, "fifo_output")
    302 
    303     try:
    304       if not os.path.exists(pulse_path):
    305         os.mkdir(pulse_path)
    306     except IOError, e:
    307       logging.error("Failed to create pulseaudio pipe: " + str(e))
    308       return False
    309 
    310     try:
    311       pulse_config = open(os.path.join(pulse_path, "daemon.conf"), "w")
    312       pulse_config.write("default-sample-format = s16le\n")
    313       pulse_config.write("default-sample-rate = 48000\n")
    314       pulse_config.write("default-sample-channels = 2\n")
    315       pulse_config.close()
    316 
    317       pulse_script = open(os.path.join(pulse_path, "default.pa"), "w")
    318       pulse_script.write("load-module module-native-protocol-unix\n")
    319       pulse_script.write(
    320           ("load-module module-pipe-sink sink_name=%s file=\"%s\" " +
    321            "rate=48000 channels=2 format=s16le\n") %
    322           (sink_name, pipe_name))
    323       pulse_script.close()
    324     except IOError, e:
    325       logging.error("Failed to write pulseaudio config: " + str(e))
    326       return False
    327 
    328     self.child_env["PULSE_CONFIG_PATH"] = pulse_path
    329     self.child_env["PULSE_RUNTIME_PATH"] = pulse_path
    330     self.child_env["PULSE_STATE_PATH"] = pulse_path
    331     self.child_env["PULSE_SINK"] = sink_name
    332     self.pulseaudio_pipe = pipe_name
    333 
    334     return True
    335 
    336   def _setup_gnubby(self):
    337     self.ssh_auth_sockname = ("/tmp/chromoting.%s.ssh_auth_sock" %
    338                               os.environ["USER"])
    339 
    340   def _launch_x_server(self, extra_x_args):
    341     x_auth_file = os.path.expanduser("~/.Xauthority")
    342     self.child_env["XAUTHORITY"] = x_auth_file
    343     devnull = open(os.devnull, "rw")
    344     display = self.get_unused_display_number()
    345 
    346     # Run "xauth add" with |child_env| so that it modifies the same XAUTHORITY
    347     # file which will be used for the X session.
    348     ret_code = subprocess.call("xauth add :%d . `mcookie`" % display,
    349                                env=self.child_env, shell=True)
    350     if ret_code != 0:
    351       raise Exception("xauth failed with code %d" % ret_code)
    352 
    353     max_width = max([width for width, height in self.sizes])
    354     max_height = max([height for width, height in self.sizes])
    355 
    356     xvfb = get_randr_supporting_x_server()
    357     if xvfb:
    358       self.server_supports_exact_resize = True
    359     else:
    360       xvfb = "Xvfb"
    361       self.server_supports_exact_resize = False
    362 
    363     # Disable the Composite extension iff the X session is the default
    364     # Unity-2D, since it uses Metacity which fails to generate DAMAGE
    365     # notifications correctly. See crbug.com/166468.
    366     x_session = choose_x_session()
    367     if (len(x_session) == 2 and
    368         x_session[1] == "/usr/bin/gnome-session --session=ubuntu-2d"):
    369       extra_x_args.extend(["-extension", "Composite"])
    370 
    371     logging.info("Starting %s on display :%d" % (xvfb, display))
    372     screen_option = "%dx%dx24" % (max_width, max_height)
    373     self.x_proc = subprocess.Popen(
    374         [xvfb, ":%d" % display,
    375          "-auth", x_auth_file,
    376          "-nolisten", "tcp",
    377          "-noreset",
    378          "-screen", "0", screen_option
    379         ] + extra_x_args)
    380     if not self.x_proc.pid:
    381       raise Exception("Could not start Xvfb.")
    382 
    383     self.child_env["DISPLAY"] = ":%d" % display
    384     self.child_env["CHROME_REMOTE_DESKTOP_SESSION"] = "1"
    385 
    386     # Use a separate profile for any instances of Chrome that are started in
    387     # the virtual session. Chrome doesn't support sharing a profile between
    388     # multiple DISPLAYs, but Chrome Sync allows for a reasonable compromise.
    389     chrome_profile = os.path.join(CONFIG_DIR, "chrome-profile")
    390     self.child_env["CHROME_USER_DATA_DIR"] = chrome_profile
    391 
    392     # Set SSH_AUTH_SOCK to the file name to listen on.
    393     if self.ssh_auth_sockname:
    394       self.child_env["SSH_AUTH_SOCK"] = self.ssh_auth_sockname
    395 
    396     # Wait for X to be active.
    397     for _test in range(5):
    398       proc = subprocess.Popen("xdpyinfo", env=self.child_env, stdout=devnull)
    399       _pid, retcode = os.waitpid(proc.pid, 0)
    400       if retcode == 0:
    401         break
    402       time.sleep(0.5)
    403     if retcode != 0:
    404       raise Exception("Could not connect to Xvfb.")
    405     else:
    406       logging.info("Xvfb is active.")
    407 
    408     # The remoting host expects the server to use "evdev" keycodes, but Xvfb
    409     # starts configured to use the "base" ruleset, resulting in XKB configuring
    410     # for "xfree86" keycodes, and screwing up some keys. See crbug.com/119013.
    411     # Reconfigure the X server to use "evdev" keymap rules.  The X server must
    412     # be started with -noreset otherwise it'll reset as soon as the command
    413     # completes, since there are no other X clients running yet.
    414     proc = subprocess.Popen("setxkbmap -rules evdev", env=self.child_env,
    415                             shell=True)
    416     _pid, retcode = os.waitpid(proc.pid, 0)
    417     if retcode != 0:
    418       logging.error("Failed to set XKB to 'evdev'")
    419 
    420     # Register the screen sizes if the X server's RANDR extension supports it.
    421     # Errors here are non-fatal; the X server will continue to run with the
    422     # dimensions from the "-screen" option.
    423     for width, height in self.sizes:
    424       label = "%dx%d" % (width, height)
    425       args = ["xrandr", "--newmode", label, "0", str(width), "0", "0", "0",
    426               str(height), "0", "0", "0"]
    427       subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
    428       args = ["xrandr", "--addmode", "screen", label]
    429       subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
    430 
    431     # Set the initial mode to the first size specified, otherwise the X server
    432     # would default to (max_width, max_height), which might not even be in the
    433     # list.
    434     label = "%dx%d" % self.sizes[0]
    435     args = ["xrandr", "-s", label]
    436     subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
    437 
    438     # Set the physical size of the display so that the initial mode is running
    439     # at approximately 96 DPI, since some desktops require the DPI to be set to
    440     # something realistic.
    441     args = ["xrandr", "--dpi", "96"]
    442     subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
    443 
    444     devnull.close()
    445 
    446   def _launch_x_session(self):
    447     # Start desktop session.
    448     # The /dev/null input redirection is necessary to prevent the X session
    449     # reading from stdin.  If this code runs as a shell background job in a
    450     # terminal, any reading from stdin causes the job to be suspended.
    451     # Daemonization would solve this problem by separating the process from the
    452     # controlling terminal.
    453     xsession_command = choose_x_session()
    454     if xsession_command is None:
    455       raise Exception("Unable to choose suitable X session command.")
    456 
    457     logging.info("Launching X session: %s" % xsession_command)
    458     self.session_proc = subprocess.Popen(xsession_command,
    459                                          stdin=open(os.devnull, "r"),
    460                                          cwd=HOME_DIR,
    461                                          env=self.child_env)
    462     if not self.session_proc.pid:
    463       raise Exception("Could not start X session")
    464 
    465   def launch_session(self, x_args):
    466     self._init_child_env()
    467     self._setup_pulseaudio()
    468     self._setup_gnubby()
    469     self._launch_x_server(x_args)
    470     self._launch_x_session()
    471 
    472   def launch_host(self, host_config):
    473     # Start remoting host
    474     args = [locate_executable(HOST_BINARY_NAME), "--host-config=-"]
    475     if self.pulseaudio_pipe:
    476       args.append("--audio-pipe-name=%s" % self.pulseaudio_pipe)
    477     if self.server_supports_exact_resize:
    478       args.append("--server-supports-exact-resize")
    479     if self.ssh_auth_sockname:
    480       args.append("--ssh-auth-sockname=%s" % self.ssh_auth_sockname)
    481 
    482     # Have the host process use SIGUSR1 to signal a successful start.
    483     def sigusr1_handler(signum, frame):
    484       _ = signum, frame
    485       logging.info("Host ready to receive connections.")
    486       self.host_ready = True
    487       if (ParentProcessLogger.instance() and
    488           False not in [desktop.host_ready for desktop in g_desktops]):
    489         ParentProcessLogger.instance().release_parent()
    490 
    491     signal.signal(signal.SIGUSR1, sigusr1_handler)
    492     args.append("--signal-parent")
    493 
    494     self.host_proc = subprocess.Popen(args, env=self.child_env,
    495                                       stdin=subprocess.PIPE)
    496     logging.info(args)
    497     if not self.host_proc.pid:
    498       raise Exception("Could not start Chrome Remote Desktop host")
    499     self.host_proc.stdin.write(json.dumps(host_config.data))
    500     self.host_proc.stdin.close()
    501 
    502 
    503 def get_daemon_proc():
    504   """Checks if there is already an instance of this script running, and returns
    505   a psutil.Process instance for it.
    506 
    507   Returns:
    508     A Process instance for the existing daemon process, or None if the daemon
    509     is not running.
    510   """
    511 
    512   uid = os.getuid()
    513   this_pid = os.getpid()
    514 
    515   # Support new & old psutil API. This is the right way to check, according to
    516   # http://grodola.blogspot.com/2014/01/psutil-20-porting.html
    517   if psutil.version_info >= (2, 0):
    518     psget = lambda x: x()
    519   else:
    520     psget = lambda x: x
    521 
    522   for process in psutil.process_iter():
    523     # Skip any processes that raise an exception, as processes may terminate
    524     # during iteration over the list.
    525     try:
    526       # Skip other users' processes.
    527       if psget(process.uids).real != uid:
    528         continue
    529 
    530       # Skip the process for this instance.
    531       if process.pid == this_pid:
    532         continue
    533 
    534       # |cmdline| will be [python-interpreter, script-file, other arguments...]
    535       cmdline = psget(process.cmdline)
    536       if len(cmdline) < 2:
    537         continue
    538       if cmdline[0] == sys.executable and cmdline[1] == sys.argv[0]:
    539         return process
    540     except (psutil.NoSuchProcess, psutil.AccessDenied):
    541       continue
    542 
    543   return None
    544 
    545 
    546 def choose_x_session():
    547   """Chooses the most appropriate X session command for this system.
    548 
    549   Returns:
    550     A string containing the command to run, or a list of strings containing
    551     the executable program and its arguments, which is suitable for passing as
    552     the first parameter of subprocess.Popen().  If a suitable session cannot
    553     be found, returns None.
    554   """
    555   XSESSION_FILES = [
    556     SESSION_FILE_PATH,
    557     SYSTEM_SESSION_FILE_PATH ]
    558   for startup_file in XSESSION_FILES:
    559     startup_file = os.path.expanduser(startup_file)
    560     if os.path.exists(startup_file):
    561       # Use the same logic that a Debian system typically uses with ~/.xsession
    562       # (see /etc/X11/Xsession.d/50x11-common_determine-startup), to determine
    563       # exactly how to run this file.
    564       if os.access(startup_file, os.X_OK):
    565         # "/bin/sh -c" is smart about how to execute the session script and
    566         # works in cases where plain exec() fails (for example, if the file is
    567         # marked executable, but is a plain script with no shebang line).
    568         return ["/bin/sh", "-c", pipes.quote(startup_file)]
    569       else:
    570         shell = os.environ.get("SHELL", "sh")
    571         return [shell, startup_file]
    572 
    573   # Choose a session wrapper script to run the session. On some systems,
    574   # /etc/X11/Xsession fails to load the user's .profile, so look for an
    575   # alternative wrapper that is more likely to match the script that the
    576   # system actually uses for console desktop sessions.
    577   SESSION_WRAPPERS = [
    578     "/usr/sbin/lightdm-session",
    579     "/etc/gdm/Xsession",
    580     "/etc/X11/Xsession" ]
    581   for session_wrapper in SESSION_WRAPPERS:
    582     if os.path.exists(session_wrapper):
    583       if os.path.exists("/usr/bin/unity-2d-panel"):
    584         # On Ubuntu 12.04, the default session relies on 3D-accelerated
    585         # hardware. Trying to run this with a virtual X display produces
    586         # weird results on some systems (for example, upside-down and
    587         # corrupt displays).  So if the ubuntu-2d session is available,
    588         # choose it explicitly.
    589         return [session_wrapper, "/usr/bin/gnome-session --session=ubuntu-2d"]
    590       else:
    591         # Use the session wrapper by itself, and let the system choose a
    592         # session.
    593         return [session_wrapper]
    594   return None
    595 
    596 
    597 def locate_executable(exe_name):
    598   if IS_INSTALLED:
    599     # If the script is running from its installed location, search the host
    600     # binary only in the same directory.
    601     paths_to_try = [ SCRIPT_PATH ]
    602   else:
    603     paths_to_try = map(lambda p: os.path.join(SCRIPT_PATH, p),
    604                        [".", "../../../out/Debug", "../../../out/Release" ])
    605   for path in paths_to_try:
    606     exe_path = os.path.join(path, exe_name)
    607     if os.path.exists(exe_path):
    608       return exe_path
    609 
    610   raise Exception("Could not locate executable '%s'" % exe_name)
    611 
    612 
    613 class ParentProcessLogger(object):
    614   """Redirects logs to the parent process, until the host is ready or quits.
    615 
    616   This class creates a pipe to allow logging from the daemon process to be
    617   copied to the parent process. The daemon process adds a log-handler that
    618   directs logging output to the pipe. The parent process reads from this pipe
    619   until and writes the content to stderr.  When the pipe is no longer needed
    620   (for example, the host signals successful launch or permanent failure), the
    621   daemon removes the log-handler and closes the pipe, causing the the parent
    622   process to reach end-of-file while reading the pipe and exit.
    623 
    624   The (singleton) logger should be instantiated before forking. The parent
    625   process should call wait_for_logs() before exiting. The (grand-)child process
    626   should call start_logging() when it starts, and then use logging.* to issue
    627   log statements, as usual. When the child has either succesfully started the
    628   host or terminated, it must call release_parent() to allow the parent to exit.
    629   """
    630 
    631   __instance = None
    632 
    633   def __init__(self):
    634     """Constructor. Must be called before forking."""
    635     read_pipe, write_pipe = os.pipe()
    636     # Ensure write_pipe is closed on exec, otherwise it will be kept open by
    637     # child processes (X, host), preventing the read pipe from EOF'ing.
    638     old_flags = fcntl.fcntl(write_pipe, fcntl.F_GETFD)
    639     fcntl.fcntl(write_pipe, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
    640     self._read_file = os.fdopen(read_pipe, 'r')
    641     self._write_file = os.fdopen(write_pipe, 'a')
    642     self._logging_handler = None
    643     ParentProcessLogger.__instance = self
    644 
    645   def start_logging(self):
    646     """Installs a logging handler that sends log entries to a pipe.
    647 
    648     Must be called by the child process.
    649     """
    650     self._read_file.close()
    651     self._logging_handler = logging.StreamHandler(self._write_file)
    652     logging.getLogger().addHandler(self._logging_handler)
    653 
    654   def release_parent(self):
    655     """Uninstalls logging handler and closes the pipe, releasing the parent.
    656 
    657     Must be called by the child process.
    658     """
    659     if self._logging_handler:
    660       logging.getLogger().removeHandler(self._logging_handler)
    661       self._logging_handler = None
    662     if not self._write_file.closed:
    663       self._write_file.close()
    664 
    665   def wait_for_logs(self):
    666     """Waits and prints log lines from the daemon until the pipe is closed.
    667 
    668     Must be called by the parent process.
    669     """
    670     # If Ctrl-C is pressed, inform the user that the daemon is still running.
    671     # This signal will cause the read loop below to stop with an EINTR IOError.
    672     def sigint_handler(signum, frame):
    673       _ = signum, frame
    674       print >> sys.stderr, ("Interrupted. The daemon is still running in the "
    675                             "background.")
    676 
    677     signal.signal(signal.SIGINT, sigint_handler)
    678 
    679     # Install a fallback timeout to release the parent process, in case the
    680     # daemon never responds (e.g. host crash-looping, daemon killed).
    681     # This signal will cause the read loop below to stop with an EINTR IOError.
    682     def sigalrm_handler(signum, frame):
    683       _ = signum, frame
    684       print >> sys.stderr, ("No response from daemon. It may have crashed, or "
    685                             "may still be running in the background.")
    686 
    687     signal.signal(signal.SIGALRM, sigalrm_handler)
    688     signal.alarm(30)
    689 
    690     self._write_file.close()
    691 
    692     # Print lines as they're logged to the pipe until EOF is reached or readline
    693     # is interrupted by one of the signal handlers above.
    694     try:
    695       for line in iter(self._read_file.readline, ''):
    696         sys.stderr.write(line)
    697     except IOError as e:
    698       if e.errno != errno.EINTR:
    699         raise
    700     print >> sys.stderr, "Log file: %s" % os.environ[LOG_FILE_ENV_VAR]
    701 
    702   @staticmethod
    703   def instance():
    704     """Returns the singleton instance, if it exists."""
    705     return ParentProcessLogger.__instance
    706 
    707 
    708 def daemonize():
    709   """Background this process and detach from controlling terminal, redirecting
    710   stdout/stderr to a log file."""
    711 
    712   # TODO(lambroslambrou): Having stdout/stderr redirected to a log file is not
    713   # ideal - it could create a filesystem DoS if the daemon or a child process
    714   # were to write excessive amounts to stdout/stderr.  Ideally, stdout/stderr
    715   # should be redirected to a pipe or socket, and a process at the other end
    716   # should consume the data and write it to a logging facility which can do
    717   # data-capping or log-rotation. The 'logger' command-line utility could be
    718   # used for this, but it might cause too much syslog spam.
    719 
    720   # Create new (temporary) file-descriptors before forking, so any errors get
    721   # reported to the main process and set the correct exit-code.
    722   # The mode is provided, since Python otherwise sets a default mode of 0777,
    723   # which would result in the new file having permissions of 0777 & ~umask,
    724   # possibly leaving the executable bits set.
    725   if not os.environ.has_key(LOG_FILE_ENV_VAR):
    726     log_file_prefix = "chrome_remote_desktop_%s_" % time.strftime(
    727         '%Y%m%d_%H%M%S', time.localtime(time.time()))
    728     log_file = tempfile.NamedTemporaryFile(prefix=log_file_prefix, delete=False)
    729     os.environ[LOG_FILE_ENV_VAR] = log_file.name
    730     log_fd = log_file.file.fileno()
    731   else:
    732     log_fd = os.open(os.environ[LOG_FILE_ENV_VAR],
    733                      os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0600)
    734 
    735   devnull_fd = os.open(os.devnull, os.O_RDONLY)
    736 
    737   parent_logger = ParentProcessLogger()
    738 
    739   pid = os.fork()
    740 
    741   if pid == 0:
    742     # Child process
    743     os.setsid()
    744 
    745     # The second fork ensures that the daemon isn't a session leader, so that
    746     # it doesn't acquire a controlling terminal.
    747     pid = os.fork()
    748 
    749     if pid == 0:
    750       # Grandchild process
    751       pass
    752     else:
    753       # Child process
    754       os._exit(0)  # pylint: disable=W0212
    755   else:
    756     # Parent process
    757     parent_logger.wait_for_logs()
    758     os._exit(0)  # pylint: disable=W0212
    759 
    760   logging.info("Daemon process started in the background, logging to '%s'" %
    761                os.environ[LOG_FILE_ENV_VAR])
    762 
    763   os.chdir(HOME_DIR)
    764 
    765   parent_logger.start_logging()
    766 
    767   # Copy the file-descriptors to create new stdin, stdout and stderr.  Note
    768   # that dup2(oldfd, newfd) closes newfd first, so this will close the current
    769   # stdin, stdout and stderr, detaching from the terminal.
    770   os.dup2(devnull_fd, sys.stdin.fileno())
    771   os.dup2(log_fd, sys.stdout.fileno())
    772   os.dup2(log_fd, sys.stderr.fileno())
    773 
    774   # Close the temporary file-descriptors.
    775   os.close(devnull_fd)
    776   os.close(log_fd)
    777 
    778 
    779 def cleanup():
    780   logging.info("Cleanup.")
    781 
    782   global g_desktops
    783   for desktop in g_desktops:
    784     if desktop.x_proc:
    785       logging.info("Terminating Xvfb")
    786       desktop.x_proc.terminate()
    787   g_desktops = []
    788   if ParentProcessLogger.instance():
    789     ParentProcessLogger.instance().release_parent()
    790 
    791 class SignalHandler:
    792   """Reload the config file on SIGHUP. Since we pass the configuration to the
    793   host processes via stdin, they can't reload it, so terminate them. They will
    794   be relaunched automatically with the new config."""
    795 
    796   def __init__(self, host_config):
    797     self.host_config = host_config
    798 
    799   def __call__(self, signum, _stackframe):
    800     if signum == signal.SIGHUP:
    801       logging.info("SIGHUP caught, restarting host.")
    802       try:
    803         self.host_config.load()
    804       except (IOError, ValueError) as e:
    805         logging.error("Failed to load config: " + str(e))
    806       for desktop in g_desktops:
    807         if desktop.host_proc:
    808           desktop.host_proc.send_signal(signal.SIGTERM)
    809     else:
    810       # Exit cleanly so the atexit handler, cleanup(), gets called.
    811       raise SystemExit
    812 
    813 
    814 class RelaunchInhibitor:
    815   """Helper class for inhibiting launch of a child process before a timeout has
    816   elapsed.
    817 
    818   A managed process can be in one of these states:
    819     running, not inhibited (running == True)
    820     stopped and inhibited (running == False and is_inhibited() == True)
    821     stopped but not inhibited (running == False and is_inhibited() == False)
    822 
    823   Attributes:
    824     label: Name of the tracked process. Only used for logging.
    825     running: Whether the process is currently running.
    826     earliest_relaunch_time: Time before which the process should not be
    827       relaunched, or 0 if there is no limit.
    828     failures: The number of times that the process ran for less than a
    829       specified timeout, and had to be inhibited.  This count is reset to 0
    830       whenever the process has run for longer than the timeout.
    831   """
    832 
    833   def __init__(self, label):
    834     self.label = label
    835     self.running = False
    836     self.earliest_relaunch_time = 0
    837     self.earliest_successful_termination = 0
    838     self.failures = 0
    839 
    840   def is_inhibited(self):
    841     return (not self.running) and (time.time() < self.earliest_relaunch_time)
    842 
    843   def record_started(self, minimum_lifetime, relaunch_delay):
    844     """Record that the process was launched, and set the inhibit time to
    845     |timeout| seconds in the future."""
    846     self.earliest_relaunch_time = time.time() + relaunch_delay
    847     self.earliest_successful_termination = time.time() + minimum_lifetime
    848     self.running = True
    849 
    850   def record_stopped(self):
    851     """Record that the process was stopped, and adjust the failure count
    852     depending on whether the process ran long enough."""
    853     self.running = False
    854     if time.time() < self.earliest_successful_termination:
    855       self.failures += 1
    856     else:
    857       self.failures = 0
    858     logging.info("Failure count for '%s' is now %d", self.label, self.failures)
    859 
    860 
    861 def relaunch_self():
    862   cleanup()
    863   os.execvp(sys.argv[0], sys.argv)
    864 
    865 
    866 def waitpid_with_timeout(pid, deadline):
    867   """Wrapper around os.waitpid() which waits until either a child process dies
    868   or the deadline elapses.
    869 
    870   Args:
    871     pid: Process ID to wait for, or -1 to wait for any child process.
    872     deadline: Waiting stops when time.time() exceeds this value.
    873 
    874   Returns:
    875     (pid, status): Same as for os.waitpid(), except that |pid| is 0 if no child
    876     changed state within the timeout.
    877 
    878   Raises:
    879     Same as for os.waitpid().
    880   """
    881   while time.time() < deadline:
    882     pid, status = os.waitpid(pid, os.WNOHANG)
    883     if pid != 0:
    884       return (pid, status)
    885     time.sleep(1)
    886   return (0, 0)
    887 
    888 
    889 def waitpid_handle_exceptions(pid, deadline):
    890   """Wrapper around os.waitpid()/waitpid_with_timeout(), which waits until
    891   either a child process exits or the deadline elapses, and retries if certain
    892   exceptions occur.
    893 
    894   Args:
    895     pid: Process ID to wait for, or -1 to wait for any child process.
    896     deadline: If non-zero, waiting stops when time.time() exceeds this value.
    897       If zero, waiting stops when a child process exits.
    898 
    899   Returns:
    900     (pid, status): Same as for waitpid_with_timeout(). |pid| is non-zero if and
    901     only if a child exited during the wait.
    902 
    903   Raises:
    904     Same as for os.waitpid(), except:
    905       OSError with errno==EINTR causes the wait to be retried (this can happen,
    906       for example, if this parent process receives SIGHUP).
    907       OSError with errno==ECHILD means there are no child processes, and so
    908       this function sleeps until |deadline|. If |deadline| is zero, this is an
    909       error and the OSError exception is raised in this case.
    910   """
    911   while True:
    912     try:
    913       if deadline == 0:
    914         pid_result, status = os.waitpid(pid, 0)
    915       else:
    916         pid_result, status = waitpid_with_timeout(pid, deadline)
    917       return (pid_result, status)
    918     except OSError, e:
    919       if e.errno == errno.EINTR:
    920         continue
    921       elif e.errno == errno.ECHILD:
    922         now = time.time()
    923         if deadline == 0:
    924           # No time-limit and no child processes. This is treated as an error
    925           # (see docstring).
    926           raise
    927         elif deadline > now:
    928           time.sleep(deadline - now)
    929         return (0, 0)
    930       else:
    931         # Anything else is an unexpected error.
    932         raise
    933 
    934 
    935 def main():
    936   EPILOG = """This script is not intended for use by end-users.  To configure
    937 Chrome Remote Desktop, please install the app from the Chrome
    938 Web Store: https://chrome.google.com/remotedesktop"""
    939   parser = optparse.OptionParser(
    940       usage="Usage: %prog [options] [ -- [ X server options ] ]",
    941       epilog=EPILOG)
    942   parser.add_option("-s", "--size", dest="size", action="append",
    943                     help="Dimensions of virtual desktop. This can be specified "
    944                     "multiple times to make multiple screen resolutions "
    945                     "available (if the Xvfb server supports this).")
    946   parser.add_option("-f", "--foreground", dest="foreground", default=False,
    947                     action="store_true",
    948                     help="Don't run as a background daemon.")
    949   parser.add_option("", "--start", dest="start", default=False,
    950                     action="store_true",
    951                     help="Start the host.")
    952   parser.add_option("-k", "--stop", dest="stop", default=False,
    953                     action="store_true",
    954                     help="Stop the daemon currently running.")
    955   parser.add_option("", "--get-status", dest="get_status", default=False,
    956                     action="store_true",
    957                     help="Prints host status")
    958   parser.add_option("", "--check-running", dest="check_running", default=False,
    959                     action="store_true",
    960                     help="Return 0 if the daemon is running, or 1 otherwise.")
    961   parser.add_option("", "--config", dest="config", action="store",
    962                     help="Use the specified configuration file.")
    963   parser.add_option("", "--reload", dest="reload", default=False,
    964                     action="store_true",
    965                     help="Signal currently running host to reload the config.")
    966   parser.add_option("", "--add-user", dest="add_user", default=False,
    967                     action="store_true",
    968                     help="Add current user to the chrome-remote-desktop group.")
    969   parser.add_option("", "--host-version", dest="host_version", default=False,
    970                     action="store_true",
    971                     help="Prints version of the host.")
    972   (options, args) = parser.parse_args()
    973 
    974   # Determine the filename of the host configuration and PID files.
    975   if not options.config:
    976     options.config = os.path.join(CONFIG_DIR, "host#%s.json" % g_host_hash)
    977 
    978   # Check for a modal command-line option (start, stop, etc.)
    979 
    980   if options.get_status:
    981     proc = get_daemon_proc()
    982     if proc is not None:
    983       print "STARTED"
    984     elif is_supported_platform():
    985       print "STOPPED"
    986     else:
    987       print "NOT_IMPLEMENTED"
    988     return 0
    989 
    990   # TODO(sergeyu): Remove --check-running once NPAPI plugin and NM host are
    991   # updated to always use get-status flag instead.
    992   if options.check_running:
    993     proc = get_daemon_proc()
    994     return 1 if proc is None else 0
    995 
    996   if options.stop:
    997     proc = get_daemon_proc()
    998     if proc is None:
    999       print "The daemon is not currently running"
   1000     else:
   1001       print "Killing process %s" % proc.pid
   1002       proc.terminate()
   1003       try:
   1004         proc.wait(timeout=30)
   1005       except psutil.TimeoutExpired:
   1006         print "Timed out trying to kill daemon process"
   1007         return 1
   1008     return 0
   1009 
   1010   if options.reload:
   1011     proc = get_daemon_proc()
   1012     if proc is None:
   1013       return 1
   1014     proc.send_signal(signal.SIGHUP)
   1015     return 0
   1016 
   1017   if options.add_user:
   1018     user = getpass.getuser()
   1019     try:
   1020       if user in grp.getgrnam(CHROME_REMOTING_GROUP_NAME).gr_mem:
   1021         logging.info("User '%s' is already a member of '%s'." %
   1022                      (user, CHROME_REMOTING_GROUP_NAME))
   1023         return 0
   1024     except KeyError:
   1025       logging.info("Group '%s' not found." % CHROME_REMOTING_GROUP_NAME)
   1026 
   1027     if os.getenv("DISPLAY"):
   1028       sudo_command = "gksudo --description \"Chrome Remote Desktop\""
   1029     else:
   1030       sudo_command = "sudo"
   1031     command = ("sudo -k && exec %(sudo)s -- sh -c "
   1032                "\"groupadd -f %(group)s && gpasswd --add %(user)s %(group)s\"" %
   1033                { 'group': CHROME_REMOTING_GROUP_NAME,
   1034                  'user': user,
   1035                  'sudo': sudo_command })
   1036     os.execv("/bin/sh", ["/bin/sh", "-c", command])
   1037     return 1
   1038 
   1039   if options.host_version:
   1040     # TODO(sergeyu): Also check RPM package version once we add RPM package.
   1041     return os.system(locate_executable(HOST_BINARY_NAME) + " --version") >> 8
   1042 
   1043   if not options.start:
   1044     # If no modal command-line options specified, print an error and exit.
   1045     print >> sys.stderr, EPILOG
   1046     return 1
   1047 
   1048   # If a RANDR-supporting Xvfb is not available, limit the default size to
   1049   # something more sensible.
   1050   if get_randr_supporting_x_server():
   1051     default_sizes = DEFAULT_SIZES
   1052   else:
   1053     default_sizes = DEFAULT_SIZE_NO_RANDR
   1054 
   1055   # Collate the list of sizes that XRANDR should support.
   1056   if not options.size:
   1057     if os.environ.has_key(DEFAULT_SIZES_ENV_VAR):
   1058       default_sizes = os.environ[DEFAULT_SIZES_ENV_VAR]
   1059     options.size = default_sizes.split(",")
   1060 
   1061   sizes = []
   1062   for size in options.size:
   1063     size_components = size.split("x")
   1064     if len(size_components) != 2:
   1065       parser.error("Incorrect size format '%s', should be WIDTHxHEIGHT" % size)
   1066 
   1067     try:
   1068       width = int(size_components[0])
   1069       height = int(size_components[1])
   1070 
   1071       # Enforce minimum desktop size, as a sanity-check.  The limit of 100 will
   1072       # detect typos of 2 instead of 3 digits.
   1073       if width < 100 or height < 100:
   1074         raise ValueError
   1075     except ValueError:
   1076       parser.error("Width and height should be 100 pixels or greater")
   1077 
   1078     sizes.append((width, height))
   1079 
   1080   # Register an exit handler to clean up session process and the PID file.
   1081   atexit.register(cleanup)
   1082 
   1083   # Load the initial host configuration.
   1084   host_config = Config(options.config)
   1085   try:
   1086     host_config.load()
   1087   except (IOError, ValueError) as e:
   1088     print >> sys.stderr, "Failed to load config: " + str(e)
   1089     return 1
   1090 
   1091   # Register handler to re-load the configuration in response to signals.
   1092   for s in [signal.SIGHUP, signal.SIGINT, signal.SIGTERM]:
   1093     signal.signal(s, SignalHandler(host_config))
   1094 
   1095   # Verify that the initial host configuration has the necessary fields.
   1096   auth = Authentication()
   1097   auth_config_valid = auth.copy_from(host_config)
   1098   host = Host()
   1099   host_config_valid = host.copy_from(host_config)
   1100   if not host_config_valid or not auth_config_valid:
   1101     logging.error("Failed to load host configuration.")
   1102     return 1
   1103 
   1104   # Determine whether a desktop is already active for the specified host
   1105   # host configuration.
   1106   proc = get_daemon_proc()
   1107   if proc is not None:
   1108     # Debian policy requires that services should "start" cleanly and return 0
   1109     # if they are already running.
   1110     print "Service already running."
   1111     return 0
   1112 
   1113   # Detach a separate "daemon" process to run the session, unless specifically
   1114   # requested to run in the foreground.
   1115   if not options.foreground:
   1116     daemonize()
   1117 
   1118   logging.info("Using host_id: " + host.host_id)
   1119 
   1120   desktop = Desktop(sizes)
   1121 
   1122   # Keep track of the number of consecutive failures of any child process to
   1123   # run for longer than a set period of time. The script will exit after a
   1124   # threshold is exceeded.
   1125   # There is no point in tracking the X session process separately, since it is
   1126   # launched at (roughly) the same time as the X server, and the termination of
   1127   # one of these triggers the termination of the other.
   1128   x_server_inhibitor = RelaunchInhibitor("X server")
   1129   host_inhibitor = RelaunchInhibitor("host")
   1130   all_inhibitors = [x_server_inhibitor, host_inhibitor]
   1131 
   1132   # Don't allow relaunching the script on the first loop iteration.
   1133   allow_relaunch_self = False
   1134 
   1135   while True:
   1136     # Set the backoff interval and exit if a process failed too many times.
   1137     backoff_time = SHORT_BACKOFF_TIME
   1138     for inhibitor in all_inhibitors:
   1139       if inhibitor.failures >= MAX_LAUNCH_FAILURES:
   1140         logging.error("Too many launch failures of '%s', exiting."
   1141                       % inhibitor.label)
   1142         return 1
   1143       elif inhibitor.failures >= SHORT_BACKOFF_THRESHOLD:
   1144         backoff_time = LONG_BACKOFF_TIME
   1145 
   1146     relaunch_times = []
   1147 
   1148     # If the session process or X server stops running (e.g. because the user
   1149     # logged out), kill the other. This will trigger the next conditional block
   1150     # as soon as os.waitpid() reaps its exit-code.
   1151     if desktop.session_proc is None and desktop.x_proc is not None:
   1152       logging.info("Terminating X server")
   1153       desktop.x_proc.terminate()
   1154     elif desktop.x_proc is None and desktop.session_proc is not None:
   1155       logging.info("Terminating X session")
   1156       desktop.session_proc.terminate()
   1157     elif desktop.x_proc is None and desktop.session_proc is None:
   1158       # Both processes have terminated.
   1159       if (allow_relaunch_self and x_server_inhibitor.failures == 0 and
   1160           host_inhibitor.failures == 0):
   1161         # Since the user's desktop is already gone at this point, there's no
   1162         # state to lose and now is a good time to pick up any updates to this
   1163         # script that might have been installed.
   1164         logging.info("Relaunching self")
   1165         relaunch_self()
   1166       else:
   1167         # If there is a non-zero |failures| count, restarting the whole script
   1168         # would lose this information, so just launch the session as normal.
   1169         if x_server_inhibitor.is_inhibited():
   1170           logging.info("Waiting before launching X server")
   1171           relaunch_times.append(x_server_inhibitor.earliest_relaunch_time)
   1172         else:
   1173           logging.info("Launching X server and X session.")
   1174           desktop.launch_session(args)
   1175           x_server_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME,
   1176                                             backoff_time)
   1177           allow_relaunch_self = True
   1178 
   1179     if desktop.host_proc is None:
   1180       if host_inhibitor.is_inhibited():
   1181         logging.info("Waiting before launching host process")
   1182         relaunch_times.append(host_inhibitor.earliest_relaunch_time)
   1183       else:
   1184         logging.info("Launching host process")
   1185         desktop.launch_host(host_config)
   1186         host_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME,
   1187                                       backoff_time)
   1188 
   1189     deadline = min(relaunch_times) if relaunch_times else 0
   1190     pid, status = waitpid_handle_exceptions(-1, deadline)
   1191     if pid == 0:
   1192       continue
   1193 
   1194     logging.info("wait() returned (%s,%s)" % (pid, status))
   1195 
   1196     # When a process has terminated, and we've reaped its exit-code, any Popen
   1197     # instance for that process is no longer valid. Reset any affected instance
   1198     # to None.
   1199     if desktop.x_proc is not None and pid == desktop.x_proc.pid:
   1200       logging.info("X server process terminated")
   1201       desktop.x_proc = None
   1202       x_server_inhibitor.record_stopped()
   1203 
   1204     if desktop.session_proc is not None and pid == desktop.session_proc.pid:
   1205       logging.info("Session process terminated")
   1206       desktop.session_proc = None
   1207 
   1208     if desktop.host_proc is not None and pid == desktop.host_proc.pid:
   1209       logging.info("Host process terminated")
   1210       desktop.host_proc = None
   1211       desktop.host_ready = False
   1212       host_inhibitor.record_stopped()
   1213 
   1214       # These exit-codes must match the ones used by the host.
   1215       # See remoting/host/host_error_codes.h.
   1216       # Delete the host or auth configuration depending on the returned error
   1217       # code, so the next time this script is run, a new configuration
   1218       # will be created and registered.
   1219       if os.WIFEXITED(status):
   1220         if os.WEXITSTATUS(status) == 100:
   1221           logging.info("Host configuration is invalid - exiting.")
   1222           return 0
   1223         elif os.WEXITSTATUS(status) == 101:
   1224           logging.info("Host ID has been deleted - exiting.")
   1225           host_config.clear()
   1226           host_config.save_and_log_errors()
   1227           return 0
   1228         elif os.WEXITSTATUS(status) == 102:
   1229           logging.info("OAuth credentials are invalid - exiting.")
   1230           return 0
   1231         elif os.WEXITSTATUS(status) == 103:
   1232           logging.info("Host domain is blocked by policy - exiting.")
   1233           return 0
   1234         # Nothing to do for Mac-only status 104 (login screen unsupported)
   1235         elif os.WEXITSTATUS(status) == 105:
   1236           logging.info("Username is blocked by policy - exiting.")
   1237           return 0
   1238         else:
   1239           logging.info("Host exited with status %s." % os.WEXITSTATUS(status))
   1240       elif os.WIFSIGNALED(status):
   1241         logging.info("Host terminated by signal %s." % os.WTERMSIG(status))
   1242 
   1243 
   1244 if __name__ == "__main__":
   1245   logging.basicConfig(level=logging.DEBUG,
   1246                       format="%(asctime)s:%(levelname)s:%(message)s")
   1247   sys.exit(main())
   1248