Roll src/third_party/WebKit f36d5e0:68b67cd (svn 193299:193303)
[chromium-blink-merge.git] / remoting / host / linux / linux_me2me_host.py
blobb615e9523fdf50a2340c02615e0502e21abb353f
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 self.login = None
178 self.oauth_refresh_token = None
180 def copy_from(self, config):
181 """Loads the config and returns false if the config is invalid."""
182 try:
183 self.login = config["xmpp_login"]
184 self.oauth_refresh_token = config["oauth_refresh_token"]
185 except KeyError:
186 return False
187 return True
189 def copy_to(self, config):
190 config["xmpp_login"] = self.login
191 config["oauth_refresh_token"] = self.oauth_refresh_token
194 class Host:
195 """This manages the configuration for a host."""
197 def __init__(self):
198 self.host_id = str(uuid.uuid1())
199 self.host_name = socket.gethostname()
200 self.host_secret_hash = None
201 self.private_key = None
203 def copy_from(self, config):
204 try:
205 self.host_id = config["host_id"]
206 self.host_name = config["host_name"]
207 self.host_secret_hash = config.get("host_secret_hash")
208 self.private_key = config["private_key"]
209 except KeyError:
210 return False
211 return True
213 def copy_to(self, config):
214 config["host_id"] = self.host_id
215 config["host_name"] = self.host_name
216 config["host_secret_hash"] = self.host_secret_hash
217 config["private_key"] = self.private_key
220 class Desktop:
221 """Manage a single virtual desktop"""
223 def __init__(self, sizes):
224 self.x_proc = None
225 self.session_proc = None
226 self.host_proc = None
227 self.child_env = None
228 self.sizes = sizes
229 self.pulseaudio_pipe = None
230 self.server_supports_exact_resize = False
231 self.host_ready = False
232 self.ssh_auth_sockname = None
233 g_desktops.append(self)
235 @staticmethod
236 def get_unused_display_number():
237 """Return a candidate display number for which there is currently no
238 X Server lock file"""
239 display = FIRST_X_DISPLAY_NUMBER
240 while os.path.exists(X_LOCK_FILE_TEMPLATE % display):
241 display += 1
242 return display
244 def _init_child_env(self):
245 # Create clean environment for new session, so it is cleanly separated from
246 # the user's console X session.
247 self.child_env = {}
249 for key in [
250 "HOME",
251 "LANG",
252 "LOGNAME",
253 "PATH",
254 "SHELL",
255 "USER",
256 "USERNAME",
257 LOG_FILE_ENV_VAR]:
258 if os.environ.has_key(key):
259 self.child_env[key] = os.environ[key]
261 # Ensure that the software-rendering GL drivers are loaded by the desktop
262 # session, instead of any hardware GL drivers installed on the system.
263 self.child_env["LD_LIBRARY_PATH"] = (
264 "/usr/lib/%(arch)s-linux-gnu/mesa:"
265 "/usr/lib/%(arch)s-linux-gnu/dri:"
266 "/usr/lib/%(arch)s-linux-gnu/gallium-pipe" %
267 { "arch": platform.machine() })
269 # Read from /etc/environment if it exists, as it is a standard place to
270 # store system-wide environment settings. During a normal login, this would
271 # typically be done by the pam_env PAM module, depending on the local PAM
272 # configuration.
273 env_filename = "/etc/environment"
274 try:
275 with open(env_filename, "r") as env_file:
276 for line in env_file:
277 line = line.rstrip("\n")
278 # Split at the first "=", leaving any further instances in the value.
279 key_value_pair = line.split("=", 1)
280 if len(key_value_pair) == 2:
281 key, value = tuple(key_value_pair)
282 # The file stores key=value assignments, but the value may be
283 # quoted, so strip leading & trailing quotes from it.
284 value = value.strip("'\"")
285 self.child_env[key] = value
286 except IOError:
287 logging.info("Failed to read %s, skipping." % env_filename)
289 def _setup_pulseaudio(self):
290 self.pulseaudio_pipe = None
292 # pulseaudio uses UNIX sockets for communication. Length of UNIX socket
293 # name is limited to 108 characters, so audio will not work properly if
294 # the path is too long. To workaround this problem we use only first 10
295 # symbols of the host hash.
296 pulse_path = os.path.join(CONFIG_DIR,
297 "pulseaudio#%s" % g_host_hash[0:10])
298 if len(pulse_path) + len("/native") >= 108:
299 logging.error("Audio will not be enabled because pulseaudio UNIX " +
300 "socket path is too long.")
301 return False
303 sink_name = "chrome_remote_desktop_session"
304 pipe_name = os.path.join(pulse_path, "fifo_output")
306 try:
307 if not os.path.exists(pulse_path):
308 os.mkdir(pulse_path)
309 except IOError, e:
310 logging.error("Failed to create pulseaudio pipe: " + str(e))
311 return False
313 try:
314 pulse_config = open(os.path.join(pulse_path, "daemon.conf"), "w")
315 pulse_config.write("default-sample-format = s16le\n")
316 pulse_config.write("default-sample-rate = 48000\n")
317 pulse_config.write("default-sample-channels = 2\n")
318 pulse_config.close()
320 pulse_script = open(os.path.join(pulse_path, "default.pa"), "w")
321 pulse_script.write("load-module module-native-protocol-unix\n")
322 pulse_script.write(
323 ("load-module module-pipe-sink sink_name=%s file=\"%s\" " +
324 "rate=48000 channels=2 format=s16le\n") %
325 (sink_name, pipe_name))
326 pulse_script.close()
327 except IOError, e:
328 logging.error("Failed to write pulseaudio config: " + str(e))
329 return False
331 self.child_env["PULSE_CONFIG_PATH"] = pulse_path
332 self.child_env["PULSE_RUNTIME_PATH"] = pulse_path
333 self.child_env["PULSE_STATE_PATH"] = pulse_path
334 self.child_env["PULSE_SINK"] = sink_name
335 self.pulseaudio_pipe = pipe_name
337 return True
339 def _setup_gnubby(self):
340 self.ssh_auth_sockname = ("/tmp/chromoting.%s.ssh_auth_sock" %
341 os.environ["USER"])
343 def _launch_x_server(self, extra_x_args):
344 x_auth_file = os.path.expanduser("~/.Xauthority")
345 self.child_env["XAUTHORITY"] = x_auth_file
346 devnull = open(os.devnull, "rw")
347 display = self.get_unused_display_number()
349 # Run "xauth add" with |child_env| so that it modifies the same XAUTHORITY
350 # file which will be used for the X session.
351 ret_code = subprocess.call("xauth add :%d . `mcookie`" % display,
352 env=self.child_env, shell=True)
353 if ret_code != 0:
354 raise Exception("xauth failed with code %d" % ret_code)
356 max_width = max([width for width, height in self.sizes])
357 max_height = max([height for width, height in self.sizes])
359 xvfb = get_randr_supporting_x_server()
360 if xvfb:
361 self.server_supports_exact_resize = True
362 else:
363 xvfb = "Xvfb"
364 self.server_supports_exact_resize = False
366 # Disable the Composite extension iff the X session is the default
367 # Unity-2D, since it uses Metacity which fails to generate DAMAGE
368 # notifications correctly. See crbug.com/166468.
369 x_session = choose_x_session()
370 if (len(x_session) == 2 and
371 x_session[1] == "/usr/bin/gnome-session --session=ubuntu-2d"):
372 extra_x_args.extend(["-extension", "Composite"])
374 logging.info("Starting %s on display :%d" % (xvfb, display))
375 screen_option = "%dx%dx24" % (max_width, max_height)
376 self.x_proc = subprocess.Popen(
377 [xvfb, ":%d" % display,
378 "-auth", x_auth_file,
379 "-nolisten", "tcp",
380 "-noreset",
381 "-screen", "0", screen_option
382 ] + extra_x_args)
383 if not self.x_proc.pid:
384 raise Exception("Could not start Xvfb.")
386 self.child_env["DISPLAY"] = ":%d" % display
387 self.child_env["CHROME_REMOTE_DESKTOP_SESSION"] = "1"
389 # Use a separate profile for any instances of Chrome that are started in
390 # the virtual session. Chrome doesn't support sharing a profile between
391 # multiple DISPLAYs, but Chrome Sync allows for a reasonable compromise.
392 chrome_profile = os.path.join(CONFIG_DIR, "chrome-profile")
393 self.child_env["CHROME_USER_DATA_DIR"] = chrome_profile
395 # Set SSH_AUTH_SOCK to the file name to listen on.
396 if self.ssh_auth_sockname:
397 self.child_env["SSH_AUTH_SOCK"] = self.ssh_auth_sockname
399 # Wait for X to be active.
400 for _test in range(20):
401 retcode = subprocess.call("xdpyinfo", env=self.child_env, stdout=devnull)
402 if retcode == 0:
403 break
404 time.sleep(0.5)
405 if retcode != 0:
406 raise Exception("Could not connect to Xvfb.")
407 else:
408 logging.info("Xvfb is active.")
410 # The remoting host expects the server to use "evdev" keycodes, but Xvfb
411 # starts configured to use the "base" ruleset, resulting in XKB configuring
412 # for "xfree86" keycodes, and screwing up some keys. See crbug.com/119013.
413 # Reconfigure the X server to use "evdev" keymap rules. The X server must
414 # be started with -noreset otherwise it'll reset as soon as the command
415 # completes, since there are no other X clients running yet.
416 retcode = subprocess.call("setxkbmap -rules evdev", env=self.child_env,
417 shell=True)
418 if retcode != 0:
419 logging.error("Failed to set XKB to 'evdev'")
421 if not self.server_supports_exact_resize:
422 return
424 # Register the screen sizes if the X server's RANDR extension supports it.
425 # Errors here are non-fatal; the X server will continue to run with the
426 # dimensions from the "-screen" option.
427 for width, height in self.sizes:
428 label = "%dx%d" % (width, height)
429 args = ["xrandr", "--newmode", label, "0", str(width), "0", "0", "0",
430 str(height), "0", "0", "0"]
431 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
432 args = ["xrandr", "--addmode", "screen", label]
433 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
435 # Set the initial mode to the first size specified, otherwise the X server
436 # would default to (max_width, max_height), which might not even be in the
437 # list.
438 initial_size = self.sizes[0]
439 label = "%dx%d" % initial_size
440 args = ["xrandr", "-s", label]
441 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
443 # Set the physical size of the display so that the initial mode is running
444 # at approximately 96 DPI, since some desktops require the DPI to be set to
445 # something realistic.
446 args = ["xrandr", "--dpi", "96"]
447 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
449 # Monitor for any automatic resolution changes from the desktop environment.
450 args = [SCRIPT_PATH, "--watch-resolution", str(initial_size[0]),
451 str(initial_size[1])]
453 # It is not necessary to wait() on the process here, as this script's main
454 # loop will reap the exit-codes of all child processes.
455 subprocess.Popen(args, env=self.child_env, stdout=devnull, stderr=devnull)
457 devnull.close()
459 def _launch_x_session(self):
460 # Start desktop session.
461 # The /dev/null input redirection is necessary to prevent the X session
462 # reading from stdin. If this code runs as a shell background job in a
463 # terminal, any reading from stdin causes the job to be suspended.
464 # Daemonization would solve this problem by separating the process from the
465 # controlling terminal.
466 xsession_command = choose_x_session()
467 if xsession_command is None:
468 raise Exception("Unable to choose suitable X session command.")
470 logging.info("Launching X session: %s" % xsession_command)
471 self.session_proc = subprocess.Popen(xsession_command,
472 stdin=open(os.devnull, "r"),
473 cwd=HOME_DIR,
474 env=self.child_env)
475 if not self.session_proc.pid:
476 raise Exception("Could not start X session")
478 def launch_session(self, x_args):
479 self._init_child_env()
480 self._setup_pulseaudio()
481 self._setup_gnubby()
482 self._launch_x_server(x_args)
483 self._launch_x_session()
485 def launch_host(self, host_config):
486 # Start remoting host
487 args = [locate_executable(HOST_BINARY_NAME), "--host-config=-"]
488 if self.pulseaudio_pipe:
489 args.append("--audio-pipe-name=%s" % self.pulseaudio_pipe)
490 if self.server_supports_exact_resize:
491 args.append("--server-supports-exact-resize")
492 if self.ssh_auth_sockname:
493 args.append("--ssh-auth-sockname=%s" % self.ssh_auth_sockname)
495 # Have the host process use SIGUSR1 to signal a successful start.
496 def sigusr1_handler(signum, frame):
497 _ = signum, frame
498 logging.info("Host ready to receive connections.")
499 self.host_ready = True
500 if (ParentProcessLogger.instance() and
501 False not in [desktop.host_ready for desktop in g_desktops]):
502 ParentProcessLogger.instance().release_parent()
504 signal.signal(signal.SIGUSR1, sigusr1_handler)
505 args.append("--signal-parent")
507 self.host_proc = subprocess.Popen(args, env=self.child_env,
508 stdin=subprocess.PIPE)
509 logging.info(args)
510 if not self.host_proc.pid:
511 raise Exception("Could not start Chrome Remote Desktop host")
512 self.host_proc.stdin.write(json.dumps(host_config.data))
513 self.host_proc.stdin.close()
516 def get_daemon_proc():
517 """Checks if there is already an instance of this script running, and returns
518 a psutil.Process instance for it.
520 Returns:
521 A Process instance for the existing daemon process, or None if the daemon
522 is not running.
525 uid = os.getuid()
526 this_pid = os.getpid()
528 # Support new & old psutil API. This is the right way to check, according to
529 # http://grodola.blogspot.com/2014/01/psutil-20-porting.html
530 if psutil.version_info >= (2, 0):
531 psget = lambda x: x()
532 else:
533 psget = lambda x: x
535 for process in psutil.process_iter():
536 # Skip any processes that raise an exception, as processes may terminate
537 # during iteration over the list.
538 try:
539 # Skip other users' processes.
540 if psget(process.uids).real != uid:
541 continue
543 # Skip the process for this instance.
544 if process.pid == this_pid:
545 continue
547 # |cmdline| will be [python-interpreter, script-file, other arguments...]
548 cmdline = psget(process.cmdline)
549 if len(cmdline) < 2:
550 continue
551 if cmdline[0] == sys.executable and cmdline[1] == sys.argv[0]:
552 return process
553 except (psutil.NoSuchProcess, psutil.AccessDenied):
554 continue
556 return None
559 def choose_x_session():
560 """Chooses the most appropriate X session command for this system.
562 Returns:
563 A string containing the command to run, or a list of strings containing
564 the executable program and its arguments, which is suitable for passing as
565 the first parameter of subprocess.Popen(). If a suitable session cannot
566 be found, returns None.
568 XSESSION_FILES = [
569 SESSION_FILE_PATH,
570 SYSTEM_SESSION_FILE_PATH ]
571 for startup_file in XSESSION_FILES:
572 startup_file = os.path.expanduser(startup_file)
573 if os.path.exists(startup_file):
574 if os.access(startup_file, os.X_OK):
575 # "/bin/sh -c" is smart about how to execute the session script and
576 # works in cases where plain exec() fails (for example, if the file is
577 # marked executable, but is a plain script with no shebang line).
578 return ["/bin/sh", "-c", pipes.quote(startup_file)]
579 else:
580 # If this is a system-wide session script, it should be run using the
581 # system shell, ignoring any login shell that might be set for the
582 # current user.
583 return ["/bin/sh", startup_file]
585 # Choose a session wrapper script to run the session. On some systems,
586 # /etc/X11/Xsession fails to load the user's .profile, so look for an
587 # alternative wrapper that is more likely to match the script that the
588 # system actually uses for console desktop sessions.
589 SESSION_WRAPPERS = [
590 "/usr/sbin/lightdm-session",
591 "/etc/gdm/Xsession",
592 "/etc/X11/Xsession" ]
593 for session_wrapper in SESSION_WRAPPERS:
594 if os.path.exists(session_wrapper):
595 if os.path.exists("/usr/bin/unity-2d-panel"):
596 # On Ubuntu 12.04, the default session relies on 3D-accelerated
597 # hardware. Trying to run this with a virtual X display produces
598 # weird results on some systems (for example, upside-down and
599 # corrupt displays). So if the ubuntu-2d session is available,
600 # choose it explicitly.
601 return [session_wrapper, "/usr/bin/gnome-session --session=ubuntu-2d"]
602 else:
603 # Use the session wrapper by itself, and let the system choose a
604 # session.
605 return [session_wrapper]
606 return None
609 def locate_executable(exe_name):
610 if IS_INSTALLED:
611 # If the script is running from its installed location, search the host
612 # binary only in the same directory.
613 paths_to_try = [ SCRIPT_DIR ]
614 else:
615 paths_to_try = map(lambda p: os.path.join(SCRIPT_DIR, p),
616 [".", "../../../out/Debug", "../../../out/Release" ])
617 for path in paths_to_try:
618 exe_path = os.path.join(path, exe_name)
619 if os.path.exists(exe_path):
620 return exe_path
622 raise Exception("Could not locate executable '%s'" % exe_name)
625 class ParentProcessLogger(object):
626 """Redirects logs to the parent process, until the host is ready or quits.
628 This class creates a pipe to allow logging from the daemon process to be
629 copied to the parent process. The daemon process adds a log-handler that
630 directs logging output to the pipe. The parent process reads from this pipe
631 until and writes the content to stderr. When the pipe is no longer needed
632 (for example, the host signals successful launch or permanent failure), the
633 daemon removes the log-handler and closes the pipe, causing the the parent
634 process to reach end-of-file while reading the pipe and exit.
636 The (singleton) logger should be instantiated before forking. The parent
637 process should call wait_for_logs() before exiting. The (grand-)child process
638 should call start_logging() when it starts, and then use logging.* to issue
639 log statements, as usual. When the child has either succesfully started the
640 host or terminated, it must call release_parent() to allow the parent to exit.
643 __instance = None
645 def __init__(self):
646 """Constructor. Must be called before forking."""
647 read_pipe, write_pipe = os.pipe()
648 # Ensure write_pipe is closed on exec, otherwise it will be kept open by
649 # child processes (X, host), preventing the read pipe from EOF'ing.
650 old_flags = fcntl.fcntl(write_pipe, fcntl.F_GETFD)
651 fcntl.fcntl(write_pipe, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
652 self._read_file = os.fdopen(read_pipe, 'r')
653 self._write_file = os.fdopen(write_pipe, 'a')
654 self._logging_handler = None
655 ParentProcessLogger.__instance = self
657 def start_logging(self):
658 """Installs a logging handler that sends log entries to a pipe.
660 Must be called by the child process.
662 self._read_file.close()
663 self._logging_handler = logging.StreamHandler(self._write_file)
664 logging.getLogger().addHandler(self._logging_handler)
666 def release_parent(self):
667 """Uninstalls logging handler and closes the pipe, releasing the parent.
669 Must be called by the child process.
671 if self._logging_handler:
672 logging.getLogger().removeHandler(self._logging_handler)
673 self._logging_handler = None
674 if not self._write_file.closed:
675 self._write_file.close()
677 def wait_for_logs(self):
678 """Waits and prints log lines from the daemon until the pipe is closed.
680 Must be called by the parent process.
682 # If Ctrl-C is pressed, inform the user that the daemon is still running.
683 # This signal will cause the read loop below to stop with an EINTR IOError.
684 def sigint_handler(signum, frame):
685 _ = signum, frame
686 print >> sys.stderr, ("Interrupted. The daemon is still running in the "
687 "background.")
689 signal.signal(signal.SIGINT, sigint_handler)
691 # Install a fallback timeout to release the parent process, in case the
692 # daemon never responds (e.g. host crash-looping, daemon killed).
693 # This signal will cause the read loop below to stop with an EINTR IOError.
694 def sigalrm_handler(signum, frame):
695 _ = signum, frame
696 print >> sys.stderr, ("No response from daemon. It may have crashed, or "
697 "may still be running in the background.")
699 signal.signal(signal.SIGALRM, sigalrm_handler)
700 signal.alarm(30)
702 self._write_file.close()
704 # Print lines as they're logged to the pipe until EOF is reached or readline
705 # is interrupted by one of the signal handlers above.
706 try:
707 for line in iter(self._read_file.readline, ''):
708 sys.stderr.write(line)
709 except IOError as e:
710 if e.errno != errno.EINTR:
711 raise
712 print >> sys.stderr, "Log file: %s" % os.environ[LOG_FILE_ENV_VAR]
714 @staticmethod
715 def instance():
716 """Returns the singleton instance, if it exists."""
717 return ParentProcessLogger.__instance
720 def daemonize():
721 """Background this process and detach from controlling terminal, redirecting
722 stdout/stderr to a log file."""
724 # TODO(lambroslambrou): Having stdout/stderr redirected to a log file is not
725 # ideal - it could create a filesystem DoS if the daemon or a child process
726 # were to write excessive amounts to stdout/stderr. Ideally, stdout/stderr
727 # should be redirected to a pipe or socket, and a process at the other end
728 # should consume the data and write it to a logging facility which can do
729 # data-capping or log-rotation. The 'logger' command-line utility could be
730 # used for this, but it might cause too much syslog spam.
732 # Create new (temporary) file-descriptors before forking, so any errors get
733 # reported to the main process and set the correct exit-code.
734 # The mode is provided, since Python otherwise sets a default mode of 0777,
735 # which would result in the new file having permissions of 0777 & ~umask,
736 # possibly leaving the executable bits set.
737 if not os.environ.has_key(LOG_FILE_ENV_VAR):
738 log_file_prefix = "chrome_remote_desktop_%s_" % time.strftime(
739 '%Y%m%d_%H%M%S', time.localtime(time.time()))
740 log_file = tempfile.NamedTemporaryFile(prefix=log_file_prefix, delete=False)
741 os.environ[LOG_FILE_ENV_VAR] = log_file.name
742 log_fd = log_file.file.fileno()
743 else:
744 log_fd = os.open(os.environ[LOG_FILE_ENV_VAR],
745 os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0600)
747 devnull_fd = os.open(os.devnull, os.O_RDONLY)
749 parent_logger = ParentProcessLogger()
751 pid = os.fork()
753 if pid == 0:
754 # Child process
755 os.setsid()
757 # The second fork ensures that the daemon isn't a session leader, so that
758 # it doesn't acquire a controlling terminal.
759 pid = os.fork()
761 if pid == 0:
762 # Grandchild process
763 pass
764 else:
765 # Child process
766 os._exit(0) # pylint: disable=W0212
767 else:
768 # Parent process
769 parent_logger.wait_for_logs()
770 os._exit(0) # pylint: disable=W0212
772 logging.info("Daemon process started in the background, logging to '%s'" %
773 os.environ[LOG_FILE_ENV_VAR])
775 os.chdir(HOME_DIR)
777 parent_logger.start_logging()
779 # Copy the file-descriptors to create new stdin, stdout and stderr. Note
780 # that dup2(oldfd, newfd) closes newfd first, so this will close the current
781 # stdin, stdout and stderr, detaching from the terminal.
782 os.dup2(devnull_fd, sys.stdin.fileno())
783 os.dup2(log_fd, sys.stdout.fileno())
784 os.dup2(log_fd, sys.stderr.fileno())
786 # Close the temporary file-descriptors.
787 os.close(devnull_fd)
788 os.close(log_fd)
791 def cleanup():
792 logging.info("Cleanup.")
794 global g_desktops
795 for desktop in g_desktops:
796 for proc, name in [(desktop.x_proc, "Xvfb"),
797 (desktop.session_proc, "session"),
798 (desktop.host_proc, "host")]:
799 if proc is not None:
800 logging.info("Terminating " + name)
801 try:
802 psutil_proc = psutil.Process(proc.pid)
803 psutil_proc.terminate()
805 # Use a short timeout, to avoid delaying service shutdown if the
806 # process refuses to die for some reason.
807 psutil_proc.wait(timeout=10)
808 except psutil.TimeoutExpired:
809 logging.error("Timed out - sending SIGKILL")
810 psutil_proc.kill()
811 except psutil.Error:
812 logging.error("Error terminating process")
814 g_desktops = []
815 if ParentProcessLogger.instance():
816 ParentProcessLogger.instance().release_parent()
818 class SignalHandler:
819 """Reload the config file on SIGHUP. Since we pass the configuration to the
820 host processes via stdin, they can't reload it, so terminate them. They will
821 be relaunched automatically with the new config."""
823 def __init__(self, host_config):
824 self.host_config = host_config
826 def __call__(self, signum, _stackframe):
827 if signum == signal.SIGHUP:
828 logging.info("SIGHUP caught, restarting host.")
829 try:
830 self.host_config.load()
831 except (IOError, ValueError) as e:
832 logging.error("Failed to load config: " + str(e))
833 for desktop in g_desktops:
834 if desktop.host_proc:
835 desktop.host_proc.send_signal(signal.SIGTERM)
836 else:
837 # Exit cleanly so the atexit handler, cleanup(), gets called.
838 raise SystemExit
841 class RelaunchInhibitor:
842 """Helper class for inhibiting launch of a child process before a timeout has
843 elapsed.
845 A managed process can be in one of these states:
846 running, not inhibited (running == True)
847 stopped and inhibited (running == False and is_inhibited() == True)
848 stopped but not inhibited (running == False and is_inhibited() == False)
850 Attributes:
851 label: Name of the tracked process. Only used for logging.
852 running: Whether the process is currently running.
853 earliest_relaunch_time: Time before which the process should not be
854 relaunched, or 0 if there is no limit.
855 failures: The number of times that the process ran for less than a
856 specified timeout, and had to be inhibited. This count is reset to 0
857 whenever the process has run for longer than the timeout.
860 def __init__(self, label):
861 self.label = label
862 self.running = False
863 self.earliest_relaunch_time = 0
864 self.earliest_successful_termination = 0
865 self.failures = 0
867 def is_inhibited(self):
868 return (not self.running) and (time.time() < self.earliest_relaunch_time)
870 def record_started(self, minimum_lifetime, relaunch_delay):
871 """Record that the process was launched, and set the inhibit time to
872 |timeout| seconds in the future."""
873 self.earliest_relaunch_time = time.time() + relaunch_delay
874 self.earliest_successful_termination = time.time() + minimum_lifetime
875 self.running = True
877 def record_stopped(self):
878 """Record that the process was stopped, and adjust the failure count
879 depending on whether the process ran long enough."""
880 self.running = False
881 if time.time() < self.earliest_successful_termination:
882 self.failures += 1
883 else:
884 self.failures = 0
885 logging.info("Failure count for '%s' is now %d", self.label, self.failures)
888 def relaunch_self():
889 cleanup()
890 os.execvp(SCRIPT_PATH, sys.argv)
893 def waitpid_with_timeout(pid, deadline):
894 """Wrapper around os.waitpid() which waits until either a child process dies
895 or the deadline elapses.
897 Args:
898 pid: Process ID to wait for, or -1 to wait for any child process.
899 deadline: Waiting stops when time.time() exceeds this value.
901 Returns:
902 (pid, status): Same as for os.waitpid(), except that |pid| is 0 if no child
903 changed state within the timeout.
905 Raises:
906 Same as for os.waitpid().
908 while time.time() < deadline:
909 pid, status = os.waitpid(pid, os.WNOHANG)
910 if pid != 0:
911 return (pid, status)
912 time.sleep(1)
913 return (0, 0)
916 def waitpid_handle_exceptions(pid, deadline):
917 """Wrapper around os.waitpid()/waitpid_with_timeout(), which waits until
918 either a child process exits or the deadline elapses, and retries if certain
919 exceptions occur.
921 Args:
922 pid: Process ID to wait for, or -1 to wait for any child process.
923 deadline: If non-zero, waiting stops when time.time() exceeds this value.
924 If zero, waiting stops when a child process exits.
926 Returns:
927 (pid, status): Same as for waitpid_with_timeout(). |pid| is non-zero if and
928 only if a child exited during the wait.
930 Raises:
931 Same as for os.waitpid(), except:
932 OSError with errno==EINTR causes the wait to be retried (this can happen,
933 for example, if this parent process receives SIGHUP).
934 OSError with errno==ECHILD means there are no child processes, and so
935 this function sleeps until |deadline|. If |deadline| is zero, this is an
936 error and the OSError exception is raised in this case.
938 while True:
939 try:
940 if deadline == 0:
941 pid_result, status = os.waitpid(pid, 0)
942 else:
943 pid_result, status = waitpid_with_timeout(pid, deadline)
944 return (pid_result, status)
945 except OSError, e:
946 if e.errno == errno.EINTR:
947 continue
948 elif e.errno == errno.ECHILD:
949 now = time.time()
950 if deadline == 0:
951 # No time-limit and no child processes. This is treated as an error
952 # (see docstring).
953 raise
954 elif deadline > now:
955 time.sleep(deadline - now)
956 return (0, 0)
957 else:
958 # Anything else is an unexpected error.
959 raise
962 def watch_for_resolution_changes(initial_size):
963 """Watches for any resolution-changes which set the maximum screen resolution,
964 and resets the initial size if this happens.
966 The Ubuntu desktop has a component (the 'xrandr' plugin of
967 unity-settings-daemon) which often changes the screen resolution to the
968 first listed mode. This is the built-in mode for the maximum screen size,
969 which can trigger excessive CPU usage in some situations. So this is a hack
970 which waits for any such events, and undoes the change if it occurs.
972 Sometimes, the user might legitimately want to use the maximum available
973 resolution, so this monitoring is limited to a short time-period.
975 for _ in range(30):
976 time.sleep(1)
978 xrandr_output = subprocess.Popen(["xrandr"],
979 stdout=subprocess.PIPE).communicate()[0]
980 matches = re.search(r'current (\d+) x (\d+), maximum (\d+) x (\d+)',
981 xrandr_output)
983 # No need to handle ValueError. If xrandr fails to give valid output,
984 # there's no point in continuing to monitor.
985 current_size = (int(matches.group(1)), int(matches.group(2)))
986 maximum_size = (int(matches.group(3)), int(matches.group(4)))
988 if current_size != initial_size:
989 # Resolution change detected.
990 if current_size == maximum_size:
991 # This was probably an automated change from unity-settings-daemon, so
992 # undo it.
993 label = "%dx%d" % initial_size
994 args = ["xrandr", "-s", label]
995 subprocess.call(args)
996 args = ["xrandr", "--dpi", "96"]
997 subprocess.call(args)
999 # Stop monitoring after any change was detected.
1000 break
1003 def main():
1004 EPILOG = """This script is not intended for use by end-users. To configure
1005 Chrome Remote Desktop, please install the app from the Chrome
1006 Web Store: https://chrome.google.com/remotedesktop"""
1007 parser = optparse.OptionParser(
1008 usage="Usage: %prog [options] [ -- [ X server options ] ]",
1009 epilog=EPILOG)
1010 parser.add_option("-s", "--size", dest="size", action="append",
1011 help="Dimensions of virtual desktop. This can be specified "
1012 "multiple times to make multiple screen resolutions "
1013 "available (if the Xvfb server supports this).")
1014 parser.add_option("-f", "--foreground", dest="foreground", default=False,
1015 action="store_true",
1016 help="Don't run as a background daemon.")
1017 parser.add_option("", "--start", dest="start", default=False,
1018 action="store_true",
1019 help="Start the host.")
1020 parser.add_option("-k", "--stop", dest="stop", default=False,
1021 action="store_true",
1022 help="Stop the daemon currently running.")
1023 parser.add_option("", "--get-status", dest="get_status", default=False,
1024 action="store_true",
1025 help="Prints host status")
1026 parser.add_option("", "--check-running", dest="check_running", default=False,
1027 action="store_true",
1028 help="Return 0 if the daemon is running, or 1 otherwise.")
1029 parser.add_option("", "--config", dest="config", action="store",
1030 help="Use the specified configuration file.")
1031 parser.add_option("", "--reload", dest="reload", default=False,
1032 action="store_true",
1033 help="Signal currently running host to reload the config.")
1034 parser.add_option("", "--add-user", dest="add_user", default=False,
1035 action="store_true",
1036 help="Add current user to the chrome-remote-desktop group.")
1037 parser.add_option("", "--add-user-as-root", dest="add_user_as_root",
1038 action="store", metavar="USER",
1039 help="Adds the specified user to the chrome-remote-desktop "
1040 "group (must be run as root).")
1041 parser.add_option("", "--host-version", dest="host_version", default=False,
1042 action="store_true",
1043 help="Prints version of the host.")
1044 parser.add_option("", "--watch-resolution", dest="watch_resolution",
1045 type="int", nargs=2, default=False, action="store",
1046 help=optparse.SUPPRESS_HELP)
1047 (options, args) = parser.parse_args()
1049 # Determine the filename of the host configuration and PID files.
1050 if not options.config:
1051 options.config = os.path.join(CONFIG_DIR, "host#%s.json" % g_host_hash)
1053 # Check for a modal command-line option (start, stop, etc.)
1054 if options.get_status:
1055 proc = get_daemon_proc()
1056 if proc is not None:
1057 print "STARTED"
1058 elif is_supported_platform():
1059 print "STOPPED"
1060 else:
1061 print "NOT_IMPLEMENTED"
1062 return 0
1064 # TODO(sergeyu): Remove --check-running once NPAPI plugin and NM host are
1065 # updated to always use get-status flag instead.
1066 if options.check_running:
1067 proc = get_daemon_proc()
1068 return 1 if proc is None else 0
1070 if options.stop:
1071 proc = get_daemon_proc()
1072 if proc is None:
1073 print "The daemon is not currently running"
1074 else:
1075 print "Killing process %s" % proc.pid
1076 proc.terminate()
1077 try:
1078 proc.wait(timeout=30)
1079 except psutil.TimeoutExpired:
1080 print "Timed out trying to kill daemon process"
1081 return 1
1082 return 0
1084 if options.reload:
1085 proc = get_daemon_proc()
1086 if proc is None:
1087 return 1
1088 proc.send_signal(signal.SIGHUP)
1089 return 0
1091 if options.add_user:
1092 user = getpass.getuser()
1094 try:
1095 if user in grp.getgrnam(CHROME_REMOTING_GROUP_NAME).gr_mem:
1096 logging.info("User '%s' is already a member of '%s'." %
1097 (user, CHROME_REMOTING_GROUP_NAME))
1098 return 0
1099 except KeyError:
1100 logging.info("Group '%s' not found." % CHROME_REMOTING_GROUP_NAME)
1102 command = [SCRIPT_PATH, '--add-user-as-root', user]
1103 if os.getenv("DISPLAY"):
1104 # TODO(rickyz): Add a Polkit policy that includes a more friendly message
1105 # about what this command does.
1106 command = ["/usr/bin/pkexec"] + command
1107 else:
1108 command = ["/usr/bin/sudo", "-k", "--"] + command
1110 # Run with an empty environment out of paranoia, though if an attacker
1111 # controls the environment this script is run under, we're already screwed
1112 # anyway.
1113 os.execve(command[0], command, {})
1114 return 1
1116 if options.add_user_as_root is not None:
1117 if os.getuid() != 0:
1118 logging.error("--add-user-as-root can only be specified as root.")
1119 return 1;
1121 user = options.add_user_as_root
1122 try:
1123 pwd.getpwnam(user)
1124 except KeyError:
1125 logging.error("user '%s' does not exist." % user)
1126 return 1
1128 try:
1129 subprocess.check_call(["/usr/sbin/groupadd", "-f",
1130 CHROME_REMOTING_GROUP_NAME])
1131 subprocess.check_call(["/usr/bin/gpasswd", "--add", user,
1132 CHROME_REMOTING_GROUP_NAME])
1133 except (ValueError, OSError, subprocess.CalledProcessError) as e:
1134 logging.error("Command failed: " + str(e))
1135 return 1
1137 return 0
1139 if options.host_version:
1140 # TODO(sergeyu): Also check RPM package version once we add RPM package.
1141 return os.system(locate_executable(HOST_BINARY_NAME) + " --version") >> 8
1143 if options.watch_resolution:
1144 watch_for_resolution_changes(options.watch_resolution)
1145 return 0
1147 if not options.start:
1148 # If no modal command-line options specified, print an error and exit.
1149 print >> sys.stderr, EPILOG
1150 return 1
1152 # If a RANDR-supporting Xvfb is not available, limit the default size to
1153 # something more sensible.
1154 if get_randr_supporting_x_server():
1155 default_sizes = DEFAULT_SIZES
1156 else:
1157 default_sizes = DEFAULT_SIZE_NO_RANDR
1159 # Collate the list of sizes that XRANDR should support.
1160 if not options.size:
1161 if os.environ.has_key(DEFAULT_SIZES_ENV_VAR):
1162 default_sizes = os.environ[DEFAULT_SIZES_ENV_VAR]
1163 options.size = default_sizes.split(",")
1165 sizes = []
1166 for size in options.size:
1167 size_components = size.split("x")
1168 if len(size_components) != 2:
1169 parser.error("Incorrect size format '%s', should be WIDTHxHEIGHT" % size)
1171 try:
1172 width = int(size_components[0])
1173 height = int(size_components[1])
1175 # Enforce minimum desktop size, as a sanity-check. The limit of 100 will
1176 # detect typos of 2 instead of 3 digits.
1177 if width < 100 or height < 100:
1178 raise ValueError
1179 except ValueError:
1180 parser.error("Width and height should be 100 pixels or greater")
1182 sizes.append((width, height))
1184 # Register an exit handler to clean up session process and the PID file.
1185 atexit.register(cleanup)
1187 # Load the initial host configuration.
1188 host_config = Config(options.config)
1189 try:
1190 host_config.load()
1191 except (IOError, ValueError) as e:
1192 print >> sys.stderr, "Failed to load config: " + str(e)
1193 return 1
1195 # Register handler to re-load the configuration in response to signals.
1196 for s in [signal.SIGHUP, signal.SIGINT, signal.SIGTERM]:
1197 signal.signal(s, SignalHandler(host_config))
1199 # Verify that the initial host configuration has the necessary fields.
1200 auth = Authentication()
1201 auth_config_valid = auth.copy_from(host_config)
1202 host = Host()
1203 host_config_valid = host.copy_from(host_config)
1204 if not host_config_valid or not auth_config_valid:
1205 logging.error("Failed to load host configuration.")
1206 return 1
1208 # Determine whether a desktop is already active for the specified host
1209 # host configuration.
1210 proc = get_daemon_proc()
1211 if proc is not None:
1212 # Debian policy requires that services should "start" cleanly and return 0
1213 # if they are already running.
1214 print "Service already running."
1215 return 0
1217 # Detach a separate "daemon" process to run the session, unless specifically
1218 # requested to run in the foreground.
1219 if not options.foreground:
1220 daemonize()
1222 logging.info("Using host_id: " + host.host_id)
1224 desktop = Desktop(sizes)
1226 # Keep track of the number of consecutive failures of any child process to
1227 # run for longer than a set period of time. The script will exit after a
1228 # threshold is exceeded.
1229 # There is no point in tracking the X session process separately, since it is
1230 # launched at (roughly) the same time as the X server, and the termination of
1231 # one of these triggers the termination of the other.
1232 x_server_inhibitor = RelaunchInhibitor("X server")
1233 host_inhibitor = RelaunchInhibitor("host")
1234 all_inhibitors = [x_server_inhibitor, host_inhibitor]
1236 # Don't allow relaunching the script on the first loop iteration.
1237 allow_relaunch_self = False
1239 while True:
1240 # Set the backoff interval and exit if a process failed too many times.
1241 backoff_time = SHORT_BACKOFF_TIME
1242 for inhibitor in all_inhibitors:
1243 if inhibitor.failures >= MAX_LAUNCH_FAILURES:
1244 logging.error("Too many launch failures of '%s', exiting."
1245 % inhibitor.label)
1246 return 1
1247 elif inhibitor.failures >= SHORT_BACKOFF_THRESHOLD:
1248 backoff_time = LONG_BACKOFF_TIME
1250 relaunch_times = []
1252 # If the session process or X server stops running (e.g. because the user
1253 # logged out), kill the other. This will trigger the next conditional block
1254 # as soon as os.waitpid() reaps its exit-code.
1255 if desktop.session_proc is None and desktop.x_proc is not None:
1256 logging.info("Terminating X server")
1257 desktop.x_proc.terminate()
1258 elif desktop.x_proc is None and desktop.session_proc is not None:
1259 logging.info("Terminating X session")
1260 desktop.session_proc.terminate()
1261 elif desktop.x_proc is None and desktop.session_proc is None:
1262 # Both processes have terminated.
1263 if (allow_relaunch_self and x_server_inhibitor.failures == 0 and
1264 host_inhibitor.failures == 0):
1265 # Since the user's desktop is already gone at this point, there's no
1266 # state to lose and now is a good time to pick up any updates to this
1267 # script that might have been installed.
1268 logging.info("Relaunching self")
1269 relaunch_self()
1270 else:
1271 # If there is a non-zero |failures| count, restarting the whole script
1272 # would lose this information, so just launch the session as normal.
1273 if x_server_inhibitor.is_inhibited():
1274 logging.info("Waiting before launching X server")
1275 relaunch_times.append(x_server_inhibitor.earliest_relaunch_time)
1276 else:
1277 logging.info("Launching X server and X session.")
1278 desktop.launch_session(args)
1279 x_server_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME,
1280 backoff_time)
1281 allow_relaunch_self = True
1283 if desktop.host_proc is None:
1284 if host_inhibitor.is_inhibited():
1285 logging.info("Waiting before launching host process")
1286 relaunch_times.append(host_inhibitor.earliest_relaunch_time)
1287 else:
1288 logging.info("Launching host process")
1289 desktop.launch_host(host_config)
1290 host_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME,
1291 backoff_time)
1293 deadline = min(relaunch_times) if relaunch_times else 0
1294 pid, status = waitpid_handle_exceptions(-1, deadline)
1295 if pid == 0:
1296 continue
1298 logging.info("wait() returned (%s,%s)" % (pid, status))
1300 # When a process has terminated, and we've reaped its exit-code, any Popen
1301 # instance for that process is no longer valid. Reset any affected instance
1302 # to None.
1303 if desktop.x_proc is not None and pid == desktop.x_proc.pid:
1304 logging.info("X server process terminated")
1305 desktop.x_proc = None
1306 x_server_inhibitor.record_stopped()
1308 if desktop.session_proc is not None and pid == desktop.session_proc.pid:
1309 logging.info("Session process terminated")
1310 desktop.session_proc = None
1312 if desktop.host_proc is not None and pid == desktop.host_proc.pid:
1313 logging.info("Host process terminated")
1314 desktop.host_proc = None
1315 desktop.host_ready = False
1316 host_inhibitor.record_stopped()
1318 # These exit-codes must match the ones used by the host.
1319 # See remoting/host/host_error_codes.h.
1320 # Delete the host or auth configuration depending on the returned error
1321 # code, so the next time this script is run, a new configuration
1322 # will be created and registered.
1323 if os.WIFEXITED(status):
1324 if os.WEXITSTATUS(status) == 100:
1325 logging.info("Host configuration is invalid - exiting.")
1326 return 0
1327 elif os.WEXITSTATUS(status) == 101:
1328 logging.info("Host ID has been deleted - exiting.")
1329 host_config.clear()
1330 host_config.save_and_log_errors()
1331 return 0
1332 elif os.WEXITSTATUS(status) == 102:
1333 logging.info("OAuth credentials are invalid - exiting.")
1334 return 0
1335 elif os.WEXITSTATUS(status) == 103:
1336 logging.info("Host domain is blocked by policy - exiting.")
1337 return 0
1338 # Nothing to do for Mac-only status 104 (login screen unsupported)
1339 elif os.WEXITSTATUS(status) == 105:
1340 logging.info("Username is blocked by policy - exiting.")
1341 return 0
1342 else:
1343 logging.info("Host exited with status %s." % os.WEXITSTATUS(status))
1344 elif os.WIFSIGNALED(status):
1345 logging.info("Host terminated by signal %s." % os.WTERMSIG(status))
1348 if __name__ == "__main__":
1349 logging.basicConfig(level=logging.DEBUG,
1350 format="%(asctime)s:%(levelname)s:%(message)s")
1351 sys.exit(main())