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.
33 LOG_FILE_ENV_VAR = "CHROME_REMOTE_DESKTOP_LOG_FILE"
35 # This script has a sensible default for the initial and maximum desktop size,
36 # which can be overridden either on the command-line, or via a comma-separated
37 # list of sizes in this environment variable.
38 DEFAULT_SIZES_ENV_VAR = "CHROME_REMOTE_DESKTOP_DEFAULT_DESKTOP_SIZES"
40 # By default, provide a relatively small size to handle the case where resize-
41 # to-client is disabled, and a much larger size to support clients with large
42 # or mulitple monitors. These defaults can be overridden in ~/.profile.
43 DEFAULT_SIZES = "1600x1200,3840x1600"
45 SCRIPT_PATH = sys.path[0]
47 IS_INSTALLED = (os.path.basename(sys.argv[0]) != 'linux_me2me_host.py')
50 HOST_BINARY_NAME = "chrome-remote-desktop-host"
52 HOST_BINARY_NAME = "remoting_me2me_host"
54 CHROME_REMOTING_GROUP_NAME = "chrome-remote-desktop"
56 HOME_DIR = os.environ["HOME"]
57 CONFIG_DIR = os.path.join(HOME_DIR, ".config/chrome-remote-desktop")
58 SESSION_FILE_PATH = os.path.join(HOME_DIR, ".chrome-remote-desktop-session")
60 X_LOCK_FILE_TEMPLATE = "/tmp/.X%d-lock"
61 FIRST_X_DISPLAY_NUMBER = 20
63 # Amount of time to wait between relaunching processes.
64 SHORT_BACKOFF_TIME = 5
65 LONG_BACKOFF_TIME = 60
67 # How long a process must run in order not to be counted against the restart
69 MINIMUM_PROCESS_LIFETIME = 60
71 # Thresholds for switching from fast- to slow-restart and for giving up
72 # trying to restart entirely.
73 SHORT_BACKOFF_THRESHOLD = 5
74 MAX_LAUNCH_FAILURES = SHORT_BACKOFF_THRESHOLD + 10
76 # Globals needed by the atexit cleanup() handler.
78 g_host_hash = hashlib.md5(socket.gethostname()).hexdigest()
80 def is_supported_platform():
81 # Always assume that the system is supported if the config directory or
83 if os.path.isdir(CONFIG_DIR) or os.path.isfile(SESSION_FILE_PATH):
86 # The host has been tested only on Ubuntu.
87 distribution = platform.linux_distribution()
88 return (distribution[0]).lower() == 'ubuntu'
91 def __init__(self, path):
97 """Loads the config from file.
100 IOError: Error reading data
101 ValueError: Error parsing JSON
103 settings_file = open(self.path, 'r')
104 self.data = json.load(settings_file)
106 settings_file.close()
109 """Saves the config to file.
112 IOError: Error writing data
113 TypeError: Error serialising JSON
117 old_umask = os.umask(0066)
119 settings_file = open(self.path, 'w')
120 settings_file.write(json.dumps(self.data, indent=2))
121 settings_file.close()
126 def save_and_log_errors(self):
127 """Calls self.save(), trapping and logging any errors."""
130 except (IOError, TypeError) as e:
131 logging.error("Failed to save config: " + str(e))
134 return self.data.get(key)
136 def __getitem__(self, key):
137 return self.data[key]
139 def __setitem__(self, key, value):
140 self.data[key] = value
148 class Authentication:
149 """Manage authentication tokens for Chromoting/xmpp"""
153 self.oauth_refresh_token = None
155 def copy_from(self, config):
156 """Loads the config and returns false if the config is invalid."""
158 self.login = config["xmpp_login"]
159 self.oauth_refresh_token = config["oauth_refresh_token"]
164 def copy_to(self, config):
165 config["xmpp_login"] = self.login
166 config["oauth_refresh_token"] = self.oauth_refresh_token
170 """This manages the configuration for a host."""
173 self.host_id = str(uuid.uuid1())
174 self.host_name = socket.gethostname()
175 self.host_secret_hash = None
176 self.private_key = None
178 def copy_from(self, config):
180 self.host_id = config["host_id"]
181 self.host_name = config["host_name"]
182 self.host_secret_hash = config.get("host_secret_hash")
183 self.private_key = config["private_key"]
188 def copy_to(self, config):
189 config["host_id"] = self.host_id
190 config["host_name"] = self.host_name
191 config["host_secret_hash"] = self.host_secret_hash
192 config["private_key"] = self.private_key
196 """Manage a single virtual desktop"""
198 def __init__(self, sizes):
200 self.session_proc = None
201 self.host_proc = None
202 self.child_env = None
204 self.pulseaudio_pipe = None
205 self.server_supports_exact_resize = False
206 self.host_ready = False
207 self.ssh_auth_sockname = None
208 g_desktops.append(self)
211 def get_unused_display_number():
212 """Return a candidate display number for which there is currently no
213 X Server lock file"""
214 display = FIRST_X_DISPLAY_NUMBER
215 while os.path.exists(X_LOCK_FILE_TEMPLATE % display):
219 def _init_child_env(self):
220 # Create clean environment for new session, so it is cleanly separated from
221 # the user's console X session.
233 if os.environ.has_key(key):
234 self.child_env[key] = os.environ[key]
236 # Read from /etc/environment if it exists, as it is a standard place to
237 # store system-wide environment settings. During a normal login, this would
238 # typically be done by the pam_env PAM module, depending on the local PAM
240 env_filename = "/etc/environment"
242 with open(env_filename, "r") as env_file:
243 for line in env_file:
244 line = line.rstrip("\n")
245 # Split at the first "=", leaving any further instances in the value.
246 key_value_pair = line.split("=", 1)
247 if len(key_value_pair) == 2:
248 key, value = tuple(key_value_pair)
249 # The file stores key=value assignments, but the value may be
250 # quoted, so strip leading & trailing quotes from it.
251 value = value.strip("'\"")
252 self.child_env[key] = value
254 logging.info("Failed to read %s, skipping." % env_filename)
256 def _setup_pulseaudio(self):
257 self.pulseaudio_pipe = None
259 # pulseaudio uses UNIX sockets for communication. Length of UNIX socket
260 # name is limited to 108 characters, so audio will not work properly if
261 # the path is too long. To workaround this problem we use only first 10
262 # symbols of the host hash.
263 pulse_path = os.path.join(CONFIG_DIR,
264 "pulseaudio#%s" % g_host_hash[0:10])
265 if len(pulse_path) + len("/native") >= 108:
266 logging.error("Audio will not be enabled because pulseaudio UNIX " +
267 "socket path is too long.")
270 sink_name = "chrome_remote_desktop_session"
271 pipe_name = os.path.join(pulse_path, "fifo_output")
274 if not os.path.exists(pulse_path):
276 if not os.path.exists(pipe_name):
279 logging.error("Failed to create pulseaudio pipe: " + str(e))
283 pulse_config = open(os.path.join(pulse_path, "daemon.conf"), "w")
284 pulse_config.write("default-sample-format = s16le\n")
285 pulse_config.write("default-sample-rate = 48000\n")
286 pulse_config.write("default-sample-channels = 2\n")
289 pulse_script = open(os.path.join(pulse_path, "default.pa"), "w")
290 pulse_script.write("load-module module-native-protocol-unix\n")
292 ("load-module module-pipe-sink sink_name=%s file=\"%s\" " +
293 "rate=48000 channels=2 format=s16le\n") %
294 (sink_name, pipe_name))
297 logging.error("Failed to write pulseaudio config: " + str(e))
300 self.child_env["PULSE_CONFIG_PATH"] = pulse_path
301 self.child_env["PULSE_RUNTIME_PATH"] = pulse_path
302 self.child_env["PULSE_STATE_PATH"] = pulse_path
303 self.child_env["PULSE_SINK"] = sink_name
304 self.pulseaudio_pipe = pipe_name
308 def _setup_gnubby(self):
309 self.ssh_auth_sockname = ("/tmp/chromoting.%s.ssh_auth_sock" %
312 def _launch_x_server(self, extra_x_args):
313 x_auth_file = os.path.expanduser("~/.Xauthority")
314 self.child_env["XAUTHORITY"] = x_auth_file
315 devnull = open(os.devnull, "rw")
316 display = self.get_unused_display_number()
318 # Run "xauth add" with |child_env| so that it modifies the same XAUTHORITY
319 # file which will be used for the X session.
320 ret_code = subprocess.call("xauth add :%d . `mcookie`" % display,
321 env=self.child_env, shell=True)
323 raise Exception("xauth failed with code %d" % ret_code)
325 max_width = max([width for width, height in self.sizes])
326 max_height = max([height for width, height in self.sizes])
329 # TODO(jamiewalch): This script expects to be installed alongside
330 # Xvfb-randr, but that's no longer the case. Fix this once we have
331 # a Xvfb-randr package that installs somewhere sensible.
332 xvfb = "/usr/bin/Xvfb-randr"
333 if not os.path.exists(xvfb):
334 xvfb = locate_executable("Xvfb-randr")
335 self.server_supports_exact_resize = True
338 self.server_supports_exact_resize = False
340 # Disable the Composite extension iff the X session is the default
341 # Unity-2D, since it uses Metacity which fails to generate DAMAGE
342 # notifications correctly. See crbug.com/166468.
343 x_session = choose_x_session()
344 if (len(x_session) == 2 and
345 x_session[1] == "/usr/bin/gnome-session --session=ubuntu-2d"):
346 extra_x_args.extend(["-extension", "Composite"])
348 logging.info("Starting %s on display :%d" % (xvfb, display))
349 screen_option = "%dx%dx24" % (max_width, max_height)
350 self.x_proc = subprocess.Popen(
351 [xvfb, ":%d" % display,
352 "-auth", x_auth_file,
355 "-screen", "0", screen_option
357 if not self.x_proc.pid:
358 raise Exception("Could not start Xvfb.")
360 self.child_env["DISPLAY"] = ":%d" % display
361 self.child_env["CHROME_REMOTE_DESKTOP_SESSION"] = "1"
363 # Use a separate profile for any instances of Chrome that are started in
364 # the virtual session. Chrome doesn't support sharing a profile between
365 # multiple DISPLAYs, but Chrome Sync allows for a reasonable compromise.
366 chrome_profile = os.path.join(CONFIG_DIR, "chrome-profile")
367 self.child_env["CHROME_USER_DATA_DIR"] = chrome_profile
369 # Set SSH_AUTH_SOCK to the file name to listen on.
370 if self.ssh_auth_sockname:
371 self.child_env["SSH_AUTH_SOCK"] = self.ssh_auth_sockname
373 # Wait for X to be active.
374 for _test in range(5):
375 proc = subprocess.Popen("xdpyinfo", env=self.child_env, stdout=devnull)
376 _pid, retcode = os.waitpid(proc.pid, 0)
381 raise Exception("Could not connect to Xvfb.")
383 logging.info("Xvfb is active.")
385 # The remoting host expects the server to use "evdev" keycodes, but Xvfb
386 # starts configured to use the "base" ruleset, resulting in XKB configuring
387 # for "xfree86" keycodes, and screwing up some keys. See crbug.com/119013.
388 # Reconfigure the X server to use "evdev" keymap rules. The X server must
389 # be started with -noreset otherwise it'll reset as soon as the command
390 # completes, since there are no other X clients running yet.
391 proc = subprocess.Popen("setxkbmap -rules evdev", env=self.child_env,
393 _pid, retcode = os.waitpid(proc.pid, 0)
395 logging.error("Failed to set XKB to 'evdev'")
397 # Register the screen sizes if the X server's RANDR extension supports it.
398 # Errors here are non-fatal; the X server will continue to run with the
399 # dimensions from the "-screen" option.
400 for width, height in self.sizes:
401 label = "%dx%d" % (width, height)
402 args = ["xrandr", "--newmode", label, "0", str(width), "0", "0", "0",
403 str(height), "0", "0", "0"]
404 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
405 args = ["xrandr", "--addmode", "screen", label]
406 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
408 # Set the initial mode to the first size specified, otherwise the X server
409 # would default to (max_width, max_height), which might not even be in the
411 label = "%dx%d" % self.sizes[0]
412 args = ["xrandr", "-s", label]
413 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
415 # Set the physical size of the display so that the initial mode is running
416 # at approximately 96 DPI, since some desktops require the DPI to be set to
417 # something realistic.
418 args = ["xrandr", "--dpi", "96"]
419 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
423 def _launch_x_session(self):
424 # Start desktop session.
425 # The /dev/null input redirection is necessary to prevent the X session
426 # reading from stdin. If this code runs as a shell background job in a
427 # terminal, any reading from stdin causes the job to be suspended.
428 # Daemonization would solve this problem by separating the process from the
429 # controlling terminal.
430 xsession_command = choose_x_session()
431 if xsession_command is None:
432 raise Exception("Unable to choose suitable X session command.")
434 logging.info("Launching X session: %s" % xsession_command)
435 self.session_proc = subprocess.Popen(xsession_command,
436 stdin=open(os.devnull, "r"),
439 if not self.session_proc.pid:
440 raise Exception("Could not start X session")
442 def launch_session(self, x_args):
443 self._init_child_env()
444 self._setup_pulseaudio()
446 self._launch_x_server(x_args)
447 self._launch_x_session()
449 def launch_host(self, host_config):
450 # Start remoting host
451 args = [locate_executable(HOST_BINARY_NAME), "--host-config=-"]
452 if self.pulseaudio_pipe:
453 args.append("--audio-pipe-name=%s" % self.pulseaudio_pipe)
454 if self.server_supports_exact_resize:
455 args.append("--server-supports-exact-resize")
456 if self.ssh_auth_sockname:
457 args.append("--ssh-auth-sockname=%s" % self.ssh_auth_sockname)
459 # Have the host process use SIGUSR1 to signal a successful start.
460 def sigusr1_handler(signum, frame):
462 logging.info("Host ready to receive connections.")
463 self.host_ready = True
464 if (ParentProcessLogger.instance() and
465 False not in [desktop.host_ready for desktop in g_desktops]):
466 ParentProcessLogger.instance().release_parent()
468 signal.signal(signal.SIGUSR1, sigusr1_handler)
469 args.append("--signal-parent")
471 self.host_proc = subprocess.Popen(args, env=self.child_env,
472 stdin=subprocess.PIPE)
474 if not self.host_proc.pid:
475 raise Exception("Could not start Chrome Remote Desktop host")
476 self.host_proc.stdin.write(json.dumps(host_config.data))
477 self.host_proc.stdin.close()
480 def get_daemon_pid():
481 """Checks if there is already an instance of this script running, and returns
485 The process ID of the existing daemon process, or 0 if the daemon is not
489 this_pid = os.getpid()
491 for process in psutil.process_iter():
492 # Skip any processes that raise an exception, as processes may terminate
493 # during iteration over the list.
495 # Skip other users' processes.
496 if process.uids.real != uid:
499 # Skip the process for this instance.
500 if process.pid == this_pid:
503 # |cmdline| will be [python-interpreter, script-file, other arguments...]
504 cmdline = process.cmdline
507 if cmdline[0] == sys.executable and cmdline[1] == sys.argv[0]:
509 except psutil.error.Error:
515 def choose_x_session():
516 """Chooses the most appropriate X session command for this system.
519 A string containing the command to run, or a list of strings containing
520 the executable program and its arguments, which is suitable for passing as
521 the first parameter of subprocess.Popen(). If a suitable session cannot
522 be found, returns None.
524 # If the session wrapper script (see below) is given a specific session as an
525 # argument (such as ubuntu-2d on Ubuntu 12.04), the wrapper will run that
526 # session instead of looking for custom .xsession files in the home directory.
527 # So it's necessary to test for these files here.
532 for startup_file in XSESSION_FILES:
533 startup_file = os.path.expanduser(startup_file)
534 if os.path.exists(startup_file):
535 # Use the same logic that a Debian system typically uses with ~/.xsession
536 # (see /etc/X11/Xsession.d/50x11-common_determine-startup), to determine
537 # exactly how to run this file.
538 if os.access(startup_file, os.X_OK):
539 # "/bin/sh -c" is smart about how to execute the session script and
540 # works in cases where plain exec() fails (for example, if the file is
541 # marked executable, but is a plain script with no shebang line).
542 return ["/bin/sh", "-c", pipes.quote(startup_file)]
544 shell = os.environ.get("SHELL", "sh")
545 return [shell, startup_file]
547 # Choose a session wrapper script to run the session. On some systems,
548 # /etc/X11/Xsession fails to load the user's .profile, so look for an
549 # alternative wrapper that is more likely to match the script that the
550 # system actually uses for console desktop sessions.
552 "/usr/sbin/lightdm-session",
554 "/etc/X11/Xsession" ]
555 for session_wrapper in SESSION_WRAPPERS:
556 if os.path.exists(session_wrapper):
557 if os.path.exists("/usr/bin/unity-2d-panel"):
558 # On Ubuntu 12.04, the default session relies on 3D-accelerated
559 # hardware. Trying to run this with a virtual X display produces
560 # weird results on some systems (for example, upside-down and
561 # corrupt displays). So if the ubuntu-2d session is available,
562 # choose it explicitly.
563 return [session_wrapper, "/usr/bin/gnome-session --session=ubuntu-2d"]
565 # Use the session wrapper by itself, and let the system choose a
567 return [session_wrapper]
571 def locate_executable(exe_name):
573 # If the script is running from its installed location, search the host
574 # binary only in the same directory.
575 paths_to_try = [ SCRIPT_PATH ]
577 paths_to_try = map(lambda p: os.path.join(SCRIPT_PATH, p),
578 [".", "../../../out/Debug", "../../../out/Release" ])
579 for path in paths_to_try:
580 exe_path = os.path.join(path, exe_name)
581 if os.path.exists(exe_path):
584 raise Exception("Could not locate executable '%s'" % exe_name)
587 class ParentProcessLogger(object):
588 """Redirects logs to the parent process, until the host is ready or quits.
590 This class creates a pipe to allow logging from the daemon process to be
591 copied to the parent process. The daemon process adds a log-handler that
592 directs logging output to the pipe. The parent process reads from this pipe
593 until and writes the content to stderr. When the pipe is no longer needed
594 (for example, the host signals successful launch or permanent failure), the
595 daemon removes the log-handler and closes the pipe, causing the the parent
596 process to reach end-of-file while reading the pipe and exit.
598 The (singleton) logger should be instantiated before forking. The parent
599 process should call wait_for_logs() before exiting. The (grand-)child process
600 should call start_logging() when it starts, and then use logging.* to issue
601 log statements, as usual. When the child has either succesfully started the
602 host or terminated, it must call release_parent() to allow the parent to exit.
608 """Constructor. Must be called before forking."""
609 read_pipe, write_pipe = os.pipe()
610 # Ensure write_pipe is closed on exec, otherwise it will be kept open by
611 # child processes (X, host), preventing the read pipe from EOF'ing.
612 old_flags = fcntl.fcntl(write_pipe, fcntl.F_GETFD)
613 fcntl.fcntl(write_pipe, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
614 self._read_file = os.fdopen(read_pipe, 'r')
615 self._write_file = os.fdopen(write_pipe, 'a')
616 self._logging_handler = None
617 ParentProcessLogger.__instance = self
619 def start_logging(self):
620 """Installs a logging handler that sends log entries to a pipe.
622 Must be called by the child process.
624 self._read_file.close()
625 self._logging_handler = logging.StreamHandler(self._write_file)
626 logging.getLogger().addHandler(self._logging_handler)
628 def release_parent(self):
629 """Uninstalls logging handler and closes the pipe, releasing the parent.
631 Must be called by the child process.
633 if self._logging_handler:
634 logging.getLogger().removeHandler(self._logging_handler)
635 self._logging_handler = None
636 if not self._write_file.closed:
637 self._write_file.close()
639 def wait_for_logs(self):
640 """Waits and prints log lines from the daemon until the pipe is closed.
642 Must be called by the parent process.
644 # If Ctrl-C is pressed, inform the user that the daemon is still running.
645 # This signal will cause the read loop below to stop with an EINTR IOError.
646 def sigint_handler(signum, frame):
648 print >> sys.stderr, ("Interrupted. The daemon is still running in the "
651 signal.signal(signal.SIGINT, sigint_handler)
653 # Install a fallback timeout to release the parent process, in case the
654 # daemon never responds (e.g. host crash-looping, daemon killed).
655 # This signal will cause the read loop below to stop with an EINTR IOError.
656 def sigalrm_handler(signum, frame):
658 print >> sys.stderr, ("No response from daemon. It may have crashed, or "
659 "may still be running in the background.")
661 signal.signal(signal.SIGALRM, sigalrm_handler)
664 self._write_file.close()
666 # Print lines as they're logged to the pipe until EOF is reached or readline
667 # is interrupted by one of the signal handlers above.
669 for line in iter(self._read_file.readline, ''):
670 sys.stderr.write(line)
672 if e.errno != errno.EINTR:
674 print >> sys.stderr, "Log file: %s" % os.environ[LOG_FILE_ENV_VAR]
678 """Returns the singleton instance, if it exists."""
679 return ParentProcessLogger.__instance
683 """Background this process and detach from controlling terminal, redirecting
684 stdout/stderr to a log file."""
686 # TODO(lambroslambrou): Having stdout/stderr redirected to a log file is not
687 # ideal - it could create a filesystem DoS if the daemon or a child process
688 # were to write excessive amounts to stdout/stderr. Ideally, stdout/stderr
689 # should be redirected to a pipe or socket, and a process at the other end
690 # should consume the data and write it to a logging facility which can do
691 # data-capping or log-rotation. The 'logger' command-line utility could be
692 # used for this, but it might cause too much syslog spam.
694 # Create new (temporary) file-descriptors before forking, so any errors get
695 # reported to the main process and set the correct exit-code.
696 # The mode is provided, since Python otherwise sets a default mode of 0777,
697 # which would result in the new file having permissions of 0777 & ~umask,
698 # possibly leaving the executable bits set.
699 if not os.environ.has_key(LOG_FILE_ENV_VAR):
700 log_file_prefix = "chrome_remote_desktop_%s_" % time.strftime(
701 '%Y%m%d_%H%M%S', time.localtime(time.time()))
702 log_file = tempfile.NamedTemporaryFile(prefix=log_file_prefix, delete=False)
703 os.environ[LOG_FILE_ENV_VAR] = log_file.name
704 log_fd = log_file.file.fileno()
706 log_fd = os.open(os.environ[LOG_FILE_ENV_VAR],
707 os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0600)
709 devnull_fd = os.open(os.devnull, os.O_RDONLY)
711 parent_logger = ParentProcessLogger()
719 # The second fork ensures that the daemon isn't a session leader, so that
720 # it doesn't acquire a controlling terminal.
728 os._exit(0) # pylint: disable=W0212
731 parent_logger.wait_for_logs()
732 os._exit(0) # pylint: disable=W0212
734 logging.info("Daemon process started in the background, logging to '%s'" %
735 os.environ[LOG_FILE_ENV_VAR])
739 parent_logger.start_logging()
741 # Copy the file-descriptors to create new stdin, stdout and stderr. Note
742 # that dup2(oldfd, newfd) closes newfd first, so this will close the current
743 # stdin, stdout and stderr, detaching from the terminal.
744 os.dup2(devnull_fd, sys.stdin.fileno())
745 os.dup2(log_fd, sys.stdout.fileno())
746 os.dup2(log_fd, sys.stderr.fileno())
748 # Close the temporary file-descriptors.
754 logging.info("Cleanup.")
757 for desktop in g_desktops:
759 logging.info("Terminating Xvfb")
760 desktop.x_proc.terminate()
762 if ParentProcessLogger.instance():
763 ParentProcessLogger.instance().release_parent()
766 """Reload the config file on SIGHUP. Since we pass the configuration to the
767 host processes via stdin, they can't reload it, so terminate them. They will
768 be relaunched automatically with the new config."""
770 def __init__(self, host_config):
771 self.host_config = host_config
773 def __call__(self, signum, _stackframe):
774 if signum == signal.SIGHUP:
775 logging.info("SIGHUP caught, restarting host.")
777 self.host_config.load()
778 except (IOError, ValueError) as e:
779 logging.error("Failed to load config: " + str(e))
780 for desktop in g_desktops:
781 if desktop.host_proc:
782 desktop.host_proc.send_signal(signal.SIGTERM)
784 # Exit cleanly so the atexit handler, cleanup(), gets called.
788 class RelaunchInhibitor:
789 """Helper class for inhibiting launch of a child process before a timeout has
792 A managed process can be in one of these states:
793 running, not inhibited (running == True)
794 stopped and inhibited (running == False and is_inhibited() == True)
795 stopped but not inhibited (running == False and is_inhibited() == False)
798 label: Name of the tracked process. Only used for logging.
799 running: Whether the process is currently running.
800 earliest_relaunch_time: Time before which the process should not be
801 relaunched, or 0 if there is no limit.
802 failures: The number of times that the process ran for less than a
803 specified timeout, and had to be inhibited. This count is reset to 0
804 whenever the process has run for longer than the timeout.
807 def __init__(self, label):
810 self.earliest_relaunch_time = 0
811 self.earliest_successful_termination = 0
814 def is_inhibited(self):
815 return (not self.running) and (time.time() < self.earliest_relaunch_time)
817 def record_started(self, minimum_lifetime, relaunch_delay):
818 """Record that the process was launched, and set the inhibit time to
819 |timeout| seconds in the future."""
820 self.earliest_relaunch_time = time.time() + relaunch_delay
821 self.earliest_successful_termination = time.time() + minimum_lifetime
824 def record_stopped(self):
825 """Record that the process was stopped, and adjust the failure count
826 depending on whether the process ran long enough."""
828 if time.time() < self.earliest_successful_termination:
832 logging.info("Failure count for '%s' is now %d", self.label, self.failures)
837 os.execvp(sys.argv[0], sys.argv)
840 def waitpid_with_timeout(pid, deadline):
841 """Wrapper around os.waitpid() which waits until either a child process dies
842 or the deadline elapses.
845 pid: Process ID to wait for, or -1 to wait for any child process.
846 deadline: Waiting stops when time.time() exceeds this value.
849 (pid, status): Same as for os.waitpid(), except that |pid| is 0 if no child
850 changed state within the timeout.
853 Same as for os.waitpid().
855 while time.time() < deadline:
856 pid, status = os.waitpid(pid, os.WNOHANG)
863 def waitpid_handle_exceptions(pid, deadline):
864 """Wrapper around os.waitpid()/waitpid_with_timeout(), which waits until
865 either a child process exits or the deadline elapses, and retries if certain
869 pid: Process ID to wait for, or -1 to wait for any child process.
870 deadline: If non-zero, waiting stops when time.time() exceeds this value.
871 If zero, waiting stops when a child process exits.
874 (pid, status): Same as for waitpid_with_timeout(). |pid| is non-zero if and
875 only if a child exited during the wait.
878 Same as for os.waitpid(), except:
879 OSError with errno==EINTR causes the wait to be retried (this can happen,
880 for example, if this parent process receives SIGHUP).
881 OSError with errno==ECHILD means there are no child processes, and so
882 this function sleeps until |deadline|. If |deadline| is zero, this is an
883 error and the OSError exception is raised in this case.
888 pid_result, status = os.waitpid(pid, 0)
890 pid_result, status = waitpid_with_timeout(pid, deadline)
891 return (pid_result, status)
893 if e.errno == errno.EINTR:
895 elif e.errno == errno.ECHILD:
898 # No time-limit and no child processes. This is treated as an error
902 time.sleep(deadline - now)
905 # Anything else is an unexpected error.
910 EPILOG = """This script is not intended for use by end-users. To configure
911 Chrome Remote Desktop, please install the app from the Chrome
912 Web Store: https://chrome.google.com/remotedesktop"""
913 parser = optparse.OptionParser(
914 usage="Usage: %prog [options] [ -- [ X server options ] ]",
916 parser.add_option("-s", "--size", dest="size", action="append",
917 help="Dimensions of virtual desktop. This can be specified "
918 "multiple times to make multiple screen resolutions "
919 "available (if the Xvfb server supports this).")
920 parser.add_option("-f", "--foreground", dest="foreground", default=False,
922 help="Don't run as a background daemon.")
923 parser.add_option("", "--start", dest="start", default=False,
925 help="Start the host.")
926 parser.add_option("-k", "--stop", dest="stop", default=False,
928 help="Stop the daemon currently running.")
929 parser.add_option("", "--get-status", dest="get_status", default=False,
931 help="Prints host status")
932 parser.add_option("", "--check-running", dest="check_running", default=False,
934 help="Return 0 if the daemon is running, or 1 otherwise.")
935 parser.add_option("", "--config", dest="config", action="store",
936 help="Use the specified configuration file.")
937 parser.add_option("", "--reload", dest="reload", default=False,
939 help="Signal currently running host to reload the config.")
940 parser.add_option("", "--add-user", dest="add_user", default=False,
942 help="Add current user to the chrome-remote-desktop group.")
943 parser.add_option("", "--host-version", dest="host_version", default=False,
945 help="Prints version of the host.")
946 (options, args) = parser.parse_args()
948 # Determine the filename of the host configuration and PID files.
949 if not options.config:
950 options.config = os.path.join(CONFIG_DIR, "host#%s.json" % g_host_hash)
952 # Check for a modal command-line option (start, stop, etc.)
954 if options.get_status:
955 pid = get_daemon_pid()
958 elif is_supported_platform():
961 print "NOT_IMPLEMENTED"
964 # TODO(sergeyu): Remove --check-running once NPAPI plugin and NM host are
965 # updated to always use get-status flag instead.
966 if options.check_running:
967 pid = get_daemon_pid()
968 return 0 if pid != 0 else 1
971 pid = get_daemon_pid()
973 print "The daemon is not currently running"
975 print "Killing process %s" % pid
976 os.kill(pid, signal.SIGTERM)
980 pid = get_daemon_pid()
983 os.kill(pid, signal.SIGHUP)
987 user = getpass.getuser()
989 if user in grp.getgrnam(CHROME_REMOTING_GROUP_NAME).gr_mem:
990 logging.info("User '%s' is already a member of '%s'." %
991 (user, CHROME_REMOTING_GROUP_NAME))
994 logging.info("Group '%s' not found." % CHROME_REMOTING_GROUP_NAME)
996 if os.getenv("DISPLAY"):
997 sudo_command = "gksudo --description \"Chrome Remote Desktop\""
999 sudo_command = "sudo"
1000 command = ("sudo -k && exec %(sudo)s -- sh -c "
1001 "\"groupadd -f %(group)s && gpasswd --add %(user)s %(group)s\"" %
1002 { 'group': CHROME_REMOTING_GROUP_NAME,
1004 'sudo': sudo_command })
1005 os.execv("/bin/sh", ["/bin/sh", "-c", command])
1008 if options.host_version:
1009 # TODO(sergeyu): Also check RPM package version once we add RPM package.
1010 return os.system(locate_executable(HOST_BINARY_NAME) + " --version") >> 8
1012 if not options.start:
1013 # If no modal command-line options specified, print an error and exit.
1014 print >> sys.stderr, EPILOG
1017 # Collate the list of sizes that XRANDR should support.
1018 if not options.size:
1019 default_sizes = DEFAULT_SIZES
1020 if os.environ.has_key(DEFAULT_SIZES_ENV_VAR):
1021 default_sizes = os.environ[DEFAULT_SIZES_ENV_VAR]
1022 options.size = default_sizes.split(",")
1025 for size in options.size:
1026 size_components = size.split("x")
1027 if len(size_components) != 2:
1028 parser.error("Incorrect size format '%s', should be WIDTHxHEIGHT" % size)
1031 width = int(size_components[0])
1032 height = int(size_components[1])
1034 # Enforce minimum desktop size, as a sanity-check. The limit of 100 will
1035 # detect typos of 2 instead of 3 digits.
1036 if width < 100 or height < 100:
1039 parser.error("Width and height should be 100 pixels or greater")
1041 sizes.append((width, height))
1043 # Register an exit handler to clean up session process and the PID file.
1044 atexit.register(cleanup)
1046 # Load the initial host configuration.
1047 host_config = Config(options.config)
1050 except (IOError, ValueError) as e:
1051 print >> sys.stderr, "Failed to load config: " + str(e)
1054 # Register handler to re-load the configuration in response to signals.
1055 for s in [signal.SIGHUP, signal.SIGINT, signal.SIGTERM]:
1056 signal.signal(s, SignalHandler(host_config))
1058 # Verify that the initial host configuration has the necessary fields.
1059 auth = Authentication()
1060 auth_config_valid = auth.copy_from(host_config)
1062 host_config_valid = host.copy_from(host_config)
1063 if not host_config_valid or not auth_config_valid:
1064 logging.error("Failed to load host configuration.")
1067 # Determine whether a desktop is already active for the specified host
1068 # host configuration.
1069 pid = get_daemon_pid()
1071 # Debian policy requires that services should "start" cleanly and return 0
1072 # if they are already running.
1073 print "Service already running."
1076 # Detach a separate "daemon" process to run the session, unless specifically
1077 # requested to run in the foreground.
1078 if not options.foreground:
1081 logging.info("Using host_id: " + host.host_id)
1083 desktop = Desktop(sizes)
1085 # Keep track of the number of consecutive failures of any child process to
1086 # run for longer than a set period of time. The script will exit after a
1087 # threshold is exceeded.
1088 # There is no point in tracking the X session process separately, since it is
1089 # launched at (roughly) the same time as the X server, and the termination of
1090 # one of these triggers the termination of the other.
1091 x_server_inhibitor = RelaunchInhibitor("X server")
1092 host_inhibitor = RelaunchInhibitor("host")
1093 all_inhibitors = [x_server_inhibitor, host_inhibitor]
1095 # Don't allow relaunching the script on the first loop iteration.
1096 allow_relaunch_self = False
1099 # Set the backoff interval and exit if a process failed too many times.
1100 backoff_time = SHORT_BACKOFF_TIME
1101 for inhibitor in all_inhibitors:
1102 if inhibitor.failures >= MAX_LAUNCH_FAILURES:
1103 logging.error("Too many launch failures of '%s', exiting."
1106 elif inhibitor.failures >= SHORT_BACKOFF_THRESHOLD:
1107 backoff_time = LONG_BACKOFF_TIME
1111 # If the session process or X server stops running (e.g. because the user
1112 # logged out), kill the other. This will trigger the next conditional block
1113 # as soon as os.waitpid() reaps its exit-code.
1114 if desktop.session_proc is None and desktop.x_proc is not None:
1115 logging.info("Terminating X server")
1116 desktop.x_proc.terminate()
1117 elif desktop.x_proc is None and desktop.session_proc is not None:
1118 logging.info("Terminating X session")
1119 desktop.session_proc.terminate()
1120 elif desktop.x_proc is None and desktop.session_proc is None:
1121 # Both processes have terminated.
1122 if (allow_relaunch_self and x_server_inhibitor.failures == 0 and
1123 host_inhibitor.failures == 0):
1124 # Since the user's desktop is already gone at this point, there's no
1125 # state to lose and now is a good time to pick up any updates to this
1126 # script that might have been installed.
1127 logging.info("Relaunching self")
1130 # If there is a non-zero |failures| count, restarting the whole script
1131 # would lose this information, so just launch the session as normal.
1132 if x_server_inhibitor.is_inhibited():
1133 logging.info("Waiting before launching X server")
1134 relaunch_times.append(x_server_inhibitor.earliest_relaunch_time)
1136 logging.info("Launching X server and X session.")
1137 desktop.launch_session(args)
1138 x_server_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME,
1140 allow_relaunch_self = True
1142 if desktop.host_proc is None:
1143 if host_inhibitor.is_inhibited():
1144 logging.info("Waiting before launching host process")
1145 relaunch_times.append(host_inhibitor.earliest_relaunch_time)
1147 logging.info("Launching host process")
1148 desktop.launch_host(host_config)
1149 host_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME,
1152 deadline = min(relaunch_times) if relaunch_times else 0
1153 pid, status = waitpid_handle_exceptions(-1, deadline)
1157 logging.info("wait() returned (%s,%s)" % (pid, status))
1159 # When a process has terminated, and we've reaped its exit-code, any Popen
1160 # instance for that process is no longer valid. Reset any affected instance
1162 if desktop.x_proc is not None and pid == desktop.x_proc.pid:
1163 logging.info("X server process terminated")
1164 desktop.x_proc = None
1165 x_server_inhibitor.record_stopped()
1167 if desktop.session_proc is not None and pid == desktop.session_proc.pid:
1168 logging.info("Session process terminated")
1169 desktop.session_proc = None
1171 if desktop.host_proc is not None and pid == desktop.host_proc.pid:
1172 logging.info("Host process terminated")
1173 desktop.host_proc = None
1174 desktop.host_ready = False
1175 host_inhibitor.record_stopped()
1177 # These exit-codes must match the ones used by the host.
1178 # See remoting/host/host_error_codes.h.
1179 # Delete the host or auth configuration depending on the returned error
1180 # code, so the next time this script is run, a new configuration
1181 # will be created and registered.
1182 if os.WIFEXITED(status):
1183 if os.WEXITSTATUS(status) == 100:
1184 logging.info("Host configuration is invalid - exiting.")
1186 elif os.WEXITSTATUS(status) == 101:
1187 logging.info("Host ID has been deleted - exiting.")
1189 host_config.save_and_log_errors()
1191 elif os.WEXITSTATUS(status) == 102:
1192 logging.info("OAuth credentials are invalid - exiting.")
1194 elif os.WEXITSTATUS(status) == 103:
1195 logging.info("Host domain is blocked by policy - exiting.")
1197 # Nothing to do for Mac-only status 104 (login screen unsupported)
1198 elif os.WEXITSTATUS(status) == 105:
1199 logging.info("Username is blocked by policy - exiting.")
1202 logging.info("Host exited with status %s." % os.WEXITSTATUS(status))
1203 elif os.WIFSIGNALED(status):
1204 logging.info("Host terminated by signal %s." % os.WTERMSIG(status))
1207 if __name__ == "__main__":
1208 logging.basicConfig(level=logging.DEBUG,
1209 format="%(asctime)s:%(levelname)s:%(message)s")