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 maximum size that is large enough to support clients
41 # with large or multiple monitors. This is a comma-separated list of
42 # resolutions that will be made available if the X server supports RANDR. These
43 # defaults can be overridden in ~/.profile.
44 DEFAULT_SIZES = "1600x1200,3840x1600"
46 # If RANDR is not available, use a smaller default size. Only a single
47 # resolution is supported in this case.
48 DEFAULT_SIZE_NO_RANDR = "1600x1200"
50 SCRIPT_PATH = sys.path[0]
52 IS_INSTALLED = (os.path.basename(sys.argv[0]) != 'linux_me2me_host.py')
55 HOST_BINARY_NAME = "chrome-remote-desktop-host"
57 HOST_BINARY_NAME = "remoting_me2me_host"
59 CHROME_REMOTING_GROUP_NAME = "chrome-remote-desktop"
61 HOME_DIR = os.environ["HOME"]
62 CONFIG_DIR = os.path.join(HOME_DIR, ".config/chrome-remote-desktop")
63 SESSION_FILE_PATH = os.path.join(HOME_DIR, ".chrome-remote-desktop-session")
64 SYSTEM_SESSION_FILE_PATH = "/etc/chrome-remote-desktop-session"
66 X_LOCK_FILE_TEMPLATE = "/tmp/.X%d-lock"
67 FIRST_X_DISPLAY_NUMBER = 20
69 # Amount of time to wait between relaunching processes.
70 SHORT_BACKOFF_TIME = 5
71 LONG_BACKOFF_TIME = 60
73 # How long a process must run in order not to be counted against the restart
75 MINIMUM_PROCESS_LIFETIME = 60
77 # Thresholds for switching from fast- to slow-restart and for giving up
78 # trying to restart entirely.
79 SHORT_BACKOFF_THRESHOLD = 5
80 MAX_LAUNCH_FAILURES = SHORT_BACKOFF_THRESHOLD + 10
82 # Globals needed by the atexit cleanup() handler.
84 g_host_hash = hashlib.md5(socket.gethostname()).hexdigest()
87 def is_supported_platform():
88 # Always assume that the system is supported if the config directory or
90 if (os.path.isdir(CONFIG_DIR) or os.path.isfile(SESSION_FILE_PATH) or
91 os.path.isfile(SYSTEM_SESSION_FILE_PATH)):
94 # The host has been tested only on Ubuntu.
95 distribution = platform.linux_distribution()
96 return (distribution[0]).lower() == 'ubuntu'
99 def get_randr_supporting_x_server():
100 """Returns a path to an X server that supports the RANDR extension, if this
101 is found on the system. Otherwise returns None."""
103 xvfb = "/usr/bin/Xvfb-randr"
104 if not os.path.exists(xvfb):
105 xvfb = locate_executable("Xvfb-randr")
112 def __init__(self, path):
118 """Loads the config from file.
121 IOError: Error reading data
122 ValueError: Error parsing JSON
124 settings_file = open(self.path, 'r')
125 self.data = json.load(settings_file)
127 settings_file.close()
130 """Saves the config to file.
133 IOError: Error writing data
134 TypeError: Error serialising JSON
138 old_umask = os.umask(0066)
140 settings_file = open(self.path, 'w')
141 settings_file.write(json.dumps(self.data, indent=2))
142 settings_file.close()
147 def save_and_log_errors(self):
148 """Calls self.save(), trapping and logging any errors."""
151 except (IOError, TypeError) as e:
152 logging.error("Failed to save config: " + str(e))
155 return self.data.get(key)
157 def __getitem__(self, key):
158 return self.data[key]
160 def __setitem__(self, key, value):
161 self.data[key] = value
169 class Authentication:
170 """Manage authentication tokens for Chromoting/xmpp"""
174 self.oauth_refresh_token = None
176 def copy_from(self, config):
177 """Loads the config and returns false if the config is invalid."""
179 self.login = config["xmpp_login"]
180 self.oauth_refresh_token = config["oauth_refresh_token"]
185 def copy_to(self, config):
186 config["xmpp_login"] = self.login
187 config["oauth_refresh_token"] = self.oauth_refresh_token
191 """This manages the configuration for a host."""
194 self.host_id = str(uuid.uuid1())
195 self.host_name = socket.gethostname()
196 self.host_secret_hash = None
197 self.private_key = None
199 def copy_from(self, config):
201 self.host_id = config["host_id"]
202 self.host_name = config["host_name"]
203 self.host_secret_hash = config.get("host_secret_hash")
204 self.private_key = config["private_key"]
209 def copy_to(self, config):
210 config["host_id"] = self.host_id
211 config["host_name"] = self.host_name
212 config["host_secret_hash"] = self.host_secret_hash
213 config["private_key"] = self.private_key
217 """Manage a single virtual desktop"""
219 def __init__(self, sizes):
221 self.session_proc = None
222 self.host_proc = None
223 self.child_env = None
225 self.pulseaudio_pipe = None
226 self.server_supports_exact_resize = False
227 self.host_ready = False
228 self.ssh_auth_sockname = None
229 g_desktops.append(self)
232 def get_unused_display_number():
233 """Return a candidate display number for which there is currently no
234 X Server lock file"""
235 display = FIRST_X_DISPLAY_NUMBER
236 while os.path.exists(X_LOCK_FILE_TEMPLATE % display):
240 def _init_child_env(self):
241 # Create clean environment for new session, so it is cleanly separated from
242 # the user's console X session.
254 if os.environ.has_key(key):
255 self.child_env[key] = os.environ[key]
257 # Read from /etc/environment if it exists, as it is a standard place to
258 # store system-wide environment settings. During a normal login, this would
259 # typically be done by the pam_env PAM module, depending on the local PAM
261 env_filename = "/etc/environment"
263 with open(env_filename, "r") as env_file:
264 for line in env_file:
265 line = line.rstrip("\n")
266 # Split at the first "=", leaving any further instances in the value.
267 key_value_pair = line.split("=", 1)
268 if len(key_value_pair) == 2:
269 key, value = tuple(key_value_pair)
270 # The file stores key=value assignments, but the value may be
271 # quoted, so strip leading & trailing quotes from it.
272 value = value.strip("'\"")
273 self.child_env[key] = value
275 logging.info("Failed to read %s, skipping." % env_filename)
277 def _setup_pulseaudio(self):
278 self.pulseaudio_pipe = None
280 # pulseaudio uses UNIX sockets for communication. Length of UNIX socket
281 # name is limited to 108 characters, so audio will not work properly if
282 # the path is too long. To workaround this problem we use only first 10
283 # symbols of the host hash.
284 pulse_path = os.path.join(CONFIG_DIR,
285 "pulseaudio#%s" % g_host_hash[0:10])
286 if len(pulse_path) + len("/native") >= 108:
287 logging.error("Audio will not be enabled because pulseaudio UNIX " +
288 "socket path is too long.")
291 sink_name = "chrome_remote_desktop_session"
292 pipe_name = os.path.join(pulse_path, "fifo_output")
295 if not os.path.exists(pulse_path):
297 if not os.path.exists(pipe_name):
300 logging.error("Failed to create pulseaudio pipe: " + str(e))
304 pulse_config = open(os.path.join(pulse_path, "daemon.conf"), "w")
305 pulse_config.write("default-sample-format = s16le\n")
306 pulse_config.write("default-sample-rate = 48000\n")
307 pulse_config.write("default-sample-channels = 2\n")
310 pulse_script = open(os.path.join(pulse_path, "default.pa"), "w")
311 pulse_script.write("load-module module-native-protocol-unix\n")
313 ("load-module module-pipe-sink sink_name=%s file=\"%s\" " +
314 "rate=48000 channels=2 format=s16le\n") %
315 (sink_name, pipe_name))
318 logging.error("Failed to write pulseaudio config: " + str(e))
321 self.child_env["PULSE_CONFIG_PATH"] = pulse_path
322 self.child_env["PULSE_RUNTIME_PATH"] = pulse_path
323 self.child_env["PULSE_STATE_PATH"] = pulse_path
324 self.child_env["PULSE_SINK"] = sink_name
325 self.pulseaudio_pipe = pipe_name
329 def _setup_gnubby(self):
330 self.ssh_auth_sockname = ("/tmp/chromoting.%s.ssh_auth_sock" %
333 def _launch_x_server(self, extra_x_args):
334 x_auth_file = os.path.expanduser("~/.Xauthority")
335 self.child_env["XAUTHORITY"] = x_auth_file
336 devnull = open(os.devnull, "rw")
337 display = self.get_unused_display_number()
339 # Run "xauth add" with |child_env| so that it modifies the same XAUTHORITY
340 # file which will be used for the X session.
341 ret_code = subprocess.call("xauth add :%d . `mcookie`" % display,
342 env=self.child_env, shell=True)
344 raise Exception("xauth failed with code %d" % ret_code)
346 max_width = max([width for width, height in self.sizes])
347 max_height = max([height for width, height in self.sizes])
349 xvfb = get_randr_supporting_x_server()
351 self.server_supports_exact_resize = True
354 self.server_supports_exact_resize = False
356 # Disable the Composite extension iff the X session is the default
357 # Unity-2D, since it uses Metacity which fails to generate DAMAGE
358 # notifications correctly. See crbug.com/166468.
359 x_session = choose_x_session()
360 if (len(x_session) == 2 and
361 x_session[1] == "/usr/bin/gnome-session --session=ubuntu-2d"):
362 extra_x_args.extend(["-extension", "Composite"])
364 logging.info("Starting %s on display :%d" % (xvfb, display))
365 screen_option = "%dx%dx24" % (max_width, max_height)
366 self.x_proc = subprocess.Popen(
367 [xvfb, ":%d" % display,
368 "-auth", x_auth_file,
371 "-screen", "0", screen_option
373 if not self.x_proc.pid:
374 raise Exception("Could not start Xvfb.")
376 self.child_env["DISPLAY"] = ":%d" % display
377 self.child_env["CHROME_REMOTE_DESKTOP_SESSION"] = "1"
379 # Use a separate profile for any instances of Chrome that are started in
380 # the virtual session. Chrome doesn't support sharing a profile between
381 # multiple DISPLAYs, but Chrome Sync allows for a reasonable compromise.
382 chrome_profile = os.path.join(CONFIG_DIR, "chrome-profile")
383 self.child_env["CHROME_USER_DATA_DIR"] = chrome_profile
385 # Set SSH_AUTH_SOCK to the file name to listen on.
386 if self.ssh_auth_sockname:
387 self.child_env["SSH_AUTH_SOCK"] = self.ssh_auth_sockname
389 # Wait for X to be active.
390 for _test in range(5):
391 proc = subprocess.Popen("xdpyinfo", env=self.child_env, stdout=devnull)
392 _pid, retcode = os.waitpid(proc.pid, 0)
397 raise Exception("Could not connect to Xvfb.")
399 logging.info("Xvfb is active.")
401 # The remoting host expects the server to use "evdev" keycodes, but Xvfb
402 # starts configured to use the "base" ruleset, resulting in XKB configuring
403 # for "xfree86" keycodes, and screwing up some keys. See crbug.com/119013.
404 # Reconfigure the X server to use "evdev" keymap rules. The X server must
405 # be started with -noreset otherwise it'll reset as soon as the command
406 # completes, since there are no other X clients running yet.
407 proc = subprocess.Popen("setxkbmap -rules evdev", env=self.child_env,
409 _pid, retcode = os.waitpid(proc.pid, 0)
411 logging.error("Failed to set XKB to 'evdev'")
413 # Register the screen sizes if the X server's RANDR extension supports it.
414 # Errors here are non-fatal; the X server will continue to run with the
415 # dimensions from the "-screen" option.
416 for width, height in self.sizes:
417 label = "%dx%d" % (width, height)
418 args = ["xrandr", "--newmode", label, "0", str(width), "0", "0", "0",
419 str(height), "0", "0", "0"]
420 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
421 args = ["xrandr", "--addmode", "screen", label]
422 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
424 # Set the initial mode to the first size specified, otherwise the X server
425 # would default to (max_width, max_height), which might not even be in the
427 label = "%dx%d" % self.sizes[0]
428 args = ["xrandr", "-s", label]
429 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
431 # Set the physical size of the display so that the initial mode is running
432 # at approximately 96 DPI, since some desktops require the DPI to be set to
433 # something realistic.
434 args = ["xrandr", "--dpi", "96"]
435 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
439 def _launch_x_session(self):
440 # Start desktop session.
441 # The /dev/null input redirection is necessary to prevent the X session
442 # reading from stdin. If this code runs as a shell background job in a
443 # terminal, any reading from stdin causes the job to be suspended.
444 # Daemonization would solve this problem by separating the process from the
445 # controlling terminal.
446 xsession_command = choose_x_session()
447 if xsession_command is None:
448 raise Exception("Unable to choose suitable X session command.")
450 logging.info("Launching X session: %s" % xsession_command)
451 self.session_proc = subprocess.Popen(xsession_command,
452 stdin=open(os.devnull, "r"),
455 if not self.session_proc.pid:
456 raise Exception("Could not start X session")
458 def launch_session(self, x_args):
459 self._init_child_env()
460 self._setup_pulseaudio()
462 self._launch_x_server(x_args)
463 self._launch_x_session()
465 def launch_host(self, host_config):
466 # Start remoting host
467 args = [locate_executable(HOST_BINARY_NAME), "--host-config=-"]
468 if self.pulseaudio_pipe:
469 args.append("--audio-pipe-name=%s" % self.pulseaudio_pipe)
470 if self.server_supports_exact_resize:
471 args.append("--server-supports-exact-resize")
472 if self.ssh_auth_sockname:
473 args.append("--ssh-auth-sockname=%s" % self.ssh_auth_sockname)
475 # Have the host process use SIGUSR1 to signal a successful start.
476 def sigusr1_handler(signum, frame):
478 logging.info("Host ready to receive connections.")
479 self.host_ready = True
480 if (ParentProcessLogger.instance() and
481 False not in [desktop.host_ready for desktop in g_desktops]):
482 ParentProcessLogger.instance().release_parent()
484 signal.signal(signal.SIGUSR1, sigusr1_handler)
485 args.append("--signal-parent")
487 self.host_proc = subprocess.Popen(args, env=self.child_env,
488 stdin=subprocess.PIPE)
490 if not self.host_proc.pid:
491 raise Exception("Could not start Chrome Remote Desktop host")
492 self.host_proc.stdin.write(json.dumps(host_config.data))
493 self.host_proc.stdin.close()
496 def get_daemon_pid():
497 """Checks if there is already an instance of this script running, and returns
501 The process ID of the existing daemon process, or 0 if the daemon is not
505 this_pid = os.getpid()
507 # Support new & old psutil API. This is the right way to check, according to
508 # http://grodola.blogspot.com/2014/01/psutil-20-porting.html
509 if psutil.version_info >= (2, 0):
510 psget = lambda x: x()
514 for process in psutil.process_iter():
515 # Skip any processes that raise an exception, as processes may terminate
516 # during iteration over the list.
518 # Skip other users' processes.
519 if psget(process.uids).real != uid:
522 # Skip the process for this instance.
523 if process.pid == this_pid:
526 # |cmdline| will be [python-interpreter, script-file, other arguments...]
527 cmdline = psget(process.cmdline)
530 if cmdline[0] == sys.executable and cmdline[1] == sys.argv[0]:
532 except (psutil.NoSuchProcess, psutil.AccessDenied):
538 def choose_x_session():
539 """Chooses the most appropriate X session command for this system.
542 A string containing the command to run, or a list of strings containing
543 the executable program and its arguments, which is suitable for passing as
544 the first parameter of subprocess.Popen(). If a suitable session cannot
545 be found, returns None.
549 SYSTEM_SESSION_FILE_PATH ]
550 for startup_file in XSESSION_FILES:
551 startup_file = os.path.expanduser(startup_file)
552 if os.path.exists(startup_file):
553 # Use the same logic that a Debian system typically uses with ~/.xsession
554 # (see /etc/X11/Xsession.d/50x11-common_determine-startup), to determine
555 # exactly how to run this file.
556 if os.access(startup_file, os.X_OK):
557 # "/bin/sh -c" is smart about how to execute the session script and
558 # works in cases where plain exec() fails (for example, if the file is
559 # marked executable, but is a plain script with no shebang line).
560 return ["/bin/sh", "-c", pipes.quote(startup_file)]
562 shell = os.environ.get("SHELL", "sh")
563 return [shell, startup_file]
565 # Choose a session wrapper script to run the session. On some systems,
566 # /etc/X11/Xsession fails to load the user's .profile, so look for an
567 # alternative wrapper that is more likely to match the script that the
568 # system actually uses for console desktop sessions.
570 "/usr/sbin/lightdm-session",
572 "/etc/X11/Xsession" ]
573 for session_wrapper in SESSION_WRAPPERS:
574 if os.path.exists(session_wrapper):
575 if os.path.exists("/usr/bin/unity-2d-panel"):
576 # On Ubuntu 12.04, the default session relies on 3D-accelerated
577 # hardware. Trying to run this with a virtual X display produces
578 # weird results on some systems (for example, upside-down and
579 # corrupt displays). So if the ubuntu-2d session is available,
580 # choose it explicitly.
581 return [session_wrapper, "/usr/bin/gnome-session --session=ubuntu-2d"]
583 # Use the session wrapper by itself, and let the system choose a
585 return [session_wrapper]
589 def locate_executable(exe_name):
591 # If the script is running from its installed location, search the host
592 # binary only in the same directory.
593 paths_to_try = [ SCRIPT_PATH ]
595 paths_to_try = map(lambda p: os.path.join(SCRIPT_PATH, p),
596 [".", "../../../out/Debug", "../../../out/Release" ])
597 for path in paths_to_try:
598 exe_path = os.path.join(path, exe_name)
599 if os.path.exists(exe_path):
602 raise Exception("Could not locate executable '%s'" % exe_name)
605 class ParentProcessLogger(object):
606 """Redirects logs to the parent process, until the host is ready or quits.
608 This class creates a pipe to allow logging from the daemon process to be
609 copied to the parent process. The daemon process adds a log-handler that
610 directs logging output to the pipe. The parent process reads from this pipe
611 until and writes the content to stderr. When the pipe is no longer needed
612 (for example, the host signals successful launch or permanent failure), the
613 daemon removes the log-handler and closes the pipe, causing the the parent
614 process to reach end-of-file while reading the pipe and exit.
616 The (singleton) logger should be instantiated before forking. The parent
617 process should call wait_for_logs() before exiting. The (grand-)child process
618 should call start_logging() when it starts, and then use logging.* to issue
619 log statements, as usual. When the child has either succesfully started the
620 host or terminated, it must call release_parent() to allow the parent to exit.
626 """Constructor. Must be called before forking."""
627 read_pipe, write_pipe = os.pipe()
628 # Ensure write_pipe is closed on exec, otherwise it will be kept open by
629 # child processes (X, host), preventing the read pipe from EOF'ing.
630 old_flags = fcntl.fcntl(write_pipe, fcntl.F_GETFD)
631 fcntl.fcntl(write_pipe, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
632 self._read_file = os.fdopen(read_pipe, 'r')
633 self._write_file = os.fdopen(write_pipe, 'a')
634 self._logging_handler = None
635 ParentProcessLogger.__instance = self
637 def start_logging(self):
638 """Installs a logging handler that sends log entries to a pipe.
640 Must be called by the child process.
642 self._read_file.close()
643 self._logging_handler = logging.StreamHandler(self._write_file)
644 logging.getLogger().addHandler(self._logging_handler)
646 def release_parent(self):
647 """Uninstalls logging handler and closes the pipe, releasing the parent.
649 Must be called by the child process.
651 if self._logging_handler:
652 logging.getLogger().removeHandler(self._logging_handler)
653 self._logging_handler = None
654 if not self._write_file.closed:
655 self._write_file.close()
657 def wait_for_logs(self):
658 """Waits and prints log lines from the daemon until the pipe is closed.
660 Must be called by the parent process.
662 # If Ctrl-C is pressed, inform the user that the daemon is still running.
663 # This signal will cause the read loop below to stop with an EINTR IOError.
664 def sigint_handler(signum, frame):
666 print >> sys.stderr, ("Interrupted. The daemon is still running in the "
669 signal.signal(signal.SIGINT, sigint_handler)
671 # Install a fallback timeout to release the parent process, in case the
672 # daemon never responds (e.g. host crash-looping, daemon killed).
673 # This signal will cause the read loop below to stop with an EINTR IOError.
674 def sigalrm_handler(signum, frame):
676 print >> sys.stderr, ("No response from daemon. It may have crashed, or "
677 "may still be running in the background.")
679 signal.signal(signal.SIGALRM, sigalrm_handler)
682 self._write_file.close()
684 # Print lines as they're logged to the pipe until EOF is reached or readline
685 # is interrupted by one of the signal handlers above.
687 for line in iter(self._read_file.readline, ''):
688 sys.stderr.write(line)
690 if e.errno != errno.EINTR:
692 print >> sys.stderr, "Log file: %s" % os.environ[LOG_FILE_ENV_VAR]
696 """Returns the singleton instance, if it exists."""
697 return ParentProcessLogger.__instance
701 """Background this process and detach from controlling terminal, redirecting
702 stdout/stderr to a log file."""
704 # TODO(lambroslambrou): Having stdout/stderr redirected to a log file is not
705 # ideal - it could create a filesystem DoS if the daemon or a child process
706 # were to write excessive amounts to stdout/stderr. Ideally, stdout/stderr
707 # should be redirected to a pipe or socket, and a process at the other end
708 # should consume the data and write it to a logging facility which can do
709 # data-capping or log-rotation. The 'logger' command-line utility could be
710 # used for this, but it might cause too much syslog spam.
712 # Create new (temporary) file-descriptors before forking, so any errors get
713 # reported to the main process and set the correct exit-code.
714 # The mode is provided, since Python otherwise sets a default mode of 0777,
715 # which would result in the new file having permissions of 0777 & ~umask,
716 # possibly leaving the executable bits set.
717 if not os.environ.has_key(LOG_FILE_ENV_VAR):
718 log_file_prefix = "chrome_remote_desktop_%s_" % time.strftime(
719 '%Y%m%d_%H%M%S', time.localtime(time.time()))
720 log_file = tempfile.NamedTemporaryFile(prefix=log_file_prefix, delete=False)
721 os.environ[LOG_FILE_ENV_VAR] = log_file.name
722 log_fd = log_file.file.fileno()
724 log_fd = os.open(os.environ[LOG_FILE_ENV_VAR],
725 os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0600)
727 devnull_fd = os.open(os.devnull, os.O_RDONLY)
729 parent_logger = ParentProcessLogger()
737 # The second fork ensures that the daemon isn't a session leader, so that
738 # it doesn't acquire a controlling terminal.
746 os._exit(0) # pylint: disable=W0212
749 parent_logger.wait_for_logs()
750 os._exit(0) # pylint: disable=W0212
752 logging.info("Daemon process started in the background, logging to '%s'" %
753 os.environ[LOG_FILE_ENV_VAR])
757 parent_logger.start_logging()
759 # Copy the file-descriptors to create new stdin, stdout and stderr. Note
760 # that dup2(oldfd, newfd) closes newfd first, so this will close the current
761 # stdin, stdout and stderr, detaching from the terminal.
762 os.dup2(devnull_fd, sys.stdin.fileno())
763 os.dup2(log_fd, sys.stdout.fileno())
764 os.dup2(log_fd, sys.stderr.fileno())
766 # Close the temporary file-descriptors.
772 logging.info("Cleanup.")
775 for desktop in g_desktops:
777 logging.info("Terminating Xvfb")
778 desktop.x_proc.terminate()
780 if ParentProcessLogger.instance():
781 ParentProcessLogger.instance().release_parent()
784 """Reload the config file on SIGHUP. Since we pass the configuration to the
785 host processes via stdin, they can't reload it, so terminate them. They will
786 be relaunched automatically with the new config."""
788 def __init__(self, host_config):
789 self.host_config = host_config
791 def __call__(self, signum, _stackframe):
792 if signum == signal.SIGHUP:
793 logging.info("SIGHUP caught, restarting host.")
795 self.host_config.load()
796 except (IOError, ValueError) as e:
797 logging.error("Failed to load config: " + str(e))
798 for desktop in g_desktops:
799 if desktop.host_proc:
800 desktop.host_proc.send_signal(signal.SIGTERM)
802 # Exit cleanly so the atexit handler, cleanup(), gets called.
806 class RelaunchInhibitor:
807 """Helper class for inhibiting launch of a child process before a timeout has
810 A managed process can be in one of these states:
811 running, not inhibited (running == True)
812 stopped and inhibited (running == False and is_inhibited() == True)
813 stopped but not inhibited (running == False and is_inhibited() == False)
816 label: Name of the tracked process. Only used for logging.
817 running: Whether the process is currently running.
818 earliest_relaunch_time: Time before which the process should not be
819 relaunched, or 0 if there is no limit.
820 failures: The number of times that the process ran for less than a
821 specified timeout, and had to be inhibited. This count is reset to 0
822 whenever the process has run for longer than the timeout.
825 def __init__(self, label):
828 self.earliest_relaunch_time = 0
829 self.earliest_successful_termination = 0
832 def is_inhibited(self):
833 return (not self.running) and (time.time() < self.earliest_relaunch_time)
835 def record_started(self, minimum_lifetime, relaunch_delay):
836 """Record that the process was launched, and set the inhibit time to
837 |timeout| seconds in the future."""
838 self.earliest_relaunch_time = time.time() + relaunch_delay
839 self.earliest_successful_termination = time.time() + minimum_lifetime
842 def record_stopped(self):
843 """Record that the process was stopped, and adjust the failure count
844 depending on whether the process ran long enough."""
846 if time.time() < self.earliest_successful_termination:
850 logging.info("Failure count for '%s' is now %d", self.label, self.failures)
855 os.execvp(sys.argv[0], sys.argv)
858 def waitpid_with_timeout(pid, deadline):
859 """Wrapper around os.waitpid() which waits until either a child process dies
860 or the deadline elapses.
863 pid: Process ID to wait for, or -1 to wait for any child process.
864 deadline: Waiting stops when time.time() exceeds this value.
867 (pid, status): Same as for os.waitpid(), except that |pid| is 0 if no child
868 changed state within the timeout.
871 Same as for os.waitpid().
873 while time.time() < deadline:
874 pid, status = os.waitpid(pid, os.WNOHANG)
881 def waitpid_handle_exceptions(pid, deadline):
882 """Wrapper around os.waitpid()/waitpid_with_timeout(), which waits until
883 either a child process exits or the deadline elapses, and retries if certain
887 pid: Process ID to wait for, or -1 to wait for any child process.
888 deadline: If non-zero, waiting stops when time.time() exceeds this value.
889 If zero, waiting stops when a child process exits.
892 (pid, status): Same as for waitpid_with_timeout(). |pid| is non-zero if and
893 only if a child exited during the wait.
896 Same as for os.waitpid(), except:
897 OSError with errno==EINTR causes the wait to be retried (this can happen,
898 for example, if this parent process receives SIGHUP).
899 OSError with errno==ECHILD means there are no child processes, and so
900 this function sleeps until |deadline|. If |deadline| is zero, this is an
901 error and the OSError exception is raised in this case.
906 pid_result, status = os.waitpid(pid, 0)
908 pid_result, status = waitpid_with_timeout(pid, deadline)
909 return (pid_result, status)
911 if e.errno == errno.EINTR:
913 elif e.errno == errno.ECHILD:
916 # No time-limit and no child processes. This is treated as an error
920 time.sleep(deadline - now)
923 # Anything else is an unexpected error.
928 EPILOG = """This script is not intended for use by end-users. To configure
929 Chrome Remote Desktop, please install the app from the Chrome
930 Web Store: https://chrome.google.com/remotedesktop"""
931 parser = optparse.OptionParser(
932 usage="Usage: %prog [options] [ -- [ X server options ] ]",
934 parser.add_option("-s", "--size", dest="size", action="append",
935 help="Dimensions of virtual desktop. This can be specified "
936 "multiple times to make multiple screen resolutions "
937 "available (if the Xvfb server supports this).")
938 parser.add_option("-f", "--foreground", dest="foreground", default=False,
940 help="Don't run as a background daemon.")
941 parser.add_option("", "--start", dest="start", default=False,
943 help="Start the host.")
944 parser.add_option("-k", "--stop", dest="stop", default=False,
946 help="Stop the daemon currently running.")
947 parser.add_option("", "--get-status", dest="get_status", default=False,
949 help="Prints host status")
950 parser.add_option("", "--check-running", dest="check_running", default=False,
952 help="Return 0 if the daemon is running, or 1 otherwise.")
953 parser.add_option("", "--config", dest="config", action="store",
954 help="Use the specified configuration file.")
955 parser.add_option("", "--reload", dest="reload", default=False,
957 help="Signal currently running host to reload the config.")
958 parser.add_option("", "--add-user", dest="add_user", default=False,
960 help="Add current user to the chrome-remote-desktop group.")
961 parser.add_option("", "--host-version", dest="host_version", default=False,
963 help="Prints version of the host.")
964 (options, args) = parser.parse_args()
966 # Determine the filename of the host configuration and PID files.
967 if not options.config:
968 options.config = os.path.join(CONFIG_DIR, "host#%s.json" % g_host_hash)
970 # Check for a modal command-line option (start, stop, etc.)
972 if options.get_status:
973 pid = get_daemon_pid()
976 elif is_supported_platform():
979 print "NOT_IMPLEMENTED"
982 # TODO(sergeyu): Remove --check-running once NPAPI plugin and NM host are
983 # updated to always use get-status flag instead.
984 if options.check_running:
985 pid = get_daemon_pid()
986 return 0 if pid != 0 else 1
989 pid = get_daemon_pid()
991 print "The daemon is not currently running"
993 print "Killing process %s" % pid
994 os.kill(pid, signal.SIGTERM)
998 pid = get_daemon_pid()
1001 os.kill(pid, signal.SIGHUP)
1004 if options.add_user:
1005 user = getpass.getuser()
1007 if user in grp.getgrnam(CHROME_REMOTING_GROUP_NAME).gr_mem:
1008 logging.info("User '%s' is already a member of '%s'." %
1009 (user, CHROME_REMOTING_GROUP_NAME))
1012 logging.info("Group '%s' not found." % CHROME_REMOTING_GROUP_NAME)
1014 if os.getenv("DISPLAY"):
1015 sudo_command = "gksudo --description \"Chrome Remote Desktop\""
1017 sudo_command = "sudo"
1018 command = ("sudo -k && exec %(sudo)s -- sh -c "
1019 "\"groupadd -f %(group)s && gpasswd --add %(user)s %(group)s\"" %
1020 { 'group': CHROME_REMOTING_GROUP_NAME,
1022 'sudo': sudo_command })
1023 os.execv("/bin/sh", ["/bin/sh", "-c", command])
1026 if options.host_version:
1027 # TODO(sergeyu): Also check RPM package version once we add RPM package.
1028 return os.system(locate_executable(HOST_BINARY_NAME) + " --version") >> 8
1030 if not options.start:
1031 # If no modal command-line options specified, print an error and exit.
1032 print >> sys.stderr, EPILOG
1035 # If a RANDR-supporting Xvfb is not available, limit the default size to
1036 # something more sensible.
1037 if get_randr_supporting_x_server():
1038 default_sizes = DEFAULT_SIZES
1040 default_sizes = DEFAULT_SIZE_NO_RANDR
1042 # Collate the list of sizes that XRANDR should support.
1043 if not options.size:
1044 if os.environ.has_key(DEFAULT_SIZES_ENV_VAR):
1045 default_sizes = os.environ[DEFAULT_SIZES_ENV_VAR]
1046 options.size = default_sizes.split(",")
1049 for size in options.size:
1050 size_components = size.split("x")
1051 if len(size_components) != 2:
1052 parser.error("Incorrect size format '%s', should be WIDTHxHEIGHT" % size)
1055 width = int(size_components[0])
1056 height = int(size_components[1])
1058 # Enforce minimum desktop size, as a sanity-check. The limit of 100 will
1059 # detect typos of 2 instead of 3 digits.
1060 if width < 100 or height < 100:
1063 parser.error("Width and height should be 100 pixels or greater")
1065 sizes.append((width, height))
1067 # Register an exit handler to clean up session process and the PID file.
1068 atexit.register(cleanup)
1070 # Load the initial host configuration.
1071 host_config = Config(options.config)
1074 except (IOError, ValueError) as e:
1075 print >> sys.stderr, "Failed to load config: " + str(e)
1078 # Register handler to re-load the configuration in response to signals.
1079 for s in [signal.SIGHUP, signal.SIGINT, signal.SIGTERM]:
1080 signal.signal(s, SignalHandler(host_config))
1082 # Verify that the initial host configuration has the necessary fields.
1083 auth = Authentication()
1084 auth_config_valid = auth.copy_from(host_config)
1086 host_config_valid = host.copy_from(host_config)
1087 if not host_config_valid or not auth_config_valid:
1088 logging.error("Failed to load host configuration.")
1091 # Determine whether a desktop is already active for the specified host
1092 # host configuration.
1093 pid = get_daemon_pid()
1095 # Debian policy requires that services should "start" cleanly and return 0
1096 # if they are already running.
1097 print "Service already running."
1100 # Detach a separate "daemon" process to run the session, unless specifically
1101 # requested to run in the foreground.
1102 if not options.foreground:
1105 logging.info("Using host_id: " + host.host_id)
1107 desktop = Desktop(sizes)
1109 # Keep track of the number of consecutive failures of any child process to
1110 # run for longer than a set period of time. The script will exit after a
1111 # threshold is exceeded.
1112 # There is no point in tracking the X session process separately, since it is
1113 # launched at (roughly) the same time as the X server, and the termination of
1114 # one of these triggers the termination of the other.
1115 x_server_inhibitor = RelaunchInhibitor("X server")
1116 host_inhibitor = RelaunchInhibitor("host")
1117 all_inhibitors = [x_server_inhibitor, host_inhibitor]
1119 # Don't allow relaunching the script on the first loop iteration.
1120 allow_relaunch_self = False
1123 # Set the backoff interval and exit if a process failed too many times.
1124 backoff_time = SHORT_BACKOFF_TIME
1125 for inhibitor in all_inhibitors:
1126 if inhibitor.failures >= MAX_LAUNCH_FAILURES:
1127 logging.error("Too many launch failures of '%s', exiting."
1130 elif inhibitor.failures >= SHORT_BACKOFF_THRESHOLD:
1131 backoff_time = LONG_BACKOFF_TIME
1135 # If the session process or X server stops running (e.g. because the user
1136 # logged out), kill the other. This will trigger the next conditional block
1137 # as soon as os.waitpid() reaps its exit-code.
1138 if desktop.session_proc is None and desktop.x_proc is not None:
1139 logging.info("Terminating X server")
1140 desktop.x_proc.terminate()
1141 elif desktop.x_proc is None and desktop.session_proc is not None:
1142 logging.info("Terminating X session")
1143 desktop.session_proc.terminate()
1144 elif desktop.x_proc is None and desktop.session_proc is None:
1145 # Both processes have terminated.
1146 if (allow_relaunch_self and x_server_inhibitor.failures == 0 and
1147 host_inhibitor.failures == 0):
1148 # Since the user's desktop is already gone at this point, there's no
1149 # state to lose and now is a good time to pick up any updates to this
1150 # script that might have been installed.
1151 logging.info("Relaunching self")
1154 # If there is a non-zero |failures| count, restarting the whole script
1155 # would lose this information, so just launch the session as normal.
1156 if x_server_inhibitor.is_inhibited():
1157 logging.info("Waiting before launching X server")
1158 relaunch_times.append(x_server_inhibitor.earliest_relaunch_time)
1160 logging.info("Launching X server and X session.")
1161 desktop.launch_session(args)
1162 x_server_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME,
1164 allow_relaunch_self = True
1166 if desktop.host_proc is None:
1167 if host_inhibitor.is_inhibited():
1168 logging.info("Waiting before launching host process")
1169 relaunch_times.append(host_inhibitor.earliest_relaunch_time)
1171 logging.info("Launching host process")
1172 desktop.launch_host(host_config)
1173 host_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME,
1176 deadline = min(relaunch_times) if relaunch_times else 0
1177 pid, status = waitpid_handle_exceptions(-1, deadline)
1181 logging.info("wait() returned (%s,%s)" % (pid, status))
1183 # When a process has terminated, and we've reaped its exit-code, any Popen
1184 # instance for that process is no longer valid. Reset any affected instance
1186 if desktop.x_proc is not None and pid == desktop.x_proc.pid:
1187 logging.info("X server process terminated")
1188 desktop.x_proc = None
1189 x_server_inhibitor.record_stopped()
1191 if desktop.session_proc is not None and pid == desktop.session_proc.pid:
1192 logging.info("Session process terminated")
1193 desktop.session_proc = None
1195 if desktop.host_proc is not None and pid == desktop.host_proc.pid:
1196 logging.info("Host process terminated")
1197 desktop.host_proc = None
1198 desktop.host_ready = False
1199 host_inhibitor.record_stopped()
1201 # These exit-codes must match the ones used by the host.
1202 # See remoting/host/host_error_codes.h.
1203 # Delete the host or auth configuration depending on the returned error
1204 # code, so the next time this script is run, a new configuration
1205 # will be created and registered.
1206 if os.WIFEXITED(status):
1207 if os.WEXITSTATUS(status) == 100:
1208 logging.info("Host configuration is invalid - exiting.")
1210 elif os.WEXITSTATUS(status) == 101:
1211 logging.info("Host ID has been deleted - exiting.")
1213 host_config.save_and_log_errors()
1215 elif os.WEXITSTATUS(status) == 102:
1216 logging.info("OAuth credentials are invalid - exiting.")
1218 elif os.WEXITSTATUS(status) == 103:
1219 logging.info("Host domain is blocked by policy - exiting.")
1221 # Nothing to do for Mac-only status 104 (login screen unsupported)
1222 elif os.WEXITSTATUS(status) == 105:
1223 logging.info("Username is blocked by policy - exiting.")
1226 logging.info("Host exited with status %s." % os.WEXITSTATUS(status))
1227 elif os.WIFSIGNALED(status):
1228 logging.info("Host terminated by signal %s." % os.WTERMSIG(status))
1231 if __name__ == "__main__":
1232 logging.basicConfig(level=logging.DEBUG,
1233 format="%(asctime)s:%(levelname)s:%(message)s")