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.
34 LOG_FILE_ENV_VAR
= "CHROME_REMOTE_DESKTOP_LOG_FILE"
36 # This script has a sensible default for the initial and maximum desktop size,
37 # which can be overridden either on the command-line, or via a comma-separated
38 # list of sizes in this environment variable.
39 DEFAULT_SIZES_ENV_VAR
= "CHROME_REMOTE_DESKTOP_DEFAULT_DESKTOP_SIZES"
41 # By default, provide a maximum size that is large enough to support clients
42 # with large or multiple monitors. This is a comma-separated list of
43 # resolutions that will be made available if the X server supports RANDR. These
44 # defaults can be overridden in ~/.profile.
45 DEFAULT_SIZES
= "1600x1200,3840x2560"
47 # If RANDR is not available, use a smaller default size. Only a single
48 # resolution is supported in this case.
49 DEFAULT_SIZE_NO_RANDR
= "1600x1200"
51 SCRIPT_PATH
= sys
.path
[0]
53 IS_INSTALLED
= (os
.path
.basename(sys
.argv
[0]) != 'linux_me2me_host.py')
56 HOST_BINARY_NAME
= "chrome-remote-desktop-host"
58 HOST_BINARY_NAME
= "remoting_me2me_host"
60 CHROME_REMOTING_GROUP_NAME
= "chrome-remote-desktop"
62 HOME_DIR
= os
.environ
["HOME"]
63 CONFIG_DIR
= os
.path
.join(HOME_DIR
, ".config/chrome-remote-desktop")
64 SESSION_FILE_PATH
= os
.path
.join(HOME_DIR
, ".chrome-remote-desktop-session")
65 SYSTEM_SESSION_FILE_PATH
= "/etc/chrome-remote-desktop-session"
67 X_LOCK_FILE_TEMPLATE
= "/tmp/.X%d-lock"
68 FIRST_X_DISPLAY_NUMBER
= 20
70 # Amount of time to wait between relaunching processes.
71 SHORT_BACKOFF_TIME
= 5
72 LONG_BACKOFF_TIME
= 60
74 # How long a process must run in order not to be counted against the restart
76 MINIMUM_PROCESS_LIFETIME
= 60
78 # Thresholds for switching from fast- to slow-restart and for giving up
79 # trying to restart entirely.
80 SHORT_BACKOFF_THRESHOLD
= 5
81 MAX_LAUNCH_FAILURES
= SHORT_BACKOFF_THRESHOLD
+ 10
83 # Globals needed by the atexit cleanup() handler.
85 g_host_hash
= hashlib
.md5(socket
.gethostname()).hexdigest()
88 def is_supported_platform():
89 # Always assume that the system is supported if the config directory or
91 if (os
.path
.isdir(CONFIG_DIR
) or os
.path
.isfile(SESSION_FILE_PATH
) or
92 os
.path
.isfile(SYSTEM_SESSION_FILE_PATH
)):
95 # The host has been tested only on Ubuntu.
96 distribution
= platform
.linux_distribution()
97 return (distribution
[0]).lower() == 'ubuntu'
100 def get_randr_supporting_x_server():
101 """Returns a path to an X server that supports the RANDR extension, if this
102 is found on the system. Otherwise returns None."""
104 xvfb
= "/usr/bin/Xvfb-randr"
105 if not os
.path
.exists(xvfb
):
106 xvfb
= locate_executable("Xvfb-randr")
113 def __init__(self
, path
):
119 """Loads the config from file.
122 IOError: Error reading data
123 ValueError: Error parsing JSON
125 settings_file
= open(self
.path
, 'r')
126 self
.data
= json
.load(settings_file
)
128 settings_file
.close()
131 """Saves the config to file.
134 IOError: Error writing data
135 TypeError: Error serialising JSON
139 old_umask
= os
.umask(0066)
141 settings_file
= open(self
.path
, 'w')
142 settings_file
.write(json
.dumps(self
.data
, indent
=2))
143 settings_file
.close()
148 def save_and_log_errors(self
):
149 """Calls self.save(), trapping and logging any errors."""
152 except (IOError, TypeError) as e
:
153 logging
.error("Failed to save config: " + str(e
))
156 return self
.data
.get(key
)
158 def __getitem__(self
, key
):
159 return self
.data
[key
]
161 def __setitem__(self
, key
, value
):
162 self
.data
[key
] = value
170 class Authentication
:
171 """Manage authentication tokens for Chromoting/xmpp"""
175 self
.oauth_refresh_token
= None
177 def copy_from(self
, config
):
178 """Loads the config and returns false if the config is invalid."""
180 self
.login
= config
["xmpp_login"]
181 self
.oauth_refresh_token
= config
["oauth_refresh_token"]
186 def copy_to(self
, config
):
187 config
["xmpp_login"] = self
.login
188 config
["oauth_refresh_token"] = self
.oauth_refresh_token
192 """This manages the configuration for a host."""
195 self
.host_id
= str(uuid
.uuid1())
196 self
.host_name
= socket
.gethostname()
197 self
.host_secret_hash
= None
198 self
.private_key
= None
200 def copy_from(self
, config
):
202 self
.host_id
= config
["host_id"]
203 self
.host_name
= config
["host_name"]
204 self
.host_secret_hash
= config
.get("host_secret_hash")
205 self
.private_key
= config
["private_key"]
210 def copy_to(self
, config
):
211 config
["host_id"] = self
.host_id
212 config
["host_name"] = self
.host_name
213 config
["host_secret_hash"] = self
.host_secret_hash
214 config
["private_key"] = self
.private_key
218 """Manage a single virtual desktop"""
220 def __init__(self
, sizes
):
222 self
.session_proc
= None
223 self
.host_proc
= None
224 self
.child_env
= None
226 self
.pulseaudio_pipe
= None
227 self
.server_supports_exact_resize
= False
228 self
.host_ready
= False
229 self
.ssh_auth_sockname
= None
230 g_desktops
.append(self
)
233 def get_unused_display_number():
234 """Return a candidate display number for which there is currently no
235 X Server lock file"""
236 display
= FIRST_X_DISPLAY_NUMBER
237 while os
.path
.exists(X_LOCK_FILE_TEMPLATE
% display
):
241 def _init_child_env(self
):
242 # Create clean environment for new session, so it is cleanly separated from
243 # the user's console X session.
255 if os
.environ
.has_key(key
):
256 self
.child_env
[key
] = os
.environ
[key
]
258 # Ensure that the software-rendering GL drivers are loaded by the desktop
259 # session, instead of any hardware GL drivers installed on the system.
260 self
.child_env
["LD_LIBRARY_PATH"] = (
261 "/usr/lib/%(arch)s-linux-gnu/mesa:"
262 "/usr/lib/%(arch)s-linux-gnu/dri:"
263 "/usr/lib/%(arch)s-linux-gnu/gallium-pipe" %
264 { "arch": platform
.machine() })
266 # Read from /etc/environment if it exists, as it is a standard place to
267 # store system-wide environment settings. During a normal login, this would
268 # typically be done by the pam_env PAM module, depending on the local PAM
270 env_filename
= "/etc/environment"
272 with
open(env_filename
, "r") as env_file
:
273 for line
in env_file
:
274 line
= line
.rstrip("\n")
275 # Split at the first "=", leaving any further instances in the value.
276 key_value_pair
= line
.split("=", 1)
277 if len(key_value_pair
) == 2:
278 key
, value
= tuple(key_value_pair
)
279 # The file stores key=value assignments, but the value may be
280 # quoted, so strip leading & trailing quotes from it.
281 value
= value
.strip("'\"")
282 self
.child_env
[key
] = value
284 logging
.info("Failed to read %s, skipping." % env_filename
)
286 def _setup_pulseaudio(self
):
287 self
.pulseaudio_pipe
= None
289 # pulseaudio uses UNIX sockets for communication. Length of UNIX socket
290 # name is limited to 108 characters, so audio will not work properly if
291 # the path is too long. To workaround this problem we use only first 10
292 # symbols of the host hash.
293 pulse_path
= os
.path
.join(CONFIG_DIR
,
294 "pulseaudio#%s" % g_host_hash
[0:10])
295 if len(pulse_path
) + len("/native") >= 108:
296 logging
.error("Audio will not be enabled because pulseaudio UNIX " +
297 "socket path is too long.")
300 sink_name
= "chrome_remote_desktop_session"
301 pipe_name
= os
.path
.join(pulse_path
, "fifo_output")
304 if not os
.path
.exists(pulse_path
):
307 logging
.error("Failed to create pulseaudio pipe: " + str(e
))
311 pulse_config
= open(os
.path
.join(pulse_path
, "daemon.conf"), "w")
312 pulse_config
.write("default-sample-format = s16le\n")
313 pulse_config
.write("default-sample-rate = 48000\n")
314 pulse_config
.write("default-sample-channels = 2\n")
317 pulse_script
= open(os
.path
.join(pulse_path
, "default.pa"), "w")
318 pulse_script
.write("load-module module-native-protocol-unix\n")
320 ("load-module module-pipe-sink sink_name=%s file=\"%s\" " +
321 "rate=48000 channels=2 format=s16le\n") %
322 (sink_name
, pipe_name
))
325 logging
.error("Failed to write pulseaudio config: " + str(e
))
328 self
.child_env
["PULSE_CONFIG_PATH"] = pulse_path
329 self
.child_env
["PULSE_RUNTIME_PATH"] = pulse_path
330 self
.child_env
["PULSE_STATE_PATH"] = pulse_path
331 self
.child_env
["PULSE_SINK"] = sink_name
332 self
.pulseaudio_pipe
= pipe_name
336 def _setup_gnubby(self
):
337 self
.ssh_auth_sockname
= ("/tmp/chromoting.%s.ssh_auth_sock" %
340 def _launch_x_server(self
, extra_x_args
):
341 x_auth_file
= os
.path
.expanduser("~/.Xauthority")
342 self
.child_env
["XAUTHORITY"] = x_auth_file
343 devnull
= open(os
.devnull
, "rw")
344 display
= self
.get_unused_display_number()
346 # Run "xauth add" with |child_env| so that it modifies the same XAUTHORITY
347 # file which will be used for the X session.
348 ret_code
= subprocess
.call("xauth add :%d . `mcookie`" % display
,
349 env
=self
.child_env
, shell
=True)
351 raise Exception("xauth failed with code %d" % ret_code
)
353 max_width
= max([width
for width
, height
in self
.sizes
])
354 max_height
= max([height
for width
, height
in self
.sizes
])
356 xvfb
= get_randr_supporting_x_server()
358 self
.server_supports_exact_resize
= True
361 self
.server_supports_exact_resize
= False
363 # Disable the Composite extension iff the X session is the default
364 # Unity-2D, since it uses Metacity which fails to generate DAMAGE
365 # notifications correctly. See crbug.com/166468.
366 x_session
= choose_x_session()
367 if (len(x_session
) == 2 and
368 x_session
[1] == "/usr/bin/gnome-session --session=ubuntu-2d"):
369 extra_x_args
.extend(["-extension", "Composite"])
371 logging
.info("Starting %s on display :%d" % (xvfb
, display
))
372 screen_option
= "%dx%dx24" % (max_width
, max_height
)
373 self
.x_proc
= subprocess
.Popen(
374 [xvfb
, ":%d" % display
,
375 "-auth", x_auth_file
,
378 "-screen", "0", screen_option
380 if not self
.x_proc
.pid
:
381 raise Exception("Could not start Xvfb.")
383 self
.child_env
["DISPLAY"] = ":%d" % display
384 self
.child_env
["CHROME_REMOTE_DESKTOP_SESSION"] = "1"
386 # Use a separate profile for any instances of Chrome that are started in
387 # the virtual session. Chrome doesn't support sharing a profile between
388 # multiple DISPLAYs, but Chrome Sync allows for a reasonable compromise.
389 chrome_profile
= os
.path
.join(CONFIG_DIR
, "chrome-profile")
390 self
.child_env
["CHROME_USER_DATA_DIR"] = chrome_profile
392 # Set SSH_AUTH_SOCK to the file name to listen on.
393 if self
.ssh_auth_sockname
:
394 self
.child_env
["SSH_AUTH_SOCK"] = self
.ssh_auth_sockname
396 # Wait for X to be active.
397 for _test
in range(5):
398 proc
= subprocess
.Popen("xdpyinfo", env
=self
.child_env
, stdout
=devnull
)
399 _pid
, retcode
= os
.waitpid(proc
.pid
, 0)
404 raise Exception("Could not connect to Xvfb.")
406 logging
.info("Xvfb is active.")
408 # The remoting host expects the server to use "evdev" keycodes, but Xvfb
409 # starts configured to use the "base" ruleset, resulting in XKB configuring
410 # for "xfree86" keycodes, and screwing up some keys. See crbug.com/119013.
411 # Reconfigure the X server to use "evdev" keymap rules. The X server must
412 # be started with -noreset otherwise it'll reset as soon as the command
413 # completes, since there are no other X clients running yet.
414 proc
= subprocess
.Popen("setxkbmap -rules evdev", env
=self
.child_env
,
416 _pid
, retcode
= os
.waitpid(proc
.pid
, 0)
418 logging
.error("Failed to set XKB to 'evdev'")
420 # Register the screen sizes if the X server's RANDR extension supports it.
421 # Errors here are non-fatal; the X server will continue to run with the
422 # dimensions from the "-screen" option.
423 for width
, height
in self
.sizes
:
424 label
= "%dx%d" % (width
, height
)
425 args
= ["xrandr", "--newmode", label
, "0", str(width
), "0", "0", "0",
426 str(height
), "0", "0", "0"]
427 subprocess
.call(args
, env
=self
.child_env
, stdout
=devnull
, stderr
=devnull
)
428 args
= ["xrandr", "--addmode", "screen", label
]
429 subprocess
.call(args
, env
=self
.child_env
, stdout
=devnull
, stderr
=devnull
)
431 # Set the initial mode to the first size specified, otherwise the X server
432 # would default to (max_width, max_height), which might not even be in the
434 label
= "%dx%d" % self
.sizes
[0]
435 args
= ["xrandr", "-s", label
]
436 subprocess
.call(args
, env
=self
.child_env
, stdout
=devnull
, stderr
=devnull
)
438 # Set the physical size of the display so that the initial mode is running
439 # at approximately 96 DPI, since some desktops require the DPI to be set to
440 # something realistic.
441 args
= ["xrandr", "--dpi", "96"]
442 subprocess
.call(args
, env
=self
.child_env
, stdout
=devnull
, stderr
=devnull
)
446 def _launch_x_session(self
):
447 # Start desktop session.
448 # The /dev/null input redirection is necessary to prevent the X session
449 # reading from stdin. If this code runs as a shell background job in a
450 # terminal, any reading from stdin causes the job to be suspended.
451 # Daemonization would solve this problem by separating the process from the
452 # controlling terminal.
453 xsession_command
= choose_x_session()
454 if xsession_command
is None:
455 raise Exception("Unable to choose suitable X session command.")
457 logging
.info("Launching X session: %s" % xsession_command
)
458 self
.session_proc
= subprocess
.Popen(xsession_command
,
459 stdin
=open(os
.devnull
, "r"),
462 if not self
.session_proc
.pid
:
463 raise Exception("Could not start X session")
465 def launch_session(self
, x_args
):
466 self
._init
_child
_env
()
467 self
._setup
_pulseaudio
()
469 self
._launch
_x
_server
(x_args
)
470 self
._launch
_x
_session
()
472 def launch_host(self
, host_config
):
473 # Start remoting host
474 args
= [locate_executable(HOST_BINARY_NAME
), "--host-config=-"]
475 if self
.pulseaudio_pipe
:
476 args
.append("--audio-pipe-name=%s" % self
.pulseaudio_pipe
)
477 if self
.server_supports_exact_resize
:
478 args
.append("--server-supports-exact-resize")
479 if self
.ssh_auth_sockname
:
480 args
.append("--ssh-auth-sockname=%s" % self
.ssh_auth_sockname
)
482 # Have the host process use SIGUSR1 to signal a successful start.
483 def sigusr1_handler(signum
, frame
):
485 logging
.info("Host ready to receive connections.")
486 self
.host_ready
= True
487 if (ParentProcessLogger
.instance() and
488 False not in [desktop
.host_ready
for desktop
in g_desktops
]):
489 ParentProcessLogger
.instance().release_parent()
491 signal
.signal(signal
.SIGUSR1
, sigusr1_handler
)
492 args
.append("--signal-parent")
494 self
.host_proc
= subprocess
.Popen(args
, env
=self
.child_env
,
495 stdin
=subprocess
.PIPE
)
497 if not self
.host_proc
.pid
:
498 raise Exception("Could not start Chrome Remote Desktop host")
499 self
.host_proc
.stdin
.write(json
.dumps(host_config
.data
))
500 self
.host_proc
.stdin
.close()
503 def get_daemon_proc():
504 """Checks if there is already an instance of this script running, and returns
505 a psutil.Process instance for it.
508 A Process instance for the existing daemon process, or None if the daemon
513 this_pid
= os
.getpid()
515 # Support new & old psutil API. This is the right way to check, according to
516 # http://grodola.blogspot.com/2014/01/psutil-20-porting.html
517 if psutil
.version_info
>= (2, 0):
518 psget
= lambda x
: x()
522 for process
in psutil
.process_iter():
523 # Skip any processes that raise an exception, as processes may terminate
524 # during iteration over the list.
526 # Skip other users' processes.
527 if psget(process
.uids
).real
!= uid
:
530 # Skip the process for this instance.
531 if process
.pid
== this_pid
:
534 # |cmdline| will be [python-interpreter, script-file, other arguments...]
535 cmdline
= psget(process
.cmdline
)
538 if cmdline
[0] == sys
.executable
and cmdline
[1] == sys
.argv
[0]:
540 except (psutil
.NoSuchProcess
, psutil
.AccessDenied
):
546 def choose_x_session():
547 """Chooses the most appropriate X session command for this system.
550 A string containing the command to run, or a list of strings containing
551 the executable program and its arguments, which is suitable for passing as
552 the first parameter of subprocess.Popen(). If a suitable session cannot
553 be found, returns None.
557 SYSTEM_SESSION_FILE_PATH
]
558 for startup_file
in XSESSION_FILES
:
559 startup_file
= os
.path
.expanduser(startup_file
)
560 if os
.path
.exists(startup_file
):
561 if os
.access(startup_file
, os
.X_OK
):
562 # "/bin/sh -c" is smart about how to execute the session script and
563 # works in cases where plain exec() fails (for example, if the file is
564 # marked executable, but is a plain script with no shebang line).
565 return ["/bin/sh", "-c", pipes
.quote(startup_file
)]
567 # If this is a system-wide session script, it should be run using the
568 # system shell, ignoring any login shell that might be set for the
570 return ["/bin/sh", startup_file
]
572 # Choose a session wrapper script to run the session. On some systems,
573 # /etc/X11/Xsession fails to load the user's .profile, so look for an
574 # alternative wrapper that is more likely to match the script that the
575 # system actually uses for console desktop sessions.
577 "/usr/sbin/lightdm-session",
579 "/etc/X11/Xsession" ]
580 for session_wrapper
in SESSION_WRAPPERS
:
581 if os
.path
.exists(session_wrapper
):
582 if os
.path
.exists("/usr/bin/unity-2d-panel"):
583 # On Ubuntu 12.04, the default session relies on 3D-accelerated
584 # hardware. Trying to run this with a virtual X display produces
585 # weird results on some systems (for example, upside-down and
586 # corrupt displays). So if the ubuntu-2d session is available,
587 # choose it explicitly.
588 return [session_wrapper
, "/usr/bin/gnome-session --session=ubuntu-2d"]
590 # Use the session wrapper by itself, and let the system choose a
592 return [session_wrapper
]
596 def locate_executable(exe_name
):
598 # If the script is running from its installed location, search the host
599 # binary only in the same directory.
600 paths_to_try
= [ SCRIPT_PATH
]
602 paths_to_try
= map(lambda p
: os
.path
.join(SCRIPT_PATH
, p
),
603 [".", "../../../out/Debug", "../../../out/Release" ])
604 for path
in paths_to_try
:
605 exe_path
= os
.path
.join(path
, exe_name
)
606 if os
.path
.exists(exe_path
):
609 raise Exception("Could not locate executable '%s'" % exe_name
)
612 class ParentProcessLogger(object):
613 """Redirects logs to the parent process, until the host is ready or quits.
615 This class creates a pipe to allow logging from the daemon process to be
616 copied to the parent process. The daemon process adds a log-handler that
617 directs logging output to the pipe. The parent process reads from this pipe
618 until and writes the content to stderr. When the pipe is no longer needed
619 (for example, the host signals successful launch or permanent failure), the
620 daemon removes the log-handler and closes the pipe, causing the the parent
621 process to reach end-of-file while reading the pipe and exit.
623 The (singleton) logger should be instantiated before forking. The parent
624 process should call wait_for_logs() before exiting. The (grand-)child process
625 should call start_logging() when it starts, and then use logging.* to issue
626 log statements, as usual. When the child has either succesfully started the
627 host or terminated, it must call release_parent() to allow the parent to exit.
633 """Constructor. Must be called before forking."""
634 read_pipe
, write_pipe
= os
.pipe()
635 # Ensure write_pipe is closed on exec, otherwise it will be kept open by
636 # child processes (X, host), preventing the read pipe from EOF'ing.
637 old_flags
= fcntl
.fcntl(write_pipe
, fcntl
.F_GETFD
)
638 fcntl
.fcntl(write_pipe
, fcntl
.F_SETFD
, old_flags | fcntl
.FD_CLOEXEC
)
639 self
._read
_file
= os
.fdopen(read_pipe
, 'r')
640 self
._write
_file
= os
.fdopen(write_pipe
, 'a')
641 self
._logging
_handler
= None
642 ParentProcessLogger
.__instance
= self
644 def start_logging(self
):
645 """Installs a logging handler that sends log entries to a pipe.
647 Must be called by the child process.
649 self
._read
_file
.close()
650 self
._logging
_handler
= logging
.StreamHandler(self
._write
_file
)
651 logging
.getLogger().addHandler(self
._logging
_handler
)
653 def release_parent(self
):
654 """Uninstalls logging handler and closes the pipe, releasing the parent.
656 Must be called by the child process.
658 if self
._logging
_handler
:
659 logging
.getLogger().removeHandler(self
._logging
_handler
)
660 self
._logging
_handler
= None
661 if not self
._write
_file
.closed
:
662 self
._write
_file
.close()
664 def wait_for_logs(self
):
665 """Waits and prints log lines from the daemon until the pipe is closed.
667 Must be called by the parent process.
669 # If Ctrl-C is pressed, inform the user that the daemon is still running.
670 # This signal will cause the read loop below to stop with an EINTR IOError.
671 def sigint_handler(signum
, frame
):
673 print >> sys
.stderr
, ("Interrupted. The daemon is still running in the "
676 signal
.signal(signal
.SIGINT
, sigint_handler
)
678 # Install a fallback timeout to release the parent process, in case the
679 # daemon never responds (e.g. host crash-looping, daemon killed).
680 # This signal will cause the read loop below to stop with an EINTR IOError.
681 def sigalrm_handler(signum
, frame
):
683 print >> sys
.stderr
, ("No response from daemon. It may have crashed, or "
684 "may still be running in the background.")
686 signal
.signal(signal
.SIGALRM
, sigalrm_handler
)
689 self
._write
_file
.close()
691 # Print lines as they're logged to the pipe until EOF is reached or readline
692 # is interrupted by one of the signal handlers above.
694 for line
in iter(self
._read
_file
.readline
, ''):
695 sys
.stderr
.write(line
)
697 if e
.errno
!= errno
.EINTR
:
699 print >> sys
.stderr
, "Log file: %s" % os
.environ
[LOG_FILE_ENV_VAR
]
703 """Returns the singleton instance, if it exists."""
704 return ParentProcessLogger
.__instance
708 """Background this process and detach from controlling terminal, redirecting
709 stdout/stderr to a log file."""
711 # TODO(lambroslambrou): Having stdout/stderr redirected to a log file is not
712 # ideal - it could create a filesystem DoS if the daemon or a child process
713 # were to write excessive amounts to stdout/stderr. Ideally, stdout/stderr
714 # should be redirected to a pipe or socket, and a process at the other end
715 # should consume the data and write it to a logging facility which can do
716 # data-capping or log-rotation. The 'logger' command-line utility could be
717 # used for this, but it might cause too much syslog spam.
719 # Create new (temporary) file-descriptors before forking, so any errors get
720 # reported to the main process and set the correct exit-code.
721 # The mode is provided, since Python otherwise sets a default mode of 0777,
722 # which would result in the new file having permissions of 0777 & ~umask,
723 # possibly leaving the executable bits set.
724 if not os
.environ
.has_key(LOG_FILE_ENV_VAR
):
725 log_file_prefix
= "chrome_remote_desktop_%s_" % time
.strftime(
726 '%Y%m%d_%H%M%S', time
.localtime(time
.time()))
727 log_file
= tempfile
.NamedTemporaryFile(prefix
=log_file_prefix
, delete
=False)
728 os
.environ
[LOG_FILE_ENV_VAR
] = log_file
.name
729 log_fd
= log_file
.file.fileno()
731 log_fd
= os
.open(os
.environ
[LOG_FILE_ENV_VAR
],
732 os
.O_WRONLY | os
.O_CREAT | os
.O_APPEND
, 0600)
734 devnull_fd
= os
.open(os
.devnull
, os
.O_RDONLY
)
736 parent_logger
= ParentProcessLogger()
744 # The second fork ensures that the daemon isn't a session leader, so that
745 # it doesn't acquire a controlling terminal.
753 os
._exit
(0) # pylint: disable=W0212
756 parent_logger
.wait_for_logs()
757 os
._exit
(0) # pylint: disable=W0212
759 logging
.info("Daemon process started in the background, logging to '%s'" %
760 os
.environ
[LOG_FILE_ENV_VAR
])
764 parent_logger
.start_logging()
766 # Copy the file-descriptors to create new stdin, stdout and stderr. Note
767 # that dup2(oldfd, newfd) closes newfd first, so this will close the current
768 # stdin, stdout and stderr, detaching from the terminal.
769 os
.dup2(devnull_fd
, sys
.stdin
.fileno())
770 os
.dup2(log_fd
, sys
.stdout
.fileno())
771 os
.dup2(log_fd
, sys
.stderr
.fileno())
773 # Close the temporary file-descriptors.
779 logging
.info("Cleanup.")
782 for desktop
in g_desktops
:
784 logging
.info("Terminating Xvfb")
785 desktop
.x_proc
.terminate()
787 if ParentProcessLogger
.instance():
788 ParentProcessLogger
.instance().release_parent()
791 """Reload the config file on SIGHUP. Since we pass the configuration to the
792 host processes via stdin, they can't reload it, so terminate them. They will
793 be relaunched automatically with the new config."""
795 def __init__(self
, host_config
):
796 self
.host_config
= host_config
798 def __call__(self
, signum
, _stackframe
):
799 if signum
== signal
.SIGHUP
:
800 logging
.info("SIGHUP caught, restarting host.")
802 self
.host_config
.load()
803 except (IOError, ValueError) as e
:
804 logging
.error("Failed to load config: " + str(e
))
805 for desktop
in g_desktops
:
806 if desktop
.host_proc
:
807 desktop
.host_proc
.send_signal(signal
.SIGTERM
)
809 # Exit cleanly so the atexit handler, cleanup(), gets called.
813 class RelaunchInhibitor
:
814 """Helper class for inhibiting launch of a child process before a timeout has
817 A managed process can be in one of these states:
818 running, not inhibited (running == True)
819 stopped and inhibited (running == False and is_inhibited() == True)
820 stopped but not inhibited (running == False and is_inhibited() == False)
823 label: Name of the tracked process. Only used for logging.
824 running: Whether the process is currently running.
825 earliest_relaunch_time: Time before which the process should not be
826 relaunched, or 0 if there is no limit.
827 failures: The number of times that the process ran for less than a
828 specified timeout, and had to be inhibited. This count is reset to 0
829 whenever the process has run for longer than the timeout.
832 def __init__(self
, label
):
835 self
.earliest_relaunch_time
= 0
836 self
.earliest_successful_termination
= 0
839 def is_inhibited(self
):
840 return (not self
.running
) and (time
.time() < self
.earliest_relaunch_time
)
842 def record_started(self
, minimum_lifetime
, relaunch_delay
):
843 """Record that the process was launched, and set the inhibit time to
844 |timeout| seconds in the future."""
845 self
.earliest_relaunch_time
= time
.time() + relaunch_delay
846 self
.earliest_successful_termination
= time
.time() + minimum_lifetime
849 def record_stopped(self
):
850 """Record that the process was stopped, and adjust the failure count
851 depending on whether the process ran long enough."""
853 if time
.time() < self
.earliest_successful_termination
:
857 logging
.info("Failure count for '%s' is now %d", self
.label
, self
.failures
)
862 os
.execvp(sys
.argv
[0], sys
.argv
)
865 def waitpid_with_timeout(pid
, deadline
):
866 """Wrapper around os.waitpid() which waits until either a child process dies
867 or the deadline elapses.
870 pid: Process ID to wait for, or -1 to wait for any child process.
871 deadline: Waiting stops when time.time() exceeds this value.
874 (pid, status): Same as for os.waitpid(), except that |pid| is 0 if no child
875 changed state within the timeout.
878 Same as for os.waitpid().
880 while time
.time() < deadline
:
881 pid
, status
= os
.waitpid(pid
, os
.WNOHANG
)
888 def waitpid_handle_exceptions(pid
, deadline
):
889 """Wrapper around os.waitpid()/waitpid_with_timeout(), which waits until
890 either a child process exits or the deadline elapses, and retries if certain
894 pid: Process ID to wait for, or -1 to wait for any child process.
895 deadline: If non-zero, waiting stops when time.time() exceeds this value.
896 If zero, waiting stops when a child process exits.
899 (pid, status): Same as for waitpid_with_timeout(). |pid| is non-zero if and
900 only if a child exited during the wait.
903 Same as for os.waitpid(), except:
904 OSError with errno==EINTR causes the wait to be retried (this can happen,
905 for example, if this parent process receives SIGHUP).
906 OSError with errno==ECHILD means there are no child processes, and so
907 this function sleeps until |deadline|. If |deadline| is zero, this is an
908 error and the OSError exception is raised in this case.
913 pid_result
, status
= os
.waitpid(pid
, 0)
915 pid_result
, status
= waitpid_with_timeout(pid
, deadline
)
916 return (pid_result
, status
)
918 if e
.errno
== errno
.EINTR
:
920 elif e
.errno
== errno
.ECHILD
:
923 # No time-limit and no child processes. This is treated as an error
927 time
.sleep(deadline
- now
)
930 # Anything else is an unexpected error.
935 EPILOG
= """This script is not intended for use by end-users. To configure
936 Chrome Remote Desktop, please install the app from the Chrome
937 Web Store: https://chrome.google.com/remotedesktop"""
938 parser
= optparse
.OptionParser(
939 usage
="Usage: %prog [options] [ -- [ X server options ] ]",
941 parser
.add_option("-s", "--size", dest
="size", action
="append",
942 help="Dimensions of virtual desktop. This can be specified "
943 "multiple times to make multiple screen resolutions "
944 "available (if the Xvfb server supports this).")
945 parser
.add_option("-f", "--foreground", dest
="foreground", default
=False,
947 help="Don't run as a background daemon.")
948 parser
.add_option("", "--start", dest
="start", default
=False,
950 help="Start the host.")
951 parser
.add_option("-k", "--stop", dest
="stop", default
=False,
953 help="Stop the daemon currently running.")
954 parser
.add_option("", "--get-status", dest
="get_status", default
=False,
956 help="Prints host status")
957 parser
.add_option("", "--check-running", dest
="check_running", default
=False,
959 help="Return 0 if the daemon is running, or 1 otherwise.")
960 parser
.add_option("", "--config", dest
="config", action
="store",
961 help="Use the specified configuration file.")
962 parser
.add_option("", "--reload", dest
="reload", default
=False,
964 help="Signal currently running host to reload the config.")
965 parser
.add_option("", "--add-user", dest
="add_user", default
=False,
967 help="Add current user to the chrome-remote-desktop group.")
968 parser
.add_option("", "--host-version", dest
="host_version", default
=False,
970 help="Prints version of the host.")
971 (options
, args
) = parser
.parse_args()
973 # Determine the filename of the host configuration and PID files.
974 if not options
.config
:
975 options
.config
= os
.path
.join(CONFIG_DIR
, "host#%s.json" % g_host_hash
)
977 # Check for a modal command-line option (start, stop, etc.)
979 if options
.get_status
:
980 proc
= get_daemon_proc()
983 elif is_supported_platform():
986 print "NOT_IMPLEMENTED"
989 # TODO(sergeyu): Remove --check-running once NPAPI plugin and NM host are
990 # updated to always use get-status flag instead.
991 if options
.check_running
:
992 proc
= get_daemon_proc()
993 return 1 if proc
is None else 0
996 proc
= get_daemon_proc()
998 print "The daemon is not currently running"
1000 print "Killing process %s" % proc
.pid
1003 proc
.wait(timeout
=30)
1004 except psutil
.TimeoutExpired
:
1005 print "Timed out trying to kill daemon process"
1010 proc
= get_daemon_proc()
1013 proc
.send_signal(signal
.SIGHUP
)
1016 if options
.add_user
:
1017 user
= getpass
.getuser()
1019 if user
in grp
.getgrnam(CHROME_REMOTING_GROUP_NAME
).gr_mem
:
1020 logging
.info("User '%s' is already a member of '%s'." %
1021 (user
, CHROME_REMOTING_GROUP_NAME
))
1024 logging
.info("Group '%s' not found." % CHROME_REMOTING_GROUP_NAME
)
1026 if os
.getenv("DISPLAY"):
1027 sudo_command
= "gksudo --description \"Chrome Remote Desktop\""
1029 sudo_command
= "sudo"
1030 command
= ("sudo -k && exec %(sudo)s -- sh -c "
1031 "\"groupadd -f %(group)s && gpasswd --add %(user)s %(group)s\"" %
1032 { 'group': CHROME_REMOTING_GROUP_NAME
,
1034 'sudo': sudo_command
})
1035 os
.execv("/bin/sh", ["/bin/sh", "-c", command
])
1038 if options
.host_version
:
1039 # TODO(sergeyu): Also check RPM package version once we add RPM package.
1040 return os
.system(locate_executable(HOST_BINARY_NAME
) + " --version") >> 8
1042 if not options
.start
:
1043 # If no modal command-line options specified, print an error and exit.
1044 print >> sys
.stderr
, EPILOG
1047 # If a RANDR-supporting Xvfb is not available, limit the default size to
1048 # something more sensible.
1049 if get_randr_supporting_x_server():
1050 default_sizes
= DEFAULT_SIZES
1052 default_sizes
= DEFAULT_SIZE_NO_RANDR
1054 # Collate the list of sizes that XRANDR should support.
1055 if not options
.size
:
1056 if os
.environ
.has_key(DEFAULT_SIZES_ENV_VAR
):
1057 default_sizes
= os
.environ
[DEFAULT_SIZES_ENV_VAR
]
1058 options
.size
= default_sizes
.split(",")
1061 for size
in options
.size
:
1062 size_components
= size
.split("x")
1063 if len(size_components
) != 2:
1064 parser
.error("Incorrect size format '%s', should be WIDTHxHEIGHT" % size
)
1067 width
= int(size_components
[0])
1068 height
= int(size_components
[1])
1070 # Enforce minimum desktop size, as a sanity-check. The limit of 100 will
1071 # detect typos of 2 instead of 3 digits.
1072 if width
< 100 or height
< 100:
1075 parser
.error("Width and height should be 100 pixels or greater")
1077 sizes
.append((width
, height
))
1079 # Register an exit handler to clean up session process and the PID file.
1080 atexit
.register(cleanup
)
1082 # Load the initial host configuration.
1083 host_config
= Config(options
.config
)
1086 except (IOError, ValueError) as e
:
1087 print >> sys
.stderr
, "Failed to load config: " + str(e
)
1090 # Register handler to re-load the configuration in response to signals.
1091 for s
in [signal
.SIGHUP
, signal
.SIGINT
, signal
.SIGTERM
]:
1092 signal
.signal(s
, SignalHandler(host_config
))
1094 # Verify that the initial host configuration has the necessary fields.
1095 auth
= Authentication()
1096 auth_config_valid
= auth
.copy_from(host_config
)
1098 host_config_valid
= host
.copy_from(host_config
)
1099 if not host_config_valid
or not auth_config_valid
:
1100 logging
.error("Failed to load host configuration.")
1103 # Determine whether a desktop is already active for the specified host
1104 # host configuration.
1105 proc
= get_daemon_proc()
1106 if proc
is not None:
1107 # Debian policy requires that services should "start" cleanly and return 0
1108 # if they are already running.
1109 print "Service already running."
1112 # Detach a separate "daemon" process to run the session, unless specifically
1113 # requested to run in the foreground.
1114 if not options
.foreground
:
1117 logging
.info("Using host_id: " + host
.host_id
)
1119 desktop
= Desktop(sizes
)
1121 # Keep track of the number of consecutive failures of any child process to
1122 # run for longer than a set period of time. The script will exit after a
1123 # threshold is exceeded.
1124 # There is no point in tracking the X session process separately, since it is
1125 # launched at (roughly) the same time as the X server, and the termination of
1126 # one of these triggers the termination of the other.
1127 x_server_inhibitor
= RelaunchInhibitor("X server")
1128 host_inhibitor
= RelaunchInhibitor("host")
1129 all_inhibitors
= [x_server_inhibitor
, host_inhibitor
]
1131 # Don't allow relaunching the script on the first loop iteration.
1132 allow_relaunch_self
= False
1135 # Set the backoff interval and exit if a process failed too many times.
1136 backoff_time
= SHORT_BACKOFF_TIME
1137 for inhibitor
in all_inhibitors
:
1138 if inhibitor
.failures
>= MAX_LAUNCH_FAILURES
:
1139 logging
.error("Too many launch failures of '%s', exiting."
1142 elif inhibitor
.failures
>= SHORT_BACKOFF_THRESHOLD
:
1143 backoff_time
= LONG_BACKOFF_TIME
1147 # If the session process or X server stops running (e.g. because the user
1148 # logged out), kill the other. This will trigger the next conditional block
1149 # as soon as os.waitpid() reaps its exit-code.
1150 if desktop
.session_proc
is None and desktop
.x_proc
is not None:
1151 logging
.info("Terminating X server")
1152 desktop
.x_proc
.terminate()
1153 elif desktop
.x_proc
is None and desktop
.session_proc
is not None:
1154 logging
.info("Terminating X session")
1155 desktop
.session_proc
.terminate()
1156 elif desktop
.x_proc
is None and desktop
.session_proc
is None:
1157 # Both processes have terminated.
1158 if (allow_relaunch_self
and x_server_inhibitor
.failures
== 0 and
1159 host_inhibitor
.failures
== 0):
1160 # Since the user's desktop is already gone at this point, there's no
1161 # state to lose and now is a good time to pick up any updates to this
1162 # script that might have been installed.
1163 logging
.info("Relaunching self")
1166 # If there is a non-zero |failures| count, restarting the whole script
1167 # would lose this information, so just launch the session as normal.
1168 if x_server_inhibitor
.is_inhibited():
1169 logging
.info("Waiting before launching X server")
1170 relaunch_times
.append(x_server_inhibitor
.earliest_relaunch_time
)
1172 logging
.info("Launching X server and X session.")
1173 desktop
.launch_session(args
)
1174 x_server_inhibitor
.record_started(MINIMUM_PROCESS_LIFETIME
,
1176 allow_relaunch_self
= True
1178 if desktop
.host_proc
is None:
1179 if host_inhibitor
.is_inhibited():
1180 logging
.info("Waiting before launching host process")
1181 relaunch_times
.append(host_inhibitor
.earliest_relaunch_time
)
1183 logging
.info("Launching host process")
1184 desktop
.launch_host(host_config
)
1185 host_inhibitor
.record_started(MINIMUM_PROCESS_LIFETIME
,
1188 deadline
= min(relaunch_times
) if relaunch_times
else 0
1189 pid
, status
= waitpid_handle_exceptions(-1, deadline
)
1193 logging
.info("wait() returned (%s,%s)" % (pid
, status
))
1195 # When a process has terminated, and we've reaped its exit-code, any Popen
1196 # instance for that process is no longer valid. Reset any affected instance
1198 if desktop
.x_proc
is not None and pid
== desktop
.x_proc
.pid
:
1199 logging
.info("X server process terminated")
1200 desktop
.x_proc
= None
1201 x_server_inhibitor
.record_stopped()
1203 if desktop
.session_proc
is not None and pid
== desktop
.session_proc
.pid
:
1204 logging
.info("Session process terminated")
1205 desktop
.session_proc
= None
1207 if desktop
.host_proc
is not None and pid
== desktop
.host_proc
.pid
:
1208 logging
.info("Host process terminated")
1209 desktop
.host_proc
= None
1210 desktop
.host_ready
= False
1211 host_inhibitor
.record_stopped()
1213 # These exit-codes must match the ones used by the host.
1214 # See remoting/host/host_error_codes.h.
1215 # Delete the host or auth configuration depending on the returned error
1216 # code, so the next time this script is run, a new configuration
1217 # will be created and registered.
1218 if os
.WIFEXITED(status
):
1219 if os
.WEXITSTATUS(status
) == 100:
1220 logging
.info("Host configuration is invalid - exiting.")
1222 elif os
.WEXITSTATUS(status
) == 101:
1223 logging
.info("Host ID has been deleted - exiting.")
1225 host_config
.save_and_log_errors()
1227 elif os
.WEXITSTATUS(status
) == 102:
1228 logging
.info("OAuth credentials are invalid - exiting.")
1230 elif os
.WEXITSTATUS(status
) == 103:
1231 logging
.info("Host domain is blocked by policy - exiting.")
1233 # Nothing to do for Mac-only status 104 (login screen unsupported)
1234 elif os
.WEXITSTATUS(status
) == 105:
1235 logging
.info("Username is blocked by policy - exiting.")
1238 logging
.info("Host exited with status %s." % os
.WEXITSTATUS(status
))
1239 elif os
.WIFSIGNALED(status
):
1240 logging
.info("Host terminated by signal %s." % os
.WTERMSIG(status
))
1243 if __name__
== "__main__":
1244 logging
.basicConfig(level
=logging
.DEBUG
,
1245 format
="%(asctime)s:%(levelname)s:%(message)s")