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