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.
32 LOG_FILE_ENV_VAR
= "CHROME_REMOTE_DESKTOP_LOG_FILE"
34 # This script has a sensible default for the initial and maximum desktop size,
35 # which can be overridden either on the command-line, or via a comma-separated
36 # list of sizes in this environment variable.
37 DEFAULT_SIZES_ENV_VAR
= "CHROME_REMOTE_DESKTOP_DEFAULT_DESKTOP_SIZES"
39 # By default, provide a relatively small size to handle the case where resize-
40 # to-client is disabled, and a much larger size to support clients with large
41 # or mulitple monitors. These defaults can be overridden in ~/.profile.
42 DEFAULT_SIZES
= "1600x1200,3840x1600"
44 SCRIPT_PATH
= sys
.path
[0]
46 DEFAULT_INSTALL_PATH
= "/opt/google/chrome-remote-desktop"
47 if SCRIPT_PATH
== DEFAULT_INSTALL_PATH
:
48 HOST_BINARY_NAME
= "chrome-remote-desktop-host"
50 HOST_BINARY_NAME
= "remoting_me2me_host"
52 CHROME_REMOTING_GROUP_NAME
= "chrome-remote-desktop"
54 CONFIG_DIR
= os
.path
.expanduser("~/.config/chrome-remote-desktop")
55 HOME_DIR
= os
.environ
["HOME"]
57 X_LOCK_FILE_TEMPLATE
= "/tmp/.X%d-lock"
58 FIRST_X_DISPLAY_NUMBER
= 20
60 # Amount of time to wait between relaunching processes.
61 SHORT_BACKOFF_TIME
= 5
62 LONG_BACKOFF_TIME
= 60
64 # How long a process must run in order not to be counted against the restart
66 MINIMUM_PROCESS_LIFETIME
= 60
68 # Thresholds for switching from fast- to slow-restart and for giving up
69 # trying to restart entirely.
70 SHORT_BACKOFF_THRESHOLD
= 5
71 MAX_LAUNCH_FAILURES
= SHORT_BACKOFF_THRESHOLD
+ 10
73 # Globals needed by the atexit cleanup() handler.
75 g_host_hash
= hashlib
.md5(socket
.gethostname()).hexdigest()
78 def __init__(self
, path
):
84 """Loads the config from file.
87 IOError: Error reading data
88 ValueError: Error parsing JSON
90 settings_file
= open(self
.path
, 'r')
91 self
.data
= json
.load(settings_file
)
96 """Saves the config to file.
99 IOError: Error writing data
100 TypeError: Error serialising JSON
104 old_umask
= os
.umask(0066)
106 settings_file
= open(self
.path
, 'w')
107 settings_file
.write(json
.dumps(self
.data
, indent
=2))
108 settings_file
.close()
113 def save_and_log_errors(self
):
114 """Calls self.save(), trapping and logging any errors."""
117 except (IOError, TypeError) as e
:
118 logging
.error("Failed to save config: " + str(e
))
121 return self
.data
.get(key
)
123 def __getitem__(self
, key
):
124 return self
.data
[key
]
126 def __setitem__(self
, key
, value
):
127 self
.data
[key
] = value
135 class Authentication
:
136 """Manage authentication tokens for Chromoting/xmpp"""
140 self
.oauth_refresh_token
= None
142 def copy_from(self
, config
):
143 """Loads the config and returns false if the config is invalid."""
145 self
.login
= config
["xmpp_login"]
146 self
.oauth_refresh_token
= config
["oauth_refresh_token"]
151 def copy_to(self
, config
):
152 config
["xmpp_login"] = self
.login
153 config
["oauth_refresh_token"] = self
.oauth_refresh_token
157 """This manages the configuration for a host."""
160 self
.host_id
= str(uuid
.uuid1())
161 self
.host_name
= socket
.gethostname()
162 self
.host_secret_hash
= None
163 self
.private_key
= None
165 def copy_from(self
, config
):
167 self
.host_id
= config
["host_id"]
168 self
.host_name
= config
["host_name"]
169 self
.host_secret_hash
= config
.get("host_secret_hash")
170 self
.private_key
= config
["private_key"]
175 def copy_to(self
, config
):
176 config
["host_id"] = self
.host_id
177 config
["host_name"] = self
.host_name
178 config
["host_secret_hash"] = self
.host_secret_hash
179 config
["private_key"] = self
.private_key
183 """Manage a single virtual desktop"""
185 def __init__(self
, sizes
):
187 self
.session_proc
= None
188 self
.host_proc
= None
189 self
.child_env
= None
191 self
.pulseaudio_pipe
= None
192 self
.server_supports_exact_resize
= False
193 self
.host_ready
= False
194 g_desktops
.append(self
)
197 def get_unused_display_number():
198 """Return a candidate display number for which there is currently no
199 X Server lock file"""
200 display
= FIRST_X_DISPLAY_NUMBER
201 while os
.path
.exists(X_LOCK_FILE_TEMPLATE
% display
):
205 def _init_child_env(self
):
206 # Create clean environment for new session, so it is cleanly separated from
207 # the user's console X session.
219 if os
.environ
.has_key(key
):
220 self
.child_env
[key
] = os
.environ
[key
]
222 # Read from /etc/environment if it exists, as it is a standard place to
223 # store system-wide environment settings. During a normal login, this would
224 # typically be done by the pam_env PAM module, depending on the local PAM
226 env_filename
= "/etc/environment"
228 with
open(env_filename
, "r") as env_file
:
229 for line
in env_file
:
230 line
= line
.rstrip("\n")
231 # Split at the first "=", leaving any further instances in the value.
232 key_value_pair
= line
.split("=", 1)
233 if len(key_value_pair
) == 2:
234 key
, value
= tuple(key_value_pair
)
235 # The file stores key=value assignments, but the value may be
236 # quoted, so strip leading & trailing quotes from it.
237 value
= value
.strip("'\"")
238 self
.child_env
[key
] = value
240 logging
.info("Failed to read %s, skipping." % env_filename
)
242 def _setup_pulseaudio(self
):
243 self
.pulseaudio_pipe
= None
245 # pulseaudio uses UNIX sockets for communication. Length of UNIX socket
246 # name is limited to 108 characters, so audio will not work properly if
247 # the path is too long. To workaround this problem we use only first 10
248 # symbols of the host hash.
249 pulse_path
= os
.path
.join(CONFIG_DIR
,
250 "pulseaudio#%s" % g_host_hash
[0:10])
251 if len(pulse_path
) + len("/native") >= 108:
252 logging
.error("Audio will not be enabled because pulseaudio UNIX " +
253 "socket path is too long.")
256 sink_name
= "chrome_remote_desktop_session"
257 pipe_name
= os
.path
.join(pulse_path
, "fifo_output")
260 if not os
.path
.exists(pulse_path
):
262 if not os
.path
.exists(pipe_name
):
265 logging
.error("Failed to create pulseaudio pipe: " + str(e
))
269 pulse_config
= open(os
.path
.join(pulse_path
, "daemon.conf"), "w")
270 pulse_config
.write("default-sample-format = s16le\n")
271 pulse_config
.write("default-sample-rate = 48000\n")
272 pulse_config
.write("default-sample-channels = 2\n")
275 pulse_script
= open(os
.path
.join(pulse_path
, "default.pa"), "w")
276 pulse_script
.write("load-module module-native-protocol-unix\n")
278 ("load-module module-pipe-sink sink_name=%s file=\"%s\" " +
279 "rate=48000 channels=2 format=s16le\n") %
280 (sink_name
, pipe_name
))
283 logging
.error("Failed to write pulseaudio config: " + str(e
))
286 self
.child_env
["PULSE_CONFIG_PATH"] = pulse_path
287 self
.child_env
["PULSE_RUNTIME_PATH"] = pulse_path
288 self
.child_env
["PULSE_STATE_PATH"] = pulse_path
289 self
.child_env
["PULSE_SINK"] = sink_name
290 self
.pulseaudio_pipe
= pipe_name
294 def _launch_x_server(self
, extra_x_args
):
295 x_auth_file
= os
.path
.expanduser("~/.Xauthority")
296 self
.child_env
["XAUTHORITY"] = x_auth_file
297 devnull
= open(os
.devnull
, "rw")
298 display
= self
.get_unused_display_number()
300 # Run "xauth add" with |child_env| so that it modifies the same XAUTHORITY
301 # file which will be used for the X session.
302 ret_code
= subprocess
.call("xauth add :%d . `mcookie`" % display
,
303 env
=self
.child_env
, shell
=True)
305 raise Exception("xauth failed with code %d" % ret_code
)
307 max_width
= max([width
for width
, height
in self
.sizes
])
308 max_height
= max([height
for width
, height
in self
.sizes
])
311 # TODO(jamiewalch): This script expects to be installed alongside
312 # Xvfb-randr, but that's no longer the case. Fix this once we have
313 # a Xvfb-randr package that installs somewhere sensible.
314 xvfb
= "/usr/bin/Xvfb-randr"
315 if not os
.path
.exists(xvfb
):
316 xvfb
= locate_executable("Xvfb-randr")
317 self
.server_supports_exact_resize
= True
320 self
.server_supports_exact_resize
= False
322 # Disable the Composite extension iff the X session is the default
323 # Unity-2D, since it uses Metacity which fails to generate DAMAGE
324 # notifications correctly. See crbug.com/166468.
325 x_session
= choose_x_session()
326 if (len(x_session
) == 2 and
327 x_session
[1] == "/usr/bin/gnome-session --session=ubuntu-2d"):
328 extra_x_args
.extend(["-extension", "Composite"])
330 logging
.info("Starting %s on display :%d" % (xvfb
, display
))
331 screen_option
= "%dx%dx24" % (max_width
, max_height
)
332 self
.x_proc
= subprocess
.Popen(
333 [xvfb
, ":%d" % display
,
334 "-auth", x_auth_file
,
337 "-screen", "0", screen_option
339 if not self
.x_proc
.pid
:
340 raise Exception("Could not start Xvfb.")
342 self
.child_env
["DISPLAY"] = ":%d" % display
343 self
.child_env
["CHROME_REMOTE_DESKTOP_SESSION"] = "1"
345 # Use a separate profile for any instances of Chrome that are started in
346 # the virtual session. Chrome doesn't support sharing a profile between
347 # multiple DISPLAYs, but Chrome Sync allows for a reasonable compromise.
348 chrome_profile
= os
.path
.join(CONFIG_DIR
, "chrome-profile")
349 self
.child_env
["CHROME_USER_DATA_DIR"] = chrome_profile
351 # Wait for X to be active.
352 for _test
in range(5):
353 proc
= subprocess
.Popen("xdpyinfo", env
=self
.child_env
, stdout
=devnull
)
354 _pid
, retcode
= os
.waitpid(proc
.pid
, 0)
359 raise Exception("Could not connect to Xvfb.")
361 logging
.info("Xvfb is active.")
363 # The remoting host expects the server to use "evdev" keycodes, but Xvfb
364 # starts configured to use the "base" ruleset, resulting in XKB configuring
365 # for "xfree86" keycodes, and screwing up some keys. See crbug.com/119013.
366 # Reconfigure the X server to use "evdev" keymap rules. The X server must
367 # be started with -noreset otherwise it'll reset as soon as the command
368 # completes, since there are no other X clients running yet.
369 proc
= subprocess
.Popen("setxkbmap -rules evdev", env
=self
.child_env
,
371 _pid
, retcode
= os
.waitpid(proc
.pid
, 0)
373 logging
.error("Failed to set XKB to 'evdev'")
375 # Register the screen sizes if the X server's RANDR extension supports it.
376 # Errors here are non-fatal; the X server will continue to run with the
377 # dimensions from the "-screen" option.
378 for width
, height
in self
.sizes
:
379 label
= "%dx%d" % (width
, height
)
380 args
= ["xrandr", "--newmode", label
, "0", str(width
), "0", "0", "0",
381 str(height
), "0", "0", "0"]
382 subprocess
.call(args
, env
=self
.child_env
, stdout
=devnull
, stderr
=devnull
)
383 args
= ["xrandr", "--addmode", "screen", label
]
384 subprocess
.call(args
, env
=self
.child_env
, stdout
=devnull
, stderr
=devnull
)
386 # Set the initial mode to the first size specified, otherwise the X server
387 # would default to (max_width, max_height), which might not even be in the
389 label
= "%dx%d" % self
.sizes
[0]
390 args
= ["xrandr", "-s", label
]
391 subprocess
.call(args
, env
=self
.child_env
, stdout
=devnull
, stderr
=devnull
)
393 # Set the physical size of the display so that the initial mode is running
394 # at approximately 96 DPI, since some desktops require the DPI to be set to
395 # something realistic.
396 args
= ["xrandr", "--dpi", "96"]
397 subprocess
.call(args
, env
=self
.child_env
, stdout
=devnull
, stderr
=devnull
)
401 def _launch_x_session(self
):
402 # Start desktop session.
403 # The /dev/null input redirection is necessary to prevent the X session
404 # reading from stdin. If this code runs as a shell background job in a
405 # terminal, any reading from stdin causes the job to be suspended.
406 # Daemonization would solve this problem by separating the process from the
407 # controlling terminal.
408 xsession_command
= choose_x_session()
409 if xsession_command
is None:
410 raise Exception("Unable to choose suitable X session command.")
412 logging
.info("Launching X session: %s" % xsession_command
)
413 self
.session_proc
= subprocess
.Popen(xsession_command
,
414 stdin
=open(os
.devnull
, "r"),
417 if not self
.session_proc
.pid
:
418 raise Exception("Could not start X session")
420 def launch_session(self
, x_args
):
421 self
._init
_child
_env
()
422 self
._setup
_pulseaudio
()
423 self
._launch
_x
_server
(x_args
)
424 self
._launch
_x
_session
()
426 def launch_host(self
, host_config
):
427 # Start remoting host
428 args
= [locate_executable(HOST_BINARY_NAME
), "--host-config=-"]
429 if self
.pulseaudio_pipe
:
430 args
.append("--audio-pipe-name=%s" % self
.pulseaudio_pipe
)
431 if self
.server_supports_exact_resize
:
432 args
.append("--server-supports-exact-resize")
434 # Have the host process use SIGUSR1 to signal a successful start.
435 def sigusr1_handler(signum
, frame
):
437 logging
.info("Host ready to receive connections.")
438 self
.host_ready
= True
439 if (ParentProcessLogger
.instance() and
440 False not in [desktop
.host_ready
for desktop
in g_desktops
]):
441 ParentProcessLogger
.instance().release_parent()
443 signal
.signal(signal
.SIGUSR1
, sigusr1_handler
)
444 args
.append("--signal-parent")
446 self
.host_proc
= subprocess
.Popen(args
, env
=self
.child_env
,
447 stdin
=subprocess
.PIPE
)
449 if not self
.host_proc
.pid
:
450 raise Exception("Could not start Chrome Remote Desktop host")
451 self
.host_proc
.stdin
.write(json
.dumps(host_config
.data
))
452 self
.host_proc
.stdin
.close()
455 def get_daemon_pid():
456 """Checks if there is already an instance of this script running, and returns
460 The process ID of the existing daemon process, or 0 if the daemon is not
464 this_pid
= os
.getpid()
466 for process
in psutil
.process_iter():
467 # Skip any processes that raise an exception, as processes may terminate
468 # during iteration over the list.
470 # Skip other users' processes.
471 if process
.uids
.real
!= uid
:
474 # Skip the process for this instance.
475 if process
.pid
== this_pid
:
478 # |cmdline| will be [python-interpreter, script-file, other arguments...]
479 cmdline
= process
.cmdline
482 if cmdline
[0] == sys
.executable
and cmdline
[1] == sys
.argv
[0]:
484 except psutil
.error
.Error
:
490 def choose_x_session():
491 """Chooses the most appropriate X session command for this system.
494 A string containing the command to run, or a list of strings containing
495 the executable program and its arguments, which is suitable for passing as
496 the first parameter of subprocess.Popen(). If a suitable session cannot
497 be found, returns None.
499 # If the session wrapper script (see below) is given a specific session as an
500 # argument (such as ubuntu-2d on Ubuntu 12.04), the wrapper will run that
501 # session instead of looking for custom .xsession files in the home directory.
502 # So it's necessary to test for these files here.
504 "~/.chrome-remote-desktop-session",
507 for startup_file
in XSESSION_FILES
:
508 startup_file
= os
.path
.expanduser(startup_file
)
509 if os
.path
.exists(startup_file
):
510 # Use the same logic that a Debian system typically uses with ~/.xsession
511 # (see /etc/X11/Xsession.d/50x11-common_determine-startup), to determine
512 # exactly how to run this file.
513 if os
.access(startup_file
, os
.X_OK
):
514 # "/bin/sh -c" is smart about how to execute the session script and
515 # works in cases where plain exec() fails (for example, if the file is
516 # marked executable, but is a plain script with no shebang line).
517 return ["/bin/sh", "-c", pipes
.quote(startup_file
)]
519 shell
= os
.environ
.get("SHELL", "sh")
520 return [shell
, startup_file
]
522 # Choose a session wrapper script to run the session. On some systems,
523 # /etc/X11/Xsession fails to load the user's .profile, so look for an
524 # alternative wrapper that is more likely to match the script that the
525 # system actually uses for console desktop sessions.
527 "/usr/sbin/lightdm-session",
529 "/etc/X11/Xsession" ]
530 for session_wrapper
in SESSION_WRAPPERS
:
531 if os
.path
.exists(session_wrapper
):
532 if os
.path
.exists("/usr/bin/unity-2d-panel"):
533 # On Ubuntu 12.04, the default session relies on 3D-accelerated
534 # hardware. Trying to run this with a virtual X display produces
535 # weird results on some systems (for example, upside-down and
536 # corrupt displays). So if the ubuntu-2d session is available,
537 # choose it explicitly.
538 return [session_wrapper
, "/usr/bin/gnome-session --session=ubuntu-2d"]
540 # Use the session wrapper by itself, and let the system choose a
542 return [session_wrapper
]
546 def locate_executable(exe_name
):
547 if SCRIPT_PATH
== DEFAULT_INSTALL_PATH
:
548 # If we are installed in the default path, then search the host binary
549 # only in the same directory.
550 paths_to_try
= [ DEFAULT_INSTALL_PATH
]
552 paths_to_try
= map(lambda p
: os
.path
.join(SCRIPT_PATH
, p
),
553 [".", "../../out/Debug", "../../out/Release" ])
554 for path
in paths_to_try
:
555 exe_path
= os
.path
.join(path
, exe_name
)
556 if os
.path
.exists(exe_path
):
559 raise Exception("Could not locate executable '%s'" % exe_name
)
562 class ParentProcessLogger(object):
563 """Redirects logs to the parent process, until the host is ready or quits.
565 This class creates a pipe to allow logging from the daemon process to be
566 copied to the parent process. The daemon process adds a log-handler that
567 directs logging output to the pipe. The parent process reads from this pipe
568 until and writes the content to stderr. When the pipe is no longer needed
569 (for example, the host signals successful launch or permanent failure), the
570 daemon removes the log-handler and closes the pipe, causing the the parent
571 process to reach end-of-file while reading the pipe and exit.
573 The (singleton) logger should be instantiated before forking. The parent
574 process should call wait_for_logs() before exiting. The (grand-)child process
575 should call start_logging() when it starts, and then use logging.* to issue
576 log statements, as usual. When the child has either succesfully started the
577 host or terminated, it must call release_parent() to allow the parent to exit.
583 """Constructor. Must be called before forking."""
584 read_pipe
, write_pipe
= os
.pipe()
585 # Ensure write_pipe is closed on exec, otherwise it will be kept open by
586 # child processes (X, host), preventing the read pipe from EOF'ing.
587 old_flags
= fcntl
.fcntl(write_pipe
, fcntl
.F_GETFD
)
588 fcntl
.fcntl(write_pipe
, fcntl
.F_SETFD
, old_flags | fcntl
.FD_CLOEXEC
)
589 self
._read
_file
= os
.fdopen(read_pipe
, 'r')
590 self
._write
_file
= os
.fdopen(write_pipe
, 'a')
591 self
._logging
_handler
= None
592 ParentProcessLogger
.__instance
= self
594 def start_logging(self
):
595 """Installs a logging handler that sends log entries to a pipe.
597 Must be called by the child process.
599 self
._read
_file
.close()
600 self
._logging
_handler
= logging
.StreamHandler(self
._write
_file
)
601 logging
.getLogger().addHandler(self
._logging
_handler
)
603 def release_parent(self
):
604 """Uninstalls logging handler and closes the pipe, releasing the parent.
606 Must be called by the child process.
608 if self
._logging
_handler
:
609 logging
.getLogger().removeHandler(self
._logging
_handler
)
610 self
._logging
_handler
= None
611 if not self
._write
_file
.closed
:
612 self
._write
_file
.close()
614 def wait_for_logs(self
):
615 """Waits and prints log lines from the daemon until the pipe is closed.
617 Must be called by the parent process.
619 # If Ctrl-C is pressed, inform the user that the daemon is still running.
620 # This signal will cause the read loop below to stop with an EINTR IOError.
621 def sigint_handler(signum
, frame
):
623 print >> sys
.stderr
, ("Interrupted. The daemon is still running in the "
626 signal
.signal(signal
.SIGINT
, sigint_handler
)
628 # Install a fallback timeout to release the parent process, in case the
629 # daemon never responds (e.g. host crash-looping, daemon killed).
630 # This signal will cause the read loop below to stop with an EINTR IOError.
631 def sigalrm_handler(signum
, frame
):
633 print >> sys
.stderr
, ("No response from daemon. It may have crashed, or "
634 "may still be running in the background.")
636 signal
.signal(signal
.SIGALRM
, sigalrm_handler
)
639 self
._write
_file
.close()
641 # Print lines as they're logged to the pipe until EOF is reached or readline
642 # is interrupted by one of the signal handlers above.
644 for line
in iter(self
._read
_file
.readline
, ''):
645 sys
.stderr
.write(line
)
647 if e
.errno
!= errno
.EINTR
:
649 print >> sys
.stderr
, "Log file: %s" % os
.environ
[LOG_FILE_ENV_VAR
]
653 """Returns the singleton instance, if it exists."""
654 return ParentProcessLogger
.__instance
658 """Background this process and detach from controlling terminal, redirecting
659 stdout/stderr to a log file."""
661 # TODO(lambroslambrou): Having stdout/stderr redirected to a log file is not
662 # ideal - it could create a filesystem DoS if the daemon or a child process
663 # were to write excessive amounts to stdout/stderr. Ideally, stdout/stderr
664 # should be redirected to a pipe or socket, and a process at the other end
665 # should consume the data and write it to a logging facility which can do
666 # data-capping or log-rotation. The 'logger' command-line utility could be
667 # used for this, but it might cause too much syslog spam.
669 # Create new (temporary) file-descriptors before forking, so any errors get
670 # reported to the main process and set the correct exit-code.
671 # The mode is provided, since Python otherwise sets a default mode of 0777,
672 # which would result in the new file having permissions of 0777 & ~umask,
673 # possibly leaving the executable bits set.
674 if not os
.environ
.has_key(LOG_FILE_ENV_VAR
):
675 log_file_prefix
= "chrome_remote_desktop_%s_" % time
.strftime(
676 '%Y%m%d_%H%M%S', time
.localtime(time
.time()))
677 log_file
= tempfile
.NamedTemporaryFile(prefix
=log_file_prefix
, delete
=False)
678 os
.environ
[LOG_FILE_ENV_VAR
] = log_file
.name
679 log_fd
= log_file
.file.fileno()
681 log_fd
= os
.open(os
.environ
[LOG_FILE_ENV_VAR
],
682 os
.O_WRONLY | os
.O_CREAT | os
.O_APPEND
, 0600)
684 devnull_fd
= os
.open(os
.devnull
, os
.O_RDONLY
)
686 parent_logger
= ParentProcessLogger()
694 # The second fork ensures that the daemon isn't a session leader, so that
695 # it doesn't acquire a controlling terminal.
703 os
._exit
(0) # pylint: disable=W0212
706 parent_logger
.wait_for_logs()
707 os
._exit
(0) # pylint: disable=W0212
709 logging
.info("Daemon process started in the background, logging to '%s'" %
710 os
.environ
[LOG_FILE_ENV_VAR
])
714 parent_logger
.start_logging()
716 # Copy the file-descriptors to create new stdin, stdout and stderr. Note
717 # that dup2(oldfd, newfd) closes newfd first, so this will close the current
718 # stdin, stdout and stderr, detaching from the terminal.
719 os
.dup2(devnull_fd
, sys
.stdin
.fileno())
720 os
.dup2(log_fd
, sys
.stdout
.fileno())
721 os
.dup2(log_fd
, sys
.stderr
.fileno())
723 # Close the temporary file-descriptors.
729 logging
.info("Cleanup.")
732 for desktop
in g_desktops
:
734 logging
.info("Terminating Xvfb")
735 desktop
.x_proc
.terminate()
737 if ParentProcessLogger
.instance():
738 ParentProcessLogger
.instance().release_parent()
741 """Reload the config file on SIGHUP. Since we pass the configuration to the
742 host processes via stdin, they can't reload it, so terminate them. They will
743 be relaunched automatically with the new config."""
745 def __init__(self
, host_config
):
746 self
.host_config
= host_config
748 def __call__(self
, signum
, _stackframe
):
749 if signum
== signal
.SIGHUP
:
750 logging
.info("SIGHUP caught, restarting host.")
752 self
.host_config
.load()
753 except (IOError, ValueError) as e
:
754 logging
.error("Failed to load config: " + str(e
))
755 for desktop
in g_desktops
:
756 if desktop
.host_proc
:
757 desktop
.host_proc
.send_signal(signal
.SIGTERM
)
759 # Exit cleanly so the atexit handler, cleanup(), gets called.
763 class RelaunchInhibitor
:
764 """Helper class for inhibiting launch of a child process before a timeout has
767 A managed process can be in one of these states:
768 running, not inhibited (running == True)
769 stopped and inhibited (running == False and is_inhibited() == True)
770 stopped but not inhibited (running == False and is_inhibited() == False)
773 label: Name of the tracked process. Only used for logging.
774 running: Whether the process is currently running.
775 earliest_relaunch_time: Time before which the process should not be
776 relaunched, or 0 if there is no limit.
777 failures: The number of times that the process ran for less than a
778 specified timeout, and had to be inhibited. This count is reset to 0
779 whenever the process has run for longer than the timeout.
782 def __init__(self
, label
):
785 self
.earliest_relaunch_time
= 0
786 self
.earliest_successful_termination
= 0
789 def is_inhibited(self
):
790 return (not self
.running
) and (time
.time() < self
.earliest_relaunch_time
)
792 def record_started(self
, minimum_lifetime
, relaunch_delay
):
793 """Record that the process was launched, and set the inhibit time to
794 |timeout| seconds in the future."""
795 self
.earliest_relaunch_time
= time
.time() + relaunch_delay
796 self
.earliest_successful_termination
= time
.time() + minimum_lifetime
799 def record_stopped(self
):
800 """Record that the process was stopped, and adjust the failure count
801 depending on whether the process ran long enough."""
803 if time
.time() < self
.earliest_successful_termination
:
807 logging
.info("Failure count for '%s' is now %d", self
.label
, self
.failures
)
812 os
.execvp(sys
.argv
[0], sys
.argv
)
815 def waitpid_with_timeout(pid
, deadline
):
816 """Wrapper around os.waitpid() which waits until either a child process dies
817 or the deadline elapses.
820 pid: Process ID to wait for, or -1 to wait for any child process.
821 deadline: Waiting stops when time.time() exceeds this value.
824 (pid, status): Same as for os.waitpid(), except that |pid| is 0 if no child
825 changed state within the timeout.
828 Same as for os.waitpid().
830 while time
.time() < deadline
:
831 pid
, status
= os
.waitpid(pid
, os
.WNOHANG
)
838 def waitpid_handle_exceptions(pid
, deadline
):
839 """Wrapper around os.waitpid()/waitpid_with_timeout(), which waits until
840 either a child process exits or the deadline elapses, and retries if certain
844 pid: Process ID to wait for, or -1 to wait for any child process.
845 deadline: If non-zero, waiting stops when time.time() exceeds this value.
846 If zero, waiting stops when a child process exits.
849 (pid, status): Same as for waitpid_with_timeout(). |pid| is non-zero if and
850 only if a child exited during the wait.
853 Same as for os.waitpid(), except:
854 OSError with errno==EINTR causes the wait to be retried (this can happen,
855 for example, if this parent process receives SIGHUP).
856 OSError with errno==ECHILD means there are no child processes, and so
857 this function sleeps until |deadline|. If |deadline| is zero, this is an
858 error and the OSError exception is raised in this case.
863 pid_result
, status
= os
.waitpid(pid
, 0)
865 pid_result
, status
= waitpid_with_timeout(pid
, deadline
)
866 return (pid_result
, status
)
868 if e
.errno
== errno
.EINTR
:
870 elif e
.errno
== errno
.ECHILD
:
873 # No time-limit and no child processes. This is treated as an error
877 time
.sleep(deadline
- now
)
880 # Anything else is an unexpected error.
885 EPILOG
= """This script is not intended for use by end-users. To configure
886 Chrome Remote Desktop, please install the app from the Chrome
887 Web Store: https://chrome.google.com/remotedesktop"""
888 parser
= optparse
.OptionParser(
889 usage
="Usage: %prog [options] [ -- [ X server options ] ]",
891 parser
.add_option("-s", "--size", dest
="size", action
="append",
892 help="Dimensions of virtual desktop. This can be specified "
893 "multiple times to make multiple screen resolutions "
894 "available (if the Xvfb server supports this).")
895 parser
.add_option("-f", "--foreground", dest
="foreground", default
=False,
897 help="Don't run as a background daemon.")
898 parser
.add_option("", "--start", dest
="start", default
=False,
900 help="Start the host.")
901 parser
.add_option("-k", "--stop", dest
="stop", default
=False,
903 help="Stop the daemon currently running.")
904 parser
.add_option("", "--check-running", dest
="check_running", default
=False,
906 help="Return 0 if the daemon is running, or 1 otherwise.")
907 parser
.add_option("", "--config", dest
="config", action
="store",
908 help="Use the specified configuration file.")
909 parser
.add_option("", "--reload", dest
="reload", default
=False,
911 help="Signal currently running host to reload the config.")
912 parser
.add_option("", "--add-user", dest
="add_user", default
=False,
914 help="Add current user to the chrome-remote-desktop group.")
915 parser
.add_option("", "--host-version", dest
="host_version", default
=False,
917 help="Prints version of the host.")
918 (options
, args
) = parser
.parse_args()
920 # Determine the filename of the host configuration and PID files.
921 if not options
.config
:
922 options
.config
= os
.path
.join(CONFIG_DIR
, "host#%s.json" % g_host_hash
)
924 # Check for a modal command-line option (start, stop, etc.)
925 if options
.check_running
:
926 pid
= get_daemon_pid()
927 return 0 if pid
!= 0 else 1
930 pid
= get_daemon_pid()
932 print "The daemon is not currently running"
934 print "Killing process %s" % pid
935 os
.kill(pid
, signal
.SIGTERM
)
939 pid
= get_daemon_pid()
942 os
.kill(pid
, signal
.SIGHUP
)
946 user
= getpass
.getuser()
948 if user
in grp
.getgrnam(CHROME_REMOTING_GROUP_NAME
).gr_mem
:
949 logging
.info("User '%s' is already a member of '%s'." %
950 (user
, CHROME_REMOTING_GROUP_NAME
))
953 logging
.info("Group '%s' not found." % CHROME_REMOTING_GROUP_NAME
)
955 if os
.getenv("DISPLAY"):
956 sudo_command
= "gksudo --description \"Chrome Remote Desktop\""
958 sudo_command
= "sudo"
959 command
= ("sudo -k && exec %(sudo)s -- sh -c "
960 "\"groupadd -f %(group)s && gpasswd --add %(user)s %(group)s\"" %
961 { 'group': CHROME_REMOTING_GROUP_NAME
,
963 'sudo': sudo_command
})
964 os
.execv("/bin/sh", ["/bin/sh", "-c", command
])
967 if options
.host_version
:
968 # TODO(sergeyu): Also check RPM package version once we add RPM package.
969 return os
.system(locate_executable(HOST_BINARY_NAME
) + " --version") >> 8
971 if not options
.start
:
972 # If no modal command-line options specified, print an error and exit.
973 print >> sys
.stderr
, EPILOG
976 # Collate the list of sizes that XRANDR should support.
978 default_sizes
= DEFAULT_SIZES
979 if os
.environ
.has_key(DEFAULT_SIZES_ENV_VAR
):
980 default_sizes
= os
.environ
[DEFAULT_SIZES_ENV_VAR
]
981 options
.size
= default_sizes
.split(",")
984 for size
in options
.size
:
985 size_components
= size
.split("x")
986 if len(size_components
) != 2:
987 parser
.error("Incorrect size format '%s', should be WIDTHxHEIGHT" % size
)
990 width
= int(size_components
[0])
991 height
= int(size_components
[1])
993 # Enforce minimum desktop size, as a sanity-check. The limit of 100 will
994 # detect typos of 2 instead of 3 digits.
995 if width
< 100 or height
< 100:
998 parser
.error("Width and height should be 100 pixels or greater")
1000 sizes
.append((width
, height
))
1002 # Register an exit handler to clean up session process and the PID file.
1003 atexit
.register(cleanup
)
1005 # Load the initial host configuration.
1006 host_config
= Config(options
.config
)
1009 except (IOError, ValueError) as e
:
1010 print >> sys
.stderr
, "Failed to load config: " + str(e
)
1013 # Register handler to re-load the configuration in response to signals.
1014 for s
in [signal
.SIGHUP
, signal
.SIGINT
, signal
.SIGTERM
]:
1015 signal
.signal(s
, SignalHandler(host_config
))
1017 # Verify that the initial host configuration has the necessary fields.
1018 auth
= Authentication()
1019 auth_config_valid
= auth
.copy_from(host_config
)
1021 host_config_valid
= host
.copy_from(host_config
)
1022 if not host_config_valid
or not auth_config_valid
:
1023 logging
.error("Failed to load host configuration.")
1026 # Determine whether a desktop is already active for the specified host
1027 # host configuration.
1028 pid
= get_daemon_pid()
1030 # Debian policy requires that services should "start" cleanly and return 0
1031 # if they are already running.
1032 print "Service already running."
1035 # Detach a separate "daemon" process to run the session, unless specifically
1036 # requested to run in the foreground.
1037 if not options
.foreground
:
1040 logging
.info("Using host_id: " + host
.host_id
)
1042 desktop
= Desktop(sizes
)
1044 # Keep track of the number of consecutive failures of any child process to
1045 # run for longer than a set period of time. The script will exit after a
1046 # threshold is exceeded.
1047 # There is no point in tracking the X session process separately, since it is
1048 # launched at (roughly) the same time as the X server, and the termination of
1049 # one of these triggers the termination of the other.
1050 x_server_inhibitor
= RelaunchInhibitor("X server")
1051 host_inhibitor
= RelaunchInhibitor("host")
1052 all_inhibitors
= [x_server_inhibitor
, host_inhibitor
]
1054 # Don't allow relaunching the script on the first loop iteration.
1055 allow_relaunch_self
= False
1058 # Set the backoff interval and exit if a process failed too many times.
1059 backoff_time
= SHORT_BACKOFF_TIME
1060 for inhibitor
in all_inhibitors
:
1061 if inhibitor
.failures
>= MAX_LAUNCH_FAILURES
:
1062 logging
.error("Too many launch failures of '%s', exiting."
1065 elif inhibitor
.failures
>= SHORT_BACKOFF_THRESHOLD
:
1066 backoff_time
= LONG_BACKOFF_TIME
1070 # If the session process or X server stops running (e.g. because the user
1071 # logged out), kill the other. This will trigger the next conditional block
1072 # as soon as os.waitpid() reaps its exit-code.
1073 if desktop
.session_proc
is None and desktop
.x_proc
is not None:
1074 logging
.info("Terminating X server")
1075 desktop
.x_proc
.terminate()
1076 elif desktop
.x_proc
is None and desktop
.session_proc
is not None:
1077 logging
.info("Terminating X session")
1078 desktop
.session_proc
.terminate()
1079 elif desktop
.x_proc
is None and desktop
.session_proc
is None:
1080 # Both processes have terminated.
1081 if (allow_relaunch_self
and x_server_inhibitor
.failures
== 0 and
1082 host_inhibitor
.failures
== 0):
1083 # Since the user's desktop is already gone at this point, there's no
1084 # state to lose and now is a good time to pick up any updates to this
1085 # script that might have been installed.
1086 logging
.info("Relaunching self")
1089 # If there is a non-zero |failures| count, restarting the whole script
1090 # would lose this information, so just launch the session as normal.
1091 if x_server_inhibitor
.is_inhibited():
1092 logging
.info("Waiting before launching X server")
1093 relaunch_times
.append(x_server_inhibitor
.earliest_relaunch_time
)
1095 logging
.info("Launching X server and X session.")
1096 desktop
.launch_session(args
)
1097 x_server_inhibitor
.record_started(MINIMUM_PROCESS_LIFETIME
,
1099 allow_relaunch_self
= True
1101 if desktop
.host_proc
is None:
1102 if host_inhibitor
.is_inhibited():
1103 logging
.info("Waiting before launching host process")
1104 relaunch_times
.append(host_inhibitor
.earliest_relaunch_time
)
1106 logging
.info("Launching host process")
1107 desktop
.launch_host(host_config
)
1108 host_inhibitor
.record_started(MINIMUM_PROCESS_LIFETIME
,
1111 deadline
= min(relaunch_times
) if relaunch_times
else 0
1112 pid
, status
= waitpid_handle_exceptions(-1, deadline
)
1116 logging
.info("wait() returned (%s,%s)" % (pid
, status
))
1118 # When a process has terminated, and we've reaped its exit-code, any Popen
1119 # instance for that process is no longer valid. Reset any affected instance
1121 if desktop
.x_proc
is not None and pid
== desktop
.x_proc
.pid
:
1122 logging
.info("X server process terminated")
1123 desktop
.x_proc
= None
1124 x_server_inhibitor
.record_stopped()
1126 if desktop
.session_proc
is not None and pid
== desktop
.session_proc
.pid
:
1127 logging
.info("Session process terminated")
1128 desktop
.session_proc
= None
1130 if desktop
.host_proc
is not None and pid
== desktop
.host_proc
.pid
:
1131 logging
.info("Host process terminated")
1132 desktop
.host_proc
= None
1133 desktop
.host_ready
= False
1134 host_inhibitor
.record_stopped()
1136 # These exit-codes must match the ones used by the host.
1137 # See remoting/host/host_error_codes.h.
1138 # Delete the host or auth configuration depending on the returned error
1139 # code, so the next time this script is run, a new configuration
1140 # will be created and registered.
1141 if os
.WIFEXITED(status
):
1142 if os
.WEXITSTATUS(status
) == 100:
1143 logging
.info("Host configuration is invalid - exiting.")
1145 elif os
.WEXITSTATUS(status
) == 101:
1146 logging
.info("Host ID has been deleted - exiting.")
1148 host_config
.save_and_log_errors()
1150 elif os
.WEXITSTATUS(status
) == 102:
1151 logging
.info("OAuth credentials are invalid - exiting.")
1153 elif os
.WEXITSTATUS(status
) == 103:
1154 logging
.info("Host domain is blocked by policy - exiting.")
1156 # Nothing to do for Mac-only status 104 (login screen unsupported)
1157 elif os
.WEXITSTATUS(status
) == 105:
1158 logging
.info("Username is blocked by policy - exiting.")
1161 logging
.info("Host exited with status %s." % os
.WEXITSTATUS(status
))
1162 elif os
.WIFSIGNALED(status
):
1163 logging
.info("Host terminated by signal %s." % os
.WTERMSIG(status
))
1166 if __name__
== "__main__":
1167 logging
.basicConfig(level
=logging
.DEBUG
,
1168 format
="%(asctime)s:%(levelname)s:%(message)s")