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.
31 LOG_FILE_ENV_VAR
= "CHROME_REMOTE_DESKTOP_LOG_FILE"
33 # This script has a sensible default for the initial and maximum desktop size,
34 # which can be overridden either on the command-line, or via a comma-separated
35 # list of sizes in this environment variable.
36 DEFAULT_SIZES_ENV_VAR
= "CHROME_REMOTE_DESKTOP_DEFAULT_DESKTOP_SIZES"
38 # By default, provide a relatively small size to handle the case where resize-
39 # to-client is disabled, and a much larger size to support clients with large
40 # or mulitple monitors. These defaults can be overridden in ~/.profile.
41 DEFAULT_SIZES
= "1600x1200,3840x1600"
43 SCRIPT_PATH
= sys
.path
[0]
45 DEFAULT_INSTALL_PATH
= "/opt/google/chrome-remote-desktop"
46 if SCRIPT_PATH
== DEFAULT_INSTALL_PATH
:
47 HOST_BINARY_NAME
= "chrome-remote-desktop-host"
49 HOST_BINARY_NAME
= "remoting_me2me_host"
51 CHROME_REMOTING_GROUP_NAME
= "chrome-remote-desktop"
53 CONFIG_DIR
= os
.path
.expanduser("~/.config/chrome-remote-desktop")
54 HOME_DIR
= os
.environ
["HOME"]
56 X_LOCK_FILE_TEMPLATE
= "/tmp/.X%d-lock"
57 FIRST_X_DISPLAY_NUMBER
= 20
59 # Amount of time to wait between relaunching processes.
60 SHORT_BACKOFF_TIME
= 5
61 LONG_BACKOFF_TIME
= 60
63 # How long a process must run in order not to be counted against the restart
65 MINIMUM_PROCESS_LIFETIME
= 60
67 # Thresholds for switching from fast- to slow-restart and for giving up
68 # trying to restart entirely.
69 SHORT_BACKOFF_THRESHOLD
= 5
70 MAX_LAUNCH_FAILURES
= SHORT_BACKOFF_THRESHOLD
+ 10
72 # Globals needed by the atexit cleanup() handler.
74 g_host_hash
= hashlib
.md5(socket
.gethostname()).hexdigest()
77 def __init__(self
, path
):
83 """Loads the config from file.
86 IOError: Error reading data
87 ValueError: Error parsing JSON
89 settings_file
= open(self
.path
, 'r')
90 self
.data
= json
.load(settings_file
)
95 """Saves the config to file.
98 IOError: Error writing data
99 TypeError: Error serialising JSON
103 old_umask
= os
.umask(0066)
105 settings_file
= open(self
.path
, 'w')
106 settings_file
.write(json
.dumps(self
.data
, indent
=2))
107 settings_file
.close()
112 def save_and_log_errors(self
):
113 """Calls self.save(), trapping and logging any errors."""
116 except (IOError, TypeError) as e
:
117 logging
.error("Failed to save config: " + str(e
))
120 return self
.data
.get(key
)
122 def __getitem__(self
, key
):
123 return self
.data
[key
]
125 def __setitem__(self
, key
, value
):
126 self
.data
[key
] = value
129 def clear_auth(self
):
130 del self
.data
["xmpp_login"]
131 del self
.data
["oauth_refresh_token"]
134 def clear_host_info(self
):
135 del self
.data
["host_id"]
136 del self
.data
["host_name"]
137 del self
.data
["host_secret_hash"]
138 del self
.data
["private_key"]
142 class Authentication
:
143 """Manage authentication tokens for Chromoting/xmpp"""
147 self
.oauth_refresh_token
= None
149 def copy_from(self
, config
):
150 """Loads the config and returns false if the config is invalid."""
152 self
.login
= config
["xmpp_login"]
153 self
.oauth_refresh_token
= config
["oauth_refresh_token"]
158 def copy_to(self
, config
):
159 config
["xmpp_login"] = self
.login
160 config
["oauth_refresh_token"] = self
.oauth_refresh_token
164 """This manages the configuration for a host."""
167 self
.host_id
= str(uuid
.uuid1())
168 self
.host_name
= socket
.gethostname()
169 self
.host_secret_hash
= None
170 self
.private_key
= None
172 def copy_from(self
, config
):
174 self
.host_id
= config
["host_id"]
175 self
.host_name
= config
["host_name"]
176 self
.host_secret_hash
= config
.get("host_secret_hash")
177 self
.private_key
= config
["private_key"]
182 def copy_to(self
, config
):
183 config
["host_id"] = self
.host_id
184 config
["host_name"] = self
.host_name
185 config
["host_secret_hash"] = self
.host_secret_hash
186 config
["private_key"] = self
.private_key
190 """Manage a single virtual desktop"""
192 def __init__(self
, sizes
):
194 self
.session_proc
= None
195 self
.host_proc
= None
196 self
.child_env
= None
198 self
.pulseaudio_pipe
= None
199 self
.server_supports_exact_resize
= False
200 self
.host_ready
= False
201 g_desktops
.append(self
)
204 def get_unused_display_number():
205 """Return a candidate display number for which there is currently no
206 X Server lock file"""
207 display
= FIRST_X_DISPLAY_NUMBER
208 while os
.path
.exists(X_LOCK_FILE_TEMPLATE
% display
):
212 def _init_child_env(self
):
213 # Create clean environment for new session, so it is cleanly separated from
214 # the user's console X session.
226 if os
.environ
.has_key(key
):
227 self
.child_env
[key
] = os
.environ
[key
]
229 # Read from /etc/environment if it exists, as it is a standard place to
230 # store system-wide environment settings. During a normal login, this would
231 # typically be done by the pam_env PAM module, depending on the local PAM
233 env_filename
= "/etc/environment"
235 with
open(env_filename
, "r") as env_file
:
236 for line
in env_file
:
237 line
= line
.rstrip("\n")
238 # Split at the first "=", leaving any further instances in the value.
239 key_value_pair
= line
.split("=", 1)
240 if len(key_value_pair
) == 2:
241 key
, value
= tuple(key_value_pair
)
242 # The file stores key=value assignments, but the value may be
243 # quoted, so strip leading & trailing quotes from it.
244 value
= value
.strip("'\"")
245 self
.child_env
[key
] = value
247 logging
.info("Failed to read %s, skipping." % env_filename
)
249 def _setup_pulseaudio(self
):
250 self
.pulseaudio_pipe
= None
252 # pulseaudio uses UNIX sockets for communication. Length of UNIX socket
253 # name is limited to 108 characters, so audio will not work properly if
254 # the path is too long. To workaround this problem we use only first 10
255 # symbols of the host hash.
256 pulse_path
= os
.path
.join(CONFIG_DIR
,
257 "pulseaudio#%s" % g_host_hash
[0:10])
258 if len(pulse_path
) + len("/native") >= 108:
259 logging
.error("Audio will not be enabled because pulseaudio UNIX " +
260 "socket path is too long.")
263 sink_name
= "chrome_remote_desktop_session"
264 pipe_name
= os
.path
.join(pulse_path
, "fifo_output")
267 if not os
.path
.exists(pulse_path
):
269 if not os
.path
.exists(pipe_name
):
272 logging
.error("Failed to create pulseaudio pipe: " + str(e
))
276 pulse_config
= open(os
.path
.join(pulse_path
, "daemon.conf"), "w")
277 pulse_config
.write("default-sample-format = s16le\n")
278 pulse_config
.write("default-sample-rate = 48000\n")
279 pulse_config
.write("default-sample-channels = 2\n")
282 pulse_script
= open(os
.path
.join(pulse_path
, "default.pa"), "w")
283 pulse_script
.write("load-module module-native-protocol-unix\n")
285 ("load-module module-pipe-sink sink_name=%s file=\"%s\" " +
286 "rate=48000 channels=2 format=s16le\n") %
287 (sink_name
, pipe_name
))
290 logging
.error("Failed to write pulseaudio config: " + str(e
))
293 self
.child_env
["PULSE_CONFIG_PATH"] = pulse_path
294 self
.child_env
["PULSE_RUNTIME_PATH"] = pulse_path
295 self
.child_env
["PULSE_STATE_PATH"] = pulse_path
296 self
.child_env
["PULSE_SINK"] = sink_name
297 self
.pulseaudio_pipe
= pipe_name
301 def _launch_x_server(self
, extra_x_args
):
302 x_auth_file
= os
.path
.expanduser("~/.Xauthority")
303 self
.child_env
["XAUTHORITY"] = x_auth_file
304 devnull
= open(os
.devnull
, "rw")
305 display
= self
.get_unused_display_number()
307 # Run "xauth add" with |child_env| so that it modifies the same XAUTHORITY
308 # file which will be used for the X session.
309 ret_code
= subprocess
.call("xauth add :%d . `mcookie`" % display
,
310 env
=self
.child_env
, shell
=True)
312 raise Exception("xauth failed with code %d" % ret_code
)
314 max_width
= max([width
for width
, height
in self
.sizes
])
315 max_height
= max([height
for width
, height
in self
.sizes
])
318 # TODO(jamiewalch): This script expects to be installed alongside
319 # Xvfb-randr, but that's no longer the case. Fix this once we have
320 # a Xvfb-randr package that installs somewhere sensible.
321 xvfb
= "/usr/bin/Xvfb-randr"
322 if not os
.path
.exists(xvfb
):
323 xvfb
= locate_executable("Xvfb-randr")
324 self
.server_supports_exact_resize
= True
327 self
.server_supports_exact_resize
= False
329 # Disable the Composite extension iff the X session is the default
330 # Unity-2D, since it uses Metacity which fails to generate DAMAGE
331 # notifications correctly. See crbug.com/166468.
332 x_session
= choose_x_session()
333 if (len(x_session
) == 2 and
334 x_session
[1] == "/usr/bin/gnome-session --session=ubuntu-2d"):
335 extra_x_args
.extend(["-extension", "Composite"])
337 logging
.info("Starting %s on display :%d" % (xvfb
, display
))
338 screen_option
= "%dx%dx24" % (max_width
, max_height
)
339 self
.x_proc
= subprocess
.Popen(
340 [xvfb
, ":%d" % display
,
341 "-auth", x_auth_file
,
344 "-screen", "0", screen_option
346 if not self
.x_proc
.pid
:
347 raise Exception("Could not start Xvfb.")
349 self
.child_env
["DISPLAY"] = ":%d" % display
350 self
.child_env
["CHROME_REMOTE_DESKTOP_SESSION"] = "1"
352 # Wait for X to be active.
353 for _test
in range(5):
354 proc
= subprocess
.Popen("xdpyinfo", env
=self
.child_env
, stdout
=devnull
)
355 _pid
, retcode
= os
.waitpid(proc
.pid
, 0)
360 raise Exception("Could not connect to Xvfb.")
362 logging
.info("Xvfb is active.")
364 # The remoting host expects the server to use "evdev" keycodes, but Xvfb
365 # starts configured to use the "base" ruleset, resulting in XKB configuring
366 # for "xfree86" keycodes, and screwing up some keys. See crbug.com/119013.
367 # Reconfigure the X server to use "evdev" keymap rules. The X server must
368 # be started with -noreset otherwise it'll reset as soon as the command
369 # completes, since there are no other X clients running yet.
370 proc
= subprocess
.Popen("setxkbmap -rules evdev", env
=self
.child_env
,
372 _pid
, retcode
= os
.waitpid(proc
.pid
, 0)
374 logging
.error("Failed to set XKB to 'evdev'")
376 # Register the screen sizes if the X server's RANDR extension supports it.
377 # Errors here are non-fatal; the X server will continue to run with the
378 # dimensions from the "-screen" option.
379 for width
, height
in self
.sizes
:
380 label
= "%dx%d" % (width
, height
)
381 args
= ["xrandr", "--newmode", label
, "0", str(width
), "0", "0", "0",
382 str(height
), "0", "0", "0"]
383 subprocess
.call(args
, env
=self
.child_env
, stdout
=devnull
, stderr
=devnull
)
384 args
= ["xrandr", "--addmode", "screen", label
]
385 subprocess
.call(args
, env
=self
.child_env
, stdout
=devnull
, stderr
=devnull
)
387 # Set the initial mode to the first size specified, otherwise the X server
388 # would default to (max_width, max_height), which might not even be in the
390 label
= "%dx%d" % self
.sizes
[0]
391 args
= ["xrandr", "-s", label
]
392 subprocess
.call(args
, env
=self
.child_env
, stdout
=devnull
, stderr
=devnull
)
394 # Set the physical size of the display so that the initial mode is running
395 # at approximately 96 DPI, since some desktops require the DPI to be set to
396 # something realistic.
397 args
= ["xrandr", "--dpi", "96"]
398 subprocess
.call(args
, env
=self
.child_env
, stdout
=devnull
, stderr
=devnull
)
402 def _launch_x_session(self
):
403 # Start desktop session.
404 # The /dev/null input redirection is necessary to prevent the X session
405 # reading from stdin. If this code runs as a shell background job in a
406 # terminal, any reading from stdin causes the job to be suspended.
407 # Daemonization would solve this problem by separating the process from the
408 # controlling terminal.
409 xsession_command
= choose_x_session()
410 if xsession_command
is None:
411 raise Exception("Unable to choose suitable X session command.")
413 logging
.info("Launching X session: %s" % xsession_command
)
414 self
.session_proc
= subprocess
.Popen(xsession_command
,
415 stdin
=open(os
.devnull
, "r"),
418 if not self
.session_proc
.pid
:
419 raise Exception("Could not start X session")
421 def launch_session(self
, x_args
):
422 self
._init
_child
_env
()
423 self
._setup
_pulseaudio
()
424 self
._launch
_x
_server
(x_args
)
425 self
._launch
_x
_session
()
427 def launch_host(self
, host_config
):
428 # Start remoting host
429 args
= [locate_executable(HOST_BINARY_NAME
), "--host-config=/dev/stdin"]
430 if self
.pulseaudio_pipe
:
431 args
.append("--audio-pipe-name=%s" % self
.pulseaudio_pipe
)
432 if self
.server_supports_exact_resize
:
433 args
.append("--server-supports-exact-resize")
435 # Have the host process use SIGUSR1 to signal a successful start.
436 def sigusr1_handler(signum
, frame
):
438 logging
.info("Host ready to receive connections.")
439 self
.host_ready
= True
440 if (ParentProcessLogger
.instance() and
441 False not in [desktop
.host_ready
for desktop
in g_desktops
]):
442 ParentProcessLogger
.instance().release_parent()
444 signal
.signal(signal
.SIGUSR1
, sigusr1_handler
)
445 args
.append("--signal-parent")
447 self
.host_proc
= subprocess
.Popen(args
, env
=self
.child_env
,
448 stdin
=subprocess
.PIPE
)
450 if not self
.host_proc
.pid
:
451 raise Exception("Could not start Chrome Remote Desktop host")
452 self
.host_proc
.stdin
.write(json
.dumps(host_config
.data
))
453 self
.host_proc
.stdin
.close()
456 def get_daemon_pid():
457 """Checks if there is already an instance of this script running, and returns
461 The process ID of the existing daemon process, or 0 if the daemon is not
465 this_pid
= os
.getpid()
467 for process
in psutil
.process_iter():
468 # Skip any processes that raise an exception, as processes may terminate
469 # during iteration over the list.
471 # Skip other users' processes.
472 if process
.uids
.real
!= uid
:
475 # Skip the process for this instance.
476 if process
.pid
== this_pid
:
479 # |cmdline| will be [python-interpreter, script-file, other arguments...]
480 cmdline
= process
.cmdline
483 if cmdline
[0] == sys
.executable
and cmdline
[1] == sys
.argv
[0]:
485 except psutil
.error
.Error
:
491 def choose_x_session():
492 """Chooses the most appropriate X session command for this system.
495 A string containing the command to run, or a list of strings containing
496 the executable program and its arguments, which is suitable for passing as
497 the first parameter of subprocess.Popen(). If a suitable session cannot
498 be found, returns None.
500 # If the session wrapper script (see below) is given a specific session as an
501 # argument (such as ubuntu-2d on Ubuntu 12.04), the wrapper will run that
502 # session instead of looking for custom .xsession files in the home directory.
503 # So it's necessary to test for these files here.
505 "~/.chrome-remote-desktop-session",
508 for startup_file
in XSESSION_FILES
:
509 startup_file
= os
.path
.expanduser(startup_file
)
510 if os
.path
.exists(startup_file
):
511 # Use the same logic that a Debian system typically uses with ~/.xsession
512 # (see /etc/X11/Xsession.d/50x11-common_determine-startup), to determine
513 # exactly how to run this file.
514 if os
.access(startup_file
, os
.X_OK
):
515 # "/bin/sh -c" is smart about how to execute the session script and
516 # works in cases where plain exec() fails (for example, if the file is
517 # marked executable, but is a plain script with no shebang line).
518 return ["/bin/sh", "-c", pipes
.quote(startup_file
)]
520 shell
= os
.environ
.get("SHELL", "sh")
521 return [shell
, startup_file
]
523 # Choose a session wrapper script to run the session. On some systems,
524 # /etc/X11/Xsession fails to load the user's .profile, so look for an
525 # alternative wrapper that is more likely to match the script that the
526 # system actually uses for console desktop sessions.
528 "/usr/sbin/lightdm-session",
530 "/etc/X11/Xsession" ]
531 for session_wrapper
in SESSION_WRAPPERS
:
532 if os
.path
.exists(session_wrapper
):
533 if os
.path
.exists("/usr/bin/unity-2d-panel"):
534 # On Ubuntu 12.04, the default session relies on 3D-accelerated
535 # hardware. Trying to run this with a virtual X display produces
536 # weird results on some systems (for example, upside-down and
537 # corrupt displays). So if the ubuntu-2d session is available,
538 # choose it explicitly.
539 return [session_wrapper
, "/usr/bin/gnome-session --session=ubuntu-2d"]
541 # Use the session wrapper by itself, and let the system choose a
543 return [session_wrapper
]
547 def locate_executable(exe_name
):
548 if SCRIPT_PATH
== DEFAULT_INSTALL_PATH
:
549 # If we are installed in the default path, then search the host binary
550 # only in the same directory.
551 paths_to_try
= [ DEFAULT_INSTALL_PATH
]
553 paths_to_try
= map(lambda p
: os
.path
.join(SCRIPT_PATH
, p
),
554 [".", "../../out/Debug", "../../out/Release" ])
555 for path
in paths_to_try
:
556 exe_path
= os
.path
.join(path
, exe_name
)
557 if os
.path
.exists(exe_path
):
560 raise Exception("Could not locate executable '%s'" % exe_name
)
563 class ParentProcessLogger(object):
564 """Redirects logs to the parent process, until the host is ready or quits.
566 This class creates a pipe to allow logging from the daemon process to be
567 copied to the parent process. The daemon process adds a log-handler that
568 directs logging output to the pipe. The parent process reads from this pipe
569 until and writes the content to stderr. When the pipe is no longer needed
570 (for example, the host signals successful launch or permanent failure), the
571 daemon removes the log-handler and closes the pipe, causing the the parent
572 process to reach end-of-file while reading the pipe and exit.
574 The (singleton) logger should be instantiated before forking. The parent
575 process should call wait_for_logs() before exiting. The (grand-)child process
576 should call start_logging() when it starts, and then use logging.* to issue
577 log statements, as usual. When the child has either succesfully started the
578 host or terminated, it must call release_parent() to allow the parent to exit.
584 """Constructor. Must be called before forking."""
585 read_pipe
, write_pipe
= os
.pipe()
586 # Ensure write_pipe is closed on exec, otherwise it will be kept open by
587 # child processes (X, host), preventing the read pipe from EOF'ing.
588 old_flags
= fcntl
.fcntl(write_pipe
, fcntl
.F_GETFD
)
589 fcntl
.fcntl(write_pipe
, fcntl
.F_SETFD
, old_flags | fcntl
.FD_CLOEXEC
)
590 self
._read
_file
= os
.fdopen(read_pipe
, 'r')
591 self
._write
_file
= os
.fdopen(write_pipe
, 'a')
592 self
._logging
_handler
= None
593 ParentProcessLogger
.__instance
= self
595 def start_logging(self
):
596 """Installs a logging handler that sends log entries to a pipe.
598 Must be called by the child process.
600 self
._read
_file
.close()
601 self
._logging
_handler
= logging
.StreamHandler(self
._write
_file
)
602 logging
.getLogger().addHandler(self
._logging
_handler
)
604 def release_parent(self
):
605 """Uninstalls logging handler and closes the pipe, releasing the parent.
607 Must be called by the child process.
609 if self
._logging
_handler
:
610 logging
.getLogger().removeHandler(self
._logging
_handler
)
611 self
._logging
_handler
= None
612 if not self
._write
_file
.closed
:
613 self
._write
_file
.close()
615 def wait_for_logs(self
):
616 """Waits and prints log lines from the daemon until the pipe is closed.
618 Must be called by the parent process.
620 # If Ctrl-C is pressed, inform the user that the daemon is still running.
621 # This signal will cause the read loop below to stop with an EINTR IOError.
622 def sigint_handler(signum
, frame
):
624 print >> sys
.stderr
, ("Interrupted. The daemon is still running in the "
627 signal
.signal(signal
.SIGINT
, sigint_handler
)
629 # Install a fallback timeout to release the parent process, in case the
630 # daemon never responds (e.g. host crash-looping, daemon killed).
631 # This signal will cause the read loop below to stop with an EINTR IOError.
632 def sigalrm_handler(signum
, frame
):
634 print >> sys
.stderr
, ("No response from daemon. It may have crashed, or "
635 "may still be running in the background.")
637 signal
.signal(signal
.SIGALRM
, sigalrm_handler
)
640 self
._write
_file
.close()
642 # Print lines as they're logged to the pipe until EOF is reached or readline
643 # is interrupted by one of the signal handlers above.
645 for line
in iter(self
._read
_file
.readline
, ''):
646 sys
.stderr
.write(line
)
648 if e
.errno
!= errno
.EINTR
:
650 print >> sys
.stderr
, "Log file: %s" % os
.environ
[LOG_FILE_ENV_VAR
]
654 """Returns the singleton instance, if it exists."""
655 return ParentProcessLogger
.__instance
659 """Background this process and detach from controlling terminal, redirecting
660 stdout/stderr to a log file."""
662 # TODO(lambroslambrou): Having stdout/stderr redirected to a log file is not
663 # ideal - it could create a filesystem DoS if the daemon or a child process
664 # were to write excessive amounts to stdout/stderr. Ideally, stdout/stderr
665 # should be redirected to a pipe or socket, and a process at the other end
666 # should consume the data and write it to a logging facility which can do
667 # data-capping or log-rotation. The 'logger' command-line utility could be
668 # used for this, but it might cause too much syslog spam.
670 # Create new (temporary) file-descriptors before forking, so any errors get
671 # reported to the main process and set the correct exit-code.
672 # The mode is provided, since Python otherwise sets a default mode of 0777,
673 # which would result in the new file having permissions of 0777 & ~umask,
674 # possibly leaving the executable bits set.
675 if not os
.environ
.has_key(LOG_FILE_ENV_VAR
):
676 log_file_prefix
= "chrome_remote_desktop_%s_" % time
.strftime(
677 '%Y%m%d_%H%M%S', time
.localtime(time
.time()))
678 log_file
= tempfile
.NamedTemporaryFile(prefix
=log_file_prefix
, delete
=False)
679 os
.environ
[LOG_FILE_ENV_VAR
] = log_file
.name
680 log_fd
= log_file
.file.fileno()
682 log_fd
= os
.open(os
.environ
[LOG_FILE_ENV_VAR
],
683 os
.O_WRONLY | os
.O_CREAT | os
.O_APPEND
, 0600)
685 devnull_fd
= os
.open(os
.devnull
, os
.O_RDONLY
)
687 parent_logger
= ParentProcessLogger()
695 # The second fork ensures that the daemon isn't a session leader, so that
696 # it doesn't acquire a controlling terminal.
704 os
._exit
(0) # pylint: disable=W0212
707 parent_logger
.wait_for_logs()
708 os
._exit
(0) # pylint: disable=W0212
710 logging
.info("Daemon process started in the background, logging to '%s'" %
711 os
.environ
[LOG_FILE_ENV_VAR
])
715 parent_logger
.start_logging()
717 # Copy the file-descriptors to create new stdin, stdout and stderr. Note
718 # that dup2(oldfd, newfd) closes newfd first, so this will close the current
719 # stdin, stdout and stderr, detaching from the terminal.
720 os
.dup2(devnull_fd
, sys
.stdin
.fileno())
721 os
.dup2(log_fd
, sys
.stdout
.fileno())
722 os
.dup2(log_fd
, sys
.stderr
.fileno())
724 # Close the temporary file-descriptors.
730 logging
.info("Cleanup.")
733 for desktop
in g_desktops
:
735 logging
.info("Terminating Xvfb")
736 desktop
.x_proc
.terminate()
738 if ParentProcessLogger
.instance():
739 ParentProcessLogger
.instance().release_parent()
742 """Reload the config file on SIGHUP. Since we pass the configuration to the
743 host processes via stdin, they can't reload it, so terminate them. They will
744 be relaunched automatically with the new config."""
746 def __init__(self
, host_config
):
747 self
.host_config
= host_config
749 def __call__(self
, signum
, _stackframe
):
750 if signum
== signal
.SIGHUP
:
751 logging
.info("SIGHUP caught, restarting host.")
753 self
.host_config
.load()
754 except (IOError, ValueError) as e
:
755 logging
.error("Failed to load config: " + str(e
))
756 for desktop
in g_desktops
:
757 if desktop
.host_proc
:
758 desktop
.host_proc
.send_signal(signal
.SIGTERM
)
760 # Exit cleanly so the atexit handler, cleanup(), gets called.
764 class RelaunchInhibitor
:
765 """Helper class for inhibiting launch of a child process before a timeout has
768 A managed process can be in one of these states:
769 running, not inhibited (running == True)
770 stopped and inhibited (running == False and is_inhibited() == True)
771 stopped but not inhibited (running == False and is_inhibited() == False)
774 label: Name of the tracked process. Only used for logging.
775 running: Whether the process is currently running.
776 earliest_relaunch_time: Time before which the process should not be
777 relaunched, or 0 if there is no limit.
778 failures: The number of times that the process ran for less than a
779 specified timeout, and had to be inhibited. This count is reset to 0
780 whenever the process has run for longer than the timeout.
783 def __init__(self
, label
):
786 self
.earliest_relaunch_time
= 0
787 self
.earliest_successful_termination
= 0
790 def is_inhibited(self
):
791 return (not self
.running
) and (time
.time() < self
.earliest_relaunch_time
)
793 def record_started(self
, minimum_lifetime
, relaunch_delay
):
794 """Record that the process was launched, and set the inhibit time to
795 |timeout| seconds in the future."""
796 self
.earliest_relaunch_time
= time
.time() + relaunch_delay
797 self
.earliest_successful_termination
= time
.time() + minimum_lifetime
800 def record_stopped(self
):
801 """Record that the process was stopped, and adjust the failure count
802 depending on whether the process ran long enough."""
804 if time
.time() < self
.earliest_successful_termination
:
808 logging
.info("Failure count for '%s' is now %d", self
.label
, self
.failures
)
813 os
.execvp(sys
.argv
[0], sys
.argv
)
816 def waitpid_with_timeout(pid
, deadline
):
817 """Wrapper around os.waitpid() which waits until either a child process dies
818 or the deadline elapses.
821 pid: Process ID to wait for, or -1 to wait for any child process.
822 deadline: Waiting stops when time.time() exceeds this value.
825 (pid, status): Same as for os.waitpid(), except that |pid| is 0 if no child
826 changed state within the timeout.
829 Same as for os.waitpid().
831 while time
.time() < deadline
:
832 pid
, status
= os
.waitpid(pid
, os
.WNOHANG
)
839 def waitpid_handle_exceptions(pid
, deadline
):
840 """Wrapper around os.waitpid()/waitpid_with_timeout(), which waits until
841 either a child process exits or the deadline elapses, and retries if certain
845 pid: Process ID to wait for, or -1 to wait for any child process.
846 deadline: If non-zero, waiting stops when time.time() exceeds this value.
847 If zero, waiting stops when a child process exits.
850 (pid, status): Same as for waitpid_with_timeout(). |pid| is non-zero if and
851 only if a child exited during the wait.
854 Same as for os.waitpid(), except:
855 OSError with errno==EINTR causes the wait to be retried (this can happen,
856 for example, if this parent process receives SIGHUP).
857 OSError with errno==ECHILD means there are no child processes, and so
858 this function sleeps until |deadline|. If |deadline| is zero, this is an
859 error and the OSError exception is raised in this case.
864 pid_result
, status
= os
.waitpid(pid
, 0)
866 pid_result
, status
= waitpid_with_timeout(pid
, deadline
)
867 return (pid_result
, status
)
869 if e
.errno
== errno
.EINTR
:
871 elif e
.errno
== errno
.ECHILD
:
874 # No time-limit and no child processes. This is treated as an error
878 time
.sleep(deadline
- now
)
881 # Anything else is an unexpected error.
886 EPILOG
= """This script is not intended for use by end-users. To configure
887 Chrome Remote Desktop, please install the app from the Chrome
888 Web Store: https://chrome.google.com/remotedesktop"""
889 parser
= optparse
.OptionParser(
890 usage
="Usage: %prog [options] [ -- [ X server options ] ]",
892 parser
.add_option("-s", "--size", dest
="size", action
="append",
893 help="Dimensions of virtual desktop. This can be specified "
894 "multiple times to make multiple screen resolutions "
895 "available (if the Xvfb server supports this).")
896 parser
.add_option("-f", "--foreground", dest
="foreground", default
=False,
898 help="Don't run as a background daemon.")
899 parser
.add_option("", "--start", dest
="start", default
=False,
901 help="Start the host.")
902 parser
.add_option("-k", "--stop", dest
="stop", default
=False,
904 help="Stop the daemon currently running.")
905 parser
.add_option("", "--check-running", dest
="check_running", default
=False,
907 help="Return 0 if the daemon is running, or 1 otherwise.")
908 parser
.add_option("", "--config", dest
="config", action
="store",
909 help="Use the specified configuration file.")
910 parser
.add_option("", "--reload", dest
="reload", default
=False,
912 help="Signal currently running host to reload the config.")
913 parser
.add_option("", "--add-user", dest
="add_user", default
=False,
915 help="Add current user to the chrome-remote-desktop group.")
916 parser
.add_option("", "--host-version", dest
="host_version", default
=False,
918 help="Prints version of the host.")
919 (options
, args
) = parser
.parse_args()
921 # Determine the filename of the host configuration and PID files.
922 if not options
.config
:
923 options
.config
= os
.path
.join(CONFIG_DIR
, "host#%s.json" % g_host_hash
)
925 # Check for a modal command-line option (start, stop, etc.)
926 if options
.check_running
:
927 pid
= get_daemon_pid()
928 return 0 if pid
!= 0 else 1
931 pid
= get_daemon_pid()
933 print "The daemon is not currently running"
935 print "Killing process %s" % pid
936 os
.kill(pid
, signal
.SIGTERM
)
940 pid
= get_daemon_pid()
943 os
.kill(pid
, signal
.SIGHUP
)
947 if os
.getenv("DISPLAY"):
948 sudo_command
= "gksudo --description \"Chrome Remote Desktop\""
950 sudo_command
= "sudo"
951 command
= ("sudo -k && exec %(sudo)s -- sh -c "
952 "\"groupadd -f %(group)s && gpasswd --add %(user)s %(group)s\"" %
953 { 'group': CHROME_REMOTING_GROUP_NAME
,
954 'user': getpass
.getuser(),
955 'sudo': sudo_command
})
956 os
.execv("/bin/sh", ["/bin/sh", "-c", command
])
959 if options
.host_version
:
960 # TODO(sergeyu): Also check RPM package version once we add RPM package.
961 return os
.system(locate_executable(HOST_BINARY_NAME
) + " --version") >> 8
963 if not options
.start
:
964 # If no modal command-line options specified, print an error and exit.
965 print >> sys
.stderr
, EPILOG
968 # Collate the list of sizes that XRANDR should support.
970 default_sizes
= DEFAULT_SIZES
971 if os
.environ
.has_key(DEFAULT_SIZES_ENV_VAR
):
972 default_sizes
= os
.environ
[DEFAULT_SIZES_ENV_VAR
]
973 options
.size
= default_sizes
.split(",")
976 for size
in options
.size
:
977 size_components
= size
.split("x")
978 if len(size_components
) != 2:
979 parser
.error("Incorrect size format '%s', should be WIDTHxHEIGHT" % size
)
982 width
= int(size_components
[0])
983 height
= int(size_components
[1])
985 # Enforce minimum desktop size, as a sanity-check. The limit of 100 will
986 # detect typos of 2 instead of 3 digits.
987 if width
< 100 or height
< 100:
990 parser
.error("Width and height should be 100 pixels or greater")
992 sizes
.append((width
, height
))
994 # Register an exit handler to clean up session process and the PID file.
995 atexit
.register(cleanup
)
997 # Load the initial host configuration.
998 host_config
= Config(options
.config
)
1001 except (IOError, ValueError) as e
:
1002 print >> sys
.stderr
, "Failed to load config: " + str(e
)
1005 # Register handler to re-load the configuration in response to signals.
1006 for s
in [signal
.SIGHUP
, signal
.SIGINT
, signal
.SIGTERM
]:
1007 signal
.signal(s
, SignalHandler(host_config
))
1009 # Verify that the initial host configuration has the necessary fields.
1010 auth
= Authentication()
1011 auth_config_valid
= auth
.copy_from(host_config
)
1013 host_config_valid
= host
.copy_from(host_config
)
1014 if not host_config_valid
or not auth_config_valid
:
1015 logging
.error("Failed to load host configuration.")
1018 # Determine whether a desktop is already active for the specified host
1019 # host configuration.
1020 pid
= get_daemon_pid()
1022 # Debian policy requires that services should "start" cleanly and return 0
1023 # if they are already running.
1024 print "Service already running."
1027 # Detach a separate "daemon" process to run the session, unless specifically
1028 # requested to run in the foreground.
1029 if not options
.foreground
:
1032 logging
.info("Using host_id: " + host
.host_id
)
1034 desktop
= Desktop(sizes
)
1036 # Keep track of the number of consecutive failures of any child process to
1037 # run for longer than a set period of time. The script will exit after a
1038 # threshold is exceeded.
1039 # There is no point in tracking the X session process separately, since it is
1040 # launched at (roughly) the same time as the X server, and the termination of
1041 # one of these triggers the termination of the other.
1042 x_server_inhibitor
= RelaunchInhibitor("X server")
1043 host_inhibitor
= RelaunchInhibitor("host")
1044 all_inhibitors
= [x_server_inhibitor
, host_inhibitor
]
1046 # Don't allow relaunching the script on the first loop iteration.
1047 allow_relaunch_self
= False
1050 # Set the backoff interval and exit if a process failed too many times.
1051 backoff_time
= SHORT_BACKOFF_TIME
1052 for inhibitor
in all_inhibitors
:
1053 if inhibitor
.failures
>= MAX_LAUNCH_FAILURES
:
1054 logging
.error("Too many launch failures of '%s', exiting."
1057 elif inhibitor
.failures
>= SHORT_BACKOFF_THRESHOLD
:
1058 backoff_time
= LONG_BACKOFF_TIME
1062 # If the session process or X server stops running (e.g. because the user
1063 # logged out), kill the other. This will trigger the next conditional block
1064 # as soon as os.waitpid() reaps its exit-code.
1065 if desktop
.session_proc
is None and desktop
.x_proc
is not None:
1066 logging
.info("Terminating X server")
1067 desktop
.x_proc
.terminate()
1068 elif desktop
.x_proc
is None and desktop
.session_proc
is not None:
1069 logging
.info("Terminating X session")
1070 desktop
.session_proc
.terminate()
1071 elif desktop
.x_proc
is None and desktop
.session_proc
is None:
1072 # Both processes have terminated.
1073 if (allow_relaunch_self
and x_server_inhibitor
.failures
== 0 and
1074 host_inhibitor
.failures
== 0):
1075 # Since the user's desktop is already gone at this point, there's no
1076 # state to lose and now is a good time to pick up any updates to this
1077 # script that might have been installed.
1078 logging
.info("Relaunching self")
1081 # If there is a non-zero |failures| count, restarting the whole script
1082 # would lose this information, so just launch the session as normal.
1083 if x_server_inhibitor
.is_inhibited():
1084 logging
.info("Waiting before launching X server")
1085 relaunch_times
.append(x_server_inhibitor
.earliest_relaunch_time
)
1087 logging
.info("Launching X server and X session.")
1088 desktop
.launch_session(args
)
1089 x_server_inhibitor
.record_started(MINIMUM_PROCESS_LIFETIME
,
1091 allow_relaunch_self
= True
1093 if desktop
.host_proc
is None:
1094 if host_inhibitor
.is_inhibited():
1095 logging
.info("Waiting before launching host process")
1096 relaunch_times
.append(host_inhibitor
.earliest_relaunch_time
)
1098 logging
.info("Launching host process")
1099 desktop
.launch_host(host_config
)
1100 host_inhibitor
.record_started(MINIMUM_PROCESS_LIFETIME
,
1103 deadline
= min(relaunch_times
) if relaunch_times
else 0
1104 pid
, status
= waitpid_handle_exceptions(-1, deadline
)
1108 logging
.info("wait() returned (%s,%s)" % (pid
, status
))
1110 # When a process has terminated, and we've reaped its exit-code, any Popen
1111 # instance for that process is no longer valid. Reset any affected instance
1113 if desktop
.x_proc
is not None and pid
== desktop
.x_proc
.pid
:
1114 logging
.info("X server process terminated")
1115 desktop
.x_proc
= None
1116 x_server_inhibitor
.record_stopped()
1118 if desktop
.session_proc
is not None and pid
== desktop
.session_proc
.pid
:
1119 logging
.info("Session process terminated")
1120 desktop
.session_proc
= None
1122 if desktop
.host_proc
is not None and pid
== desktop
.host_proc
.pid
:
1123 logging
.info("Host process terminated")
1124 desktop
.host_proc
= None
1125 desktop
.host_ready
= False
1126 host_inhibitor
.record_stopped()
1128 # These exit-codes must match the ones used by the host.
1129 # See remoting/host/host_error_codes.h.
1130 # Delete the host or auth configuration depending on the returned error
1131 # code, so the next time this script is run, a new configuration
1132 # will be created and registered.
1133 if os
.WIFEXITED(status
):
1134 if os
.WEXITSTATUS(status
) == 100:
1135 logging
.info("Host configuration is invalid - exiting.")
1136 host_config
.clear_auth()
1137 host_config
.clear_host_info()
1138 host_config
.save_and_log_errors()
1140 elif os
.WEXITSTATUS(status
) == 101:
1141 logging
.info("Host ID has been deleted - exiting.")
1142 host_config
.clear_host_info()
1143 host_config
.save_and_log_errors()
1145 elif os
.WEXITSTATUS(status
) == 102:
1146 logging
.info("OAuth credentials are invalid - exiting.")
1147 host_config
.clear_auth()
1148 host_config
.save_and_log_errors()
1150 elif os
.WEXITSTATUS(status
) == 103:
1151 logging
.info("Host domain is blocked by policy - exiting.")
1152 host_config
.clear_auth()
1153 host_config
.clear_host_info()
1154 host_config
.save_and_log_errors()
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.")
1159 host_config
.clear_auth()
1160 host_config
.clear_host_info()
1161 host_config
.save_and_log_errors()
1164 logging
.info("Host exited with status %s." % os
.WEXITSTATUS(status
))
1165 elif os
.WIFSIGNALED(status
):
1166 logging
.info("Host terminated by signal %s." % os
.WTERMSIG(status
))
1169 if __name__
== "__main__":
1170 logging
.basicConfig(level
=logging
.DEBUG
,
1171 format
="%(asctime)s:%(levelname)s:%(message)s")