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