· 6 years ago · Nov 18, 2019, 08:52 PM
1#!/usr/bin/python2
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.
5
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.
11
12from __future__ import print_function
13
14import sys
15if sys.version_info[0] != 2 or sys.version_info[1] < 7:
16 print("This script requires Python version 2.7")
17 sys.exit(1)
18
19import argparse
20import atexit
21import errno
22import fcntl
23import getpass
24import grp
25import hashlib
26import json
27import logging
28import os
29import pipes
30import platform
31import psutil
32import platform
33import pwd
34import re
35import signal
36import socket
37import subprocess
38import tempfile
39import threading
40import time
41import uuid
42
43# If this env var is defined, extra host params will be loaded from this env var
44# as a list of strings separated by space (\s+). Note that param that contains
45# space is currently NOT supported and will be broken down into two params at
46# the space character.
47HOST_EXTRA_PARAMS_ENV_VAR = "CHROME_REMOTE_DESKTOP_HOST_EXTRA_PARAMS"
48
49# This script has a sensible default for the initial and maximum desktop size,
50# which can be overridden either on the command-line, or via a comma-separated
51# list of sizes in this environment variable.
52DEFAULT_SIZES_ENV_VAR = "CHROME_REMOTE_DESKTOP_DEFAULT_DESKTOP_SIZES"
53
54# By default, this script launches Xvfb as the virtual X display. When this
55# environment variable is set, the script will instead launch an instance of
56# Xorg using the dummy display driver and void input device. In order for this
57# to work, both the dummy display driver and void input device need to be
58# installed:
59#
60# sudo apt-get install xserver-xorg-video-dummy
61# sudo apt-get install xserver-xorg-input-void
62#
63# TODO(rkjnsn): Add xserver-xorg-video-dummy and xserver-xorg-input-void as
64# package dependencies at the same time we switch the default to Xorg
65USE_XORG_ENV_VAR = "CHROME_REMOTE_DESKTOP_USE_XORG"
66
67# The amount of video RAM the dummy driver should claim to have, which limits
68# the maximum possible resolution.
69# 1048576 KiB = 1 GiB, which is the amount of video RAM needed to have a
70# 16384x16384 pixel frame buffer (the maximum size supported by VP8) with 32
71# bits per pixel.
72XORG_DUMMY_VIDEO_RAM = 1048576 # KiB
73
74# By default, provide a maximum size that is large enough to support clients
75# with large or multiple monitors. This is a comma-separated list of
76# resolutions that will be made available if the X server supports RANDR. These
77# defaults can be overridden in ~/.profile.
78DEFAULT_SIZES = "1600x1200,3840x2560"
79
80# Xorg's dummy driver only supports switching between preconfigured sizes. To
81# make resize-to-fit somewhat useful, include several common resolutions by
82# default.
83DEFAULT_SIZES_XORG = ("1600x1200,1600x900,1440x900,1366x768,1360x768,1280x1024,"
84 "1280x800,1280x768,1280x720,1152x864,1024x768,1024x600,"
85 "800x600,1680x1050,1920x1080,1920x1200,2560x1440,"
86 "2560x1600,3840x2160,3840x2560")
87
88SCRIPT_PATH = os.path.abspath(sys.argv[0])
89SCRIPT_DIR = os.path.dirname(SCRIPT_PATH)
90
91if (os.path.basename(sys.argv[0]) == 'linux_me2me_host.py'):
92 # Needed for swarming/isolate tests.
93 HOST_BINARY_PATH = os.path.join(SCRIPT_DIR,
94 "../../../out/Release/remoting_me2me_host")
95else:
96 HOST_BINARY_PATH = os.path.join(SCRIPT_DIR, "chrome-remote-desktop-host")
97
98USER_SESSION_PATH = os.path.join(SCRIPT_DIR, "user-session")
99
100CHROME_REMOTING_GROUP_NAME = "chrome-remote-desktop"
101
102HOME_DIR = os.environ["HOME"]
103CONFIG_DIR = os.path.join(HOME_DIR, ".config/chrome-remote-desktop")
104SESSION_FILE_PATH = os.path.join(HOME_DIR, ".chrome-remote-desktop-session")
105SYSTEM_SESSION_FILE_PATH = "/etc/chrome-remote-desktop-session"
106
107X_LOCK_FILE_TEMPLATE = "/tmp/.X%d-lock"
108FIRST_X_DISPLAY_NUMBER = 0
109
110# Amount of time to wait between relaunching processes.
111SHORT_BACKOFF_TIME = 5
112LONG_BACKOFF_TIME = 60
113
114# How long a process must run in order not to be counted against the restart
115# thresholds.
116MINIMUM_PROCESS_LIFETIME = 60
117
118# Thresholds for switching from fast- to slow-restart and for giving up
119# trying to restart entirely.
120SHORT_BACKOFF_THRESHOLD = 5
121MAX_LAUNCH_FAILURES = SHORT_BACKOFF_THRESHOLD + 10
122
123# Number of seconds to save session output to the log.
124SESSION_OUTPUT_TIME_LIMIT_SECONDS = 300
125
126# Host offline reason if the X server retry count is exceeded.
127HOST_OFFLINE_REASON_X_SERVER_RETRIES_EXCEEDED = "X_SERVER_RETRIES_EXCEEDED"
128
129# Host offline reason if the X session retry count is exceeded.
130HOST_OFFLINE_REASON_SESSION_RETRIES_EXCEEDED = "SESSION_RETRIES_EXCEEDED"
131
132# Host offline reason if the host retry count is exceeded. (Note: It may or may
133# not be possible to send this, depending on why the host is failing.)
134HOST_OFFLINE_REASON_HOST_RETRIES_EXCEEDED = "HOST_RETRIES_EXCEEDED"
135
136# This is the file descriptor used to pass messages to the user_session binary
137# during startup. It must be kept in sync with kMessageFd in
138# remoting_user_session.cc.
139USER_SESSION_MESSAGE_FD = 202
140
141# This is the exit code used to signal to wrapper that it should restart instead
142# of exiting. It must be kept in sync with kRelaunchExitCode in
143# remoting_user_session.cc.
144RELAUNCH_EXIT_CODE = 41
145
146# This exit code is returned when a needed binary such as user-session or sg
147# cannot be found.
148COMMAND_NOT_FOUND_EXIT_CODE = 127
149
150# This exit code is returned when a needed binary exists but cannot be executed.
151COMMAND_NOT_EXECUTABLE_EXIT_CODE = 126
152
153# Globals needed by the atexit cleanup() handler.
154g_desktop = None
155g_host_hash = hashlib.md5(socket.gethostname()).hexdigest()
156
157def gen_xorg_config(sizes):
158 return (
159 # This causes X to load the default GLX module, even if a proprietary one
160 # is installed in a different directory.
161 'Section "Files"\n'
162 ' ModulePath "/usr/lib/xorg/modules"\n'
163 'EndSection\n'
164 '\n'
165 # Suppress device probing, which happens by default.
166 'Section "ServerFlags"\n'
167 ' Option "AutoAddDevices" "false"\n'
168 ' Option "AutoEnableDevices" "false"\n'
169 ' Option "DontVTSwitch" "true"\n'
170 ' Option "PciForceNone" "true"\n'
171 'EndSection\n'
172 '\n'
173 'Section "InputDevice"\n'
174 # The host looks for this name to check whether it's running in a virtual
175 # session
176 ' Identifier "Chrome Remote Desktop Input"\n'
177 # While the xorg.conf man page specifies that both of these options are
178 # deprecated synonyms for `Option "Floating" "false"`, it turns out that
179 # if both aren't specified, the Xorg server will automatically attempt to
180 # add additional devices.
181 ' Option "CoreKeyboard" "true"\n'
182 ' Option "CorePointer" "true"\n'
183 ' Driver "void"\n'
184 'EndSection\n'
185 '\n'
186 'Section "Device"\n'
187 ' Identifier "Chrome Remote Desktop Videocard"\n'
188 ' Driver "dummy"\n'
189 ' VideoRam {video_ram}\n'
190 'EndSection\n'
191 '\n'
192 'Section "Monitor"\n'
193 ' Identifier "Chrome Remote Desktop Monitor"\n'
194 # The horizontal sync rate was calculated from the vertical refresh rate
195 # and the modline template:
196 # (33000 (vert total) * 0.1 Hz = 3.3 kHz)
197 ' HorizSync 3.3\n' # kHz
198 # The vertical refresh rate was chosen both to be low enough to have an
199 # acceptable dot clock at high resolutions, and then bumped down a little
200 # more so that in the unlikely event that a low refresh rate would break
201 # something, it would break obviously.
202 ' VertRefresh 0.1\n' # Hz
203 '{modelines}'
204 'EndSection\n'
205 '\n'
206 'Section "Screen"\n'
207 ' Identifier "Chrome Remote Desktop Screen"\n'
208 ' Device "Chrome Remote Desktop Videocard"\n'
209 ' Monitor "Chrome Remote Desktop Monitor"\n'
210 ' DefaultDepth 24\n'
211 ' SubSection "Display"\n'
212 ' Viewport 0 0\n'
213 ' Depth 24\n'
214 ' Modes {modes}\n'
215 ' EndSubSection\n'
216 'EndSection\n'
217 '\n'
218 'Section "ServerLayout"\n'
219 ' Identifier "Chrome Remote Desktop Layout"\n'
220 ' Screen "Chrome Remote Desktop Screen"\n'
221 ' InputDevice "Chrome Remote Desktop Input"\n'
222 'EndSection\n'.format(
223 # This Modeline template allows resolutions up to the dummy driver's
224 # max supported resolution of 32767x32767 without additional
225 # calculation while meeting the driver's dot clock requirements. Note
226 # that VP8 (and thus the amount of video RAM chosen) only support a
227 # maximum resolution of 16384x16384.
228 # 32767x32767 should be possible if we switch fully to VP9 and
229 # increase the video RAM to 4GiB.
230 # The dot clock was calculated to match the VirtRefresh chosen above.
231 # (33000 * 33000 * 0.1 Hz = 108.9 MHz)
232 # Changes this line require matching changes to HorizSync and
233 # VertRefresh.
234 modelines="".join(
235 ' Modeline "{0}x{1}" 108.9 {0} 32998 32999 33000 '
236 '{1} 32998 32999 33000\n'.format(w, h) for w, h in sizes),
237 modes=" ".join('"{0}x{1}"'.format(w, h) for w, h in sizes),
238 video_ram=XORG_DUMMY_VIDEO_RAM))
239
240
241def is_supported_platform():
242 # Always assume that the system is supported if the config directory or
243 # session file exist.
244 if (os.path.isdir(CONFIG_DIR) or os.path.isfile(SESSION_FILE_PATH) or
245 os.path.isfile(SYSTEM_SESSION_FILE_PATH)):
246 return True
247
248 # The host has been tested only on Ubuntu.
249 distribution = platform.linux_distribution()
250 return (distribution[0]).lower() == 'ubuntu'
251
252
253class Config:
254 def __init__(self, path):
255 self.path = path
256 self.data = {}
257 self.changed = False
258
259 def load(self):
260 """Loads the config from file.
261
262 Raises:
263 IOError: Error reading data
264 ValueError: Error parsing JSON
265 """
266 settings_file = open(self.path, 'r')
267 self.data = json.load(settings_file)
268 self.changed = False
269 settings_file.close()
270
271 def save(self):
272 """Saves the config to file.
273
274 Raises:
275 IOError: Error writing data
276 TypeError: Error serialising JSON
277 """
278 if not self.changed:
279 return
280 old_umask = os.umask(0o066)
281 try:
282 settings_file = open(self.path, 'w')
283 settings_file.write(json.dumps(self.data, indent=2))
284 settings_file.close()
285 self.changed = False
286 finally:
287 os.umask(old_umask)
288
289 def save_and_log_errors(self):
290 """Calls self.save(), trapping and logging any errors."""
291 try:
292 self.save()
293 except (IOError, TypeError) as e:
294 logging.error("Failed to save config: " + str(e))
295
296 def get(self, key):
297 return self.data.get(key)
298
299 def __getitem__(self, key):
300 return self.data[key]
301
302 def __setitem__(self, key, value):
303 self.data[key] = value
304 self.changed = True
305
306 def clear(self):
307 self.data = {}
308 self.changed = True
309
310
311class Authentication:
312 """Manage authentication tokens for Chromoting/xmpp"""
313
314 def __init__(self):
315 # Note: Initial values are never used.
316 self.login = None
317 self.oauth_refresh_token = None
318
319 def copy_from(self, config):
320 """Loads the config and returns false if the config is invalid."""
321 try:
322 self.login = config["xmpp_login"]
323 self.oauth_refresh_token = config["oauth_refresh_token"]
324 except KeyError:
325 return False
326 return True
327
328 def copy_to(self, config):
329 config["xmpp_login"] = self.login
330 config["oauth_refresh_token"] = self.oauth_refresh_token
331
332
333class Host:
334 """This manages the configuration for a host."""
335
336 def __init__(self):
337 # Note: Initial values are never used.
338 self.host_id = None
339 self.gcd_device_id = None
340 self.host_name = None
341 self.host_secret_hash = None
342 self.private_key = None
343
344 def copy_from(self, config):
345 try:
346 self.host_id = config.get("host_id")
347 self.gcd_device_id = config.get("gcd_device_id")
348 self.host_name = config["host_name"]
349 self.host_secret_hash = config.get("host_secret_hash")
350 self.private_key = config["private_key"]
351 except KeyError:
352 return False
353 return bool(self.host_id or self.gcd_device_id)
354
355 def copy_to(self, config):
356 if self.host_id:
357 config["host_id"] = self.host_id
358 if self.gcd_device_id:
359 config["gcd_device_id"] = self.gcd_device_id
360 config["host_name"] = self.host_name
361 config["host_secret_hash"] = self.host_secret_hash
362 config["private_key"] = self.private_key
363
364
365class SessionOutputFilterThread(threading.Thread):
366 """Reads session log from a pipe and logs the output for amount of time
367 defined by SESSION_OUTPUT_TIME_LIMIT_SECONDS."""
368
369 def __init__(self, stream):
370 threading.Thread.__init__(self)
371 self.stream = stream
372 self.daemon = True
373
374 def run(self):
375 started_time = time.time()
376 is_logging = True
377 while True:
378 try:
379 line = self.stream.readline();
380 except IOError as e:
381 print("IOError when reading session output: ", e)
382 return
383
384 if line == "":
385 # EOF reached. Just stop the thread.
386 return
387
388 if not is_logging:
389 continue
390
391 if time.time() - started_time >= SESSION_OUTPUT_TIME_LIMIT_SECONDS:
392 is_logging = False
393 print("Suppressing rest of the session output.")
394 sys.stdout.flush()
395 else:
396 print("Session output: %s" % line.strip("\n"))
397 sys.stdout.flush()
398
399
400class Desktop:
401 """Manage a single virtual desktop"""
402
403 def __init__(self, sizes):
404 self.x_proc = None
405 self.session_proc = None
406 self.host_proc = None
407 self.child_env = None
408 self.sizes = sizes
409 self.xorg_conf = None
410 self.pulseaudio_pipe = None
411 self.server_supports_exact_resize = False
412 self.server_supports_randr = False
413 self.randr_add_sizes = False
414 self.host_ready = False
415 self.ssh_auth_sockname = None
416 global g_desktop
417 assert(g_desktop is None)
418 g_desktop = self
419
420 @staticmethod
421 def get_unused_display_number():
422 """Return a candidate display number for which there is currently no
423 X Server lock file"""
424 display = FIRST_X_DISPLAY_NUMBER
425# while os.path.exists(X_LOCK_FILE_TEMPLATE % display):
426# display += 1
427 return display
428
429 def _init_child_env(self):
430 self.child_env = dict(os.environ)
431
432 # Ensure that the software-rendering GL drivers are loaded by the desktop
433 # session, instead of any hardware GL drivers installed on the system.
434 library_path = (
435 "/usr/lib/mesa-diverted/%(arch)s-linux-gnu:"
436 "/usr/lib/%(arch)s-linux-gnu/mesa:"
437 "/usr/lib/%(arch)s-linux-gnu/dri:"
438 "/usr/lib/%(arch)s-linux-gnu/gallium-pipe" %
439 { "arch": platform.machine() })
440
441 if "LD_LIBRARY_PATH" in self.child_env:
442 library_path += ":" + self.child_env["LD_LIBRARY_PATH"]
443
444 self.child_env["LD_LIBRARY_PATH"] = library_path
445
446 def _setup_pulseaudio(self):
447 self.pulseaudio_pipe = None
448
449 # pulseaudio uses UNIX sockets for communication. Length of UNIX socket
450 # name is limited to 108 characters, so audio will not work properly if
451 # the path is too long. To workaround this problem we use only first 10
452 # symbols of the host hash.
453 pulse_path = os.path.join(CONFIG_DIR,
454 "pulseaudio#%s" % g_host_hash[0:10])
455 if len(pulse_path) + len("/native") >= 108:
456 logging.error("Audio will not be enabled because pulseaudio UNIX " +
457 "socket path is too long.")
458 return False
459
460 sink_name = "chrome_remote_desktop_session"
461 pipe_name = os.path.join(pulse_path, "fifo_output")
462
463 try:
464 if not os.path.exists(pulse_path):
465 os.mkdir(pulse_path)
466 except IOError as e:
467 logging.error("Failed to create pulseaudio pipe: " + str(e))
468 return False
469
470 try:
471 pulse_config = open(os.path.join(pulse_path, "daemon.conf"), "w")
472 pulse_config.write("default-sample-format = s16le\n")
473 pulse_config.write("default-sample-rate = 48000\n")
474 pulse_config.write("default-sample-channels = 2\n")
475 pulse_config.close()
476
477 pulse_script = open(os.path.join(pulse_path, "default.pa"), "w")
478 pulse_script.write("load-module module-native-protocol-unix\n")
479 pulse_script.write(
480 ("load-module module-pipe-sink sink_name=%s file=\"%s\" " +
481 "rate=48000 channels=2 format=s16le\n") %
482 (sink_name, pipe_name))
483 pulse_script.close()
484 except IOError as e:
485 logging.error("Failed to write pulseaudio config: " + str(e))
486 return False
487
488 self.child_env["PULSE_CONFIG_PATH"] = pulse_path
489 self.child_env["PULSE_RUNTIME_PATH"] = pulse_path
490 self.child_env["PULSE_STATE_PATH"] = pulse_path
491 self.child_env["PULSE_SINK"] = sink_name
492 self.pulseaudio_pipe = pipe_name
493
494 return True
495
496 def _setup_gnubby(self):
497 self.ssh_auth_sockname = ("/tmp/chromoting.%s.ssh_auth_sock" %
498 os.environ["USER"])
499
500 # Returns child environment not containing TMPDIR.
501 # Certain values of TMPDIR can break the X server (crbug.com/672684), so we
502 # want to make sure it isn't set in the envirionment we use to start the
503 # server.
504 def _x_env(self):
505 if "TMPDIR" not in self.child_env:
506 return self.child_env
507 else:
508 env_copy = dict(self.child_env)
509 del env_copy["TMPDIR"]
510 return env_copy
511
512 def check_x_responding(self):
513 """Checks if the X server is responding to connections."""
514 with open(os.devnull, "r+") as devnull:
515 exit_code = subprocess.call("xdpyinfo", env=self.child_env,
516 stdout=devnull)
517 return exit_code == 0
518
519 def _wait_for_x(self):
520 # Wait for X to be active.
521 for _test in range(20):
522 if self.check_x_responding():
523 logging.info("X server is active.")
524 return
525 time.sleep(0.5)
526
527 raise Exception("Could not connect to X server.")
528
529 def _launch_xvfb(self, display, x_auth_file, extra_x_args):
530 max_width = max([width for width, height in self.sizes])
531 max_height = max([height for width, height in self.sizes])
532
533 logging.info("Starting Xvfb on display :%d" % display)
534 screen_option = "%dx%dx24" % (max_width, max_height)
535 self.x_proc = subprocess.Popen(
536 ["Xvfb", ":%d" % display,
537 "-auth", x_auth_file,
538 "-nolisten", "tcp",
539 "-noreset",
540 "-screen", "0", screen_option
541 ] + extra_x_args, env=self._x_env())
542 if not self.x_proc.pid:
543 raise Exception("Could not start Xvfb.")
544
545 self._wait_for_x()
546
547 with open(os.devnull, "r+") as devnull:
548 exit_code = subprocess.call("xrandr", env=self.child_env,
549 stdout=devnull, stderr=devnull)
550 if exit_code == 0:
551 # RandR is supported
552 self.server_supports_exact_resize = True
553 self.server_supports_randr = True
554 self.randr_add_sizes = True
555
556 def _launch_xorg(self, display, x_auth_file, extra_x_args):
557 with tempfile.NamedTemporaryFile(
558 prefix="chrome_remote_desktop_",
559 suffix=".conf", delete=False) as config_file:
560 config_file.write(gen_xorg_config(self.sizes))
561
562 # We can't support exact resize with the current Xorg dummy driver.
563 self.server_supports_exact_resize = False
564 # But dummy does support RandR 1.0.
565 self.server_supports_randr = True
566 self.xorg_conf = config_file.name
567
568 logging.info("Starting Xorg on display :%d" % display)
569 # We use the child environment so the Xorg server picks up the Mesa libGL
570 # instead of any proprietary versions that may be installed, thanks to
571 # LD_LIBRARY_PATH.
572 # Note: This prevents any environment variable the user has set from
573 # affecting the Xorg server.
574 self.x_proc = subprocess.Popen(
575 ["Xorg", ":%d" % display,
576 "-auth", x_auth_file,
577 "-nolisten", "tcp",
578 "-noreset",
579 # Disable logging to a file and instead bump up the stderr verbosity
580 # so the equivalent information gets logged in our main log file.
581 "-logfile", "/dev/null",
582 "-verbose", "3",
583 "-config", config_file.name
584 ] + extra_x_args, env=self._x_env())
585 if not self.x_proc.pid:
586 raise Exception("Could not start Xorg.")
587 self._wait_for_x()
588
589 def _launch_x_server(self, extra_x_args):
590 x_auth_file = os.path.expanduser("~/.Xauthority")
591 self.child_env["XAUTHORITY"] = x_auth_file
592 display = self.get_unused_display_number()
593
594 # Run "xauth add" with |child_env| so that it modifies the same XAUTHORITY
595 # file which will be used for the X session.
596 exit_code = subprocess.call("xauth add :%d . `mcookie`" % display,
597 env=self.child_env, shell=True)
598 if exit_code != 0:
599 raise Exception("xauth failed with code %d" % exit_code)
600
601 # Disable the Composite extension iff the X session is the default
602 # Unity-2D, since it uses Metacity which fails to generate DAMAGE
603 # notifications correctly. See crbug.com/166468.
604 x_session = choose_x_session()
605 if (len(x_session) == 2 and
606 x_session[1] == "/usr/bin/gnome-session --session=ubuntu-2d"):
607 extra_x_args.extend(["-extension", "Composite"])
608
609 self.child_env["DISPLAY"] = ":%d" % display
610 self.child_env["CHROME_REMOTE_DESKTOP_SESSION"] = "1"
611
612 # Use a separate profile for any instances of Chrome that are started in
613 # the virtual session. Chrome doesn't support sharing a profile between
614 # multiple DISPLAYs, but Chrome Sync allows for a reasonable compromise.
615 chrome_profile = os.path.join(CONFIG_DIR, "chrome-profile")
616 self.child_env["CHROME_USER_DATA_DIR"] = chrome_profile
617
618 # Set SSH_AUTH_SOCK to the file name to listen on.
619 if self.ssh_auth_sockname:
620 self.child_env["SSH_AUTH_SOCK"] = self.ssh_auth_sockname
621
622 if USE_XORG_ENV_VAR in os.environ:
623 self._launch_xorg(display, x_auth_file, extra_x_args)
624 else:
625 self._launch_xvfb(display, x_auth_file, extra_x_args)
626
627 # The remoting host expects the server to use "evdev" keycodes, but Xvfb
628 # starts configured to use the "base" ruleset, resulting in XKB configuring
629 # for "xfree86" keycodes, and screwing up some keys. See crbug.com/119013.
630 # Reconfigure the X server to use "evdev" keymap rules. The X server must
631 # be started with -noreset otherwise it'll reset as soon as the command
632 # completes, since there are no other X clients running yet.
633 exit_code = subprocess.call(["setxkbmap", "-rules", "evdev"],
634 env=self.child_env)
635 if exit_code != 0:
636 logging.error("Failed to set XKB to 'evdev'")
637
638 if not self.server_supports_randr:
639 return
640
641 with open(os.devnull, "r+") as devnull:
642 # Register the screen sizes with RandR, if needed. Errors here are
643 # non-fatal; the X server will continue to run with the dimensions from
644 # the "-screen" option.
645 if self.randr_add_sizes:
646 for width, height in self.sizes:
647 label = "%dx%d" % (width, height)
648 args = ["xrandr", "--newmode", label, "0", str(width), "0", "0", "0",
649 str(height), "0", "0", "0"]
650 subprocess.call(args, env=self.child_env, stdout=devnull,
651 stderr=devnull)
652 args = ["xrandr", "--addmode", "screen", label]
653 subprocess.call(args, env=self.child_env, stdout=devnull,
654 stderr=devnull)
655
656 # Set the initial mode to the first size specified, otherwise the X server
657 # would default to (max_width, max_height), which might not even be in the
658 # list.
659 initial_size = self.sizes[0]
660 label = "%dx%d" % initial_size
661 args = ["xrandr", "-s", label]
662 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
663
664 # Set the physical size of the display so that the initial mode is running
665 # at approximately 96 DPI, since some desktops require the DPI to be set
666 # to something realistic.
667 args = ["xrandr", "--dpi", "96"]
668 subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
669
670 # Monitor for any automatic resolution changes from the desktop
671 # environment.
672 args = [SCRIPT_PATH, "--watch-resolution", str(initial_size[0]),
673 str(initial_size[1])]
674
675 # It is not necessary to wait() on the process here, as this script's main
676 # loop will reap the exit-codes of all child processes.
677 subprocess.Popen(args, env=self.child_env, stdout=devnull, stderr=devnull)
678
679 def _launch_x_session(self):
680 # Start desktop session.
681 # The /dev/null input redirection is necessary to prevent the X session
682 # reading from stdin. If this code runs as a shell background job in a
683 # terminal, any reading from stdin causes the job to be suspended.
684 # Daemonization would solve this problem by separating the process from the
685 # controlling terminal.
686 xsession_command = choose_x_session()
687 if xsession_command is None:
688 raise Exception("Unable to choose suitable X session command.")
689
690 logging.info("Launching X session: %s" % xsession_command)
691 self.session_proc = subprocess.Popen(xsession_command,
692 stdin=open(os.devnull, "r"),
693 stdout=subprocess.PIPE,
694 stderr=subprocess.STDOUT,
695 cwd=HOME_DIR,
696 env=self.child_env)
697
698 output_filter_thread = SessionOutputFilterThread(self.session_proc.stdout)
699 output_filter_thread.start()
700
701 if not self.session_proc.pid:
702 raise Exception("Could not start X session")
703
704 def launch_session(self, x_args):
705 self._init_child_env()
706 self._setup_pulseaudio()
707 self._setup_gnubby()
708 #self._launch_x_server(x_args)
709 #self._launch_x_session()
710 display = self.get_unused_display_number()
711 self.child_env["DISPLAY"] = ":%d" % display
712
713
714
715 def launch_host(self, host_config, extra_start_host_args):
716 # Start remoting host
717 args = [HOST_BINARY_PATH, "--host-config=-"]
718 if self.pulseaudio_pipe:
719 args.append("--audio-pipe-name=%s" % self.pulseaudio_pipe)
720 if self.server_supports_exact_resize:
721 args.append("--server-supports-exact-resize")
722 if self.ssh_auth_sockname:
723 args.append("--ssh-auth-sockname=%s" % self.ssh_auth_sockname)
724
725 args.extend(extra_start_host_args)
726
727 # Have the host process use SIGUSR1 to signal a successful start.
728 def sigusr1_handler(signum, frame):
729 _ = signum, frame
730 logging.info("Host ready to receive connections.")
731 self.host_ready = True
732 ParentProcessLogger.release_parent_if_connected(True)
733
734 signal.signal(signal.SIGUSR1, sigusr1_handler)
735 args.append("--signal-parent")
736
737 logging.info(args)
738 self.host_proc = subprocess.Popen(args, env=self.child_env,
739 stdin=subprocess.PIPE)
740 if not self.host_proc.pid:
741 raise Exception("Could not start Chrome Remote Desktop host")
742
743 try:
744 self.host_proc.stdin.write(json.dumps(host_config.data).encode('UTF-8'))
745 self.host_proc.stdin.flush()
746 except IOError as e:
747 # This can occur in rare situations, for example, if the machine is
748 # heavily loaded and the host process dies quickly (maybe if the X
749 # connection failed), the host process might be gone before this code
750 # writes to the host's stdin. Catch and log the exception, allowing
751 # the process to be retried instead of exiting the script completely.
752 logging.error("Failed writing to host's stdin: " + str(e))
753 finally:
754 self.host_proc.stdin.close()
755
756 def shutdown_all_procs(self):
757 """Send SIGTERM to all procs and wait for them to exit. Will fallback to
758 SIGKILL if a process doesn't exit within 10 seconds.
759 """
760 for proc, name in [(self.x_proc, "X server"),
761 (self.session_proc, "session"),
762 (self.host_proc, "host")]:
763 if proc is not None:
764 logging.info("Terminating " + name)
765 try:
766 psutil_proc = psutil.Process(proc.pid)
767 psutil_proc.terminate()
768
769 # Use a short timeout, to avoid delaying service shutdown if the
770 # process refuses to die for some reason.
771 psutil_proc.wait(timeout=10)
772 except psutil.TimeoutExpired:
773 logging.error("Timed out - sending SIGKILL")
774 psutil_proc.kill()
775 except psutil.Error:
776 logging.error("Error terminating process")
777 self.x_proc = None
778 self.session_proc = None
779 self.host_proc = None
780
781 def report_offline_reason(self, host_config, reason):
782 """Attempt to report the specified offline reason to the registry. This
783 is best effort, and requires a valid host config.
784 """
785 logging.info("Attempting to report offline reason: " + reason)
786 args = [HOST_BINARY_PATH, "--host-config=-",
787 "--report-offline-reason=" + reason]
788 proc = subprocess.Popen(args, env=self.child_env, stdin=subprocess.PIPE)
789 proc.communicate(json.dumps(host_config.data).encode('UTF-8'))
790
791
792def parse_config_arg(args):
793 """Parses only the --config option from a given command-line.
794
795 Returns:
796 A two-tuple. The first element is the value of the --config option (or None
797 if it is not specified), and the second is a list containing the remaining
798 arguments
799 """
800
801 # By default, argparse will exit the program on error. We would like it not to
802 # do that.
803 class ArgumentParserError(Exception):
804 pass
805 class ThrowingArgumentParser(argparse.ArgumentParser):
806 def error(self, message):
807 raise ArgumentParserError(message)
808
809 parser = ThrowingArgumentParser()
810 parser.add_argument("--config", nargs='?', action="store")
811
812 try:
813 result = parser.parse_known_args(args)
814 return (result[0].config, result[1])
815 except ArgumentParserError:
816 return (None, list(args))
817
818
819def get_daemon_proc(config_file, require_child_process=False):
820 """Checks if there is already an instance of this script running against
821 |config_file|, and returns a psutil.Process instance for it. If
822 |require_child_process| is true, only check for an instance with the
823 --child-process flag specified.
824
825 If a process is found without --config in the command line, get_daemon_proc
826 will fall back to the old behavior of checking whether the script path matches
827 the current script. This is to facilitate upgrades from previous versions.
828
829 Returns:
830 A Process instance for the existing daemon process, or None if the daemon
831 is not running.
832 """
833
834 # Note: When making changes to how instances are detected, it is imperative
835 # that this function retains the ability to find older versions. Otherwise,
836 # upgrades can leave the user with two running sessions, with confusing
837 # results.
838
839 uid = os.getuid()
840 this_pid = os.getpid()
841
842 # This function should return the process with the --child-process flag if it
843 # exists. If there's only a process without, it might be a legacy process.
844 non_child_process = None
845
846 # Support new & old psutil API. This is the right way to check, according to
847 # http://grodola.blogspot.com/2014/01/psutil-20-porting.html
848 if psutil.version_info >= (2, 0):
849 psget = lambda x: x()
850 else:
851 psget = lambda x: x
852
853 for process in psutil.process_iter():
854 # Skip any processes that raise an exception, as processes may terminate
855 # during iteration over the list.
856 try:
857 # Skip other users' processes.
858 if psget(process.uids).real != uid:
859 continue
860
861 # Skip the process for this instance.
862 if process.pid == this_pid:
863 continue
864
865 # |cmdline| will be [python-interpreter, script-file, other arguments...]
866 cmdline = psget(process.cmdline)
867 if len(cmdline) < 2:
868 continue
869 if (os.path.basename(cmdline[0]).startswith('python') and
870 os.path.basename(cmdline[1]) == os.path.basename(sys.argv[0]) and
871 "--start" in cmdline):
872 process_config = parse_config_arg(cmdline[2:])[0]
873
874 # Fall back to old behavior if there is no --config argument
875 # TODO(rkjnsn): Consider removing this fallback once sufficient time
876 # has passed.
877 if process_config == config_file or (process_config is None and
878 cmdline[1] == sys.argv[0]):
879 if "--child-process" in cmdline:
880 return process
881 else:
882 non_child_process = process
883
884 except (psutil.NoSuchProcess, psutil.AccessDenied):
885 continue
886
887 return non_child_process if not require_child_process else None
888
889
890def choose_x_session():
891 """Chooses the most appropriate X session command for this system.
892
893 Returns:
894 A string containing the command to run, or a list of strings containing
895 the executable program and its arguments, which is suitable for passing as
896 the first parameter of subprocess.Popen(). If a suitable session cannot
897 be found, returns None.
898 """
899 XSESSION_FILES = [
900 SESSION_FILE_PATH,
901 SYSTEM_SESSION_FILE_PATH ]
902 for startup_file in XSESSION_FILES:
903 startup_file = os.path.expanduser(startup_file)
904 if os.path.exists(startup_file):
905 if os.access(startup_file, os.X_OK):
906 # "/bin/sh -c" is smart about how to execute the session script and
907 # works in cases where plain exec() fails (for example, if the file is
908 # marked executable, but is a plain script with no shebang line).
909 return ["/bin/sh", "-c", pipes.quote(startup_file)]
910 else:
911 # If this is a system-wide session script, it should be run using the
912 # system shell, ignoring any login shell that might be set for the
913 # current user.
914 return ["/bin/sh", startup_file]
915
916 # Choose a session wrapper script to run the session. On some systems,
917 # /etc/X11/Xsession fails to load the user's .profile, so look for an
918 # alternative wrapper that is more likely to match the script that the
919 # system actually uses for console desktop sessions.
920 SESSION_WRAPPERS = [
921 "/usr/sbin/lightdm-session",
922 "/etc/gdm/Xsession",
923 "/etc/X11/Xsession" ]
924 for session_wrapper in SESSION_WRAPPERS:
925 if os.path.exists(session_wrapper):
926 if os.path.exists("/usr/bin/unity-2d-panel"):
927 # On Ubuntu 12.04, the default session relies on 3D-accelerated
928 # hardware. Trying to run this with a virtual X display produces
929 # weird results on some systems (for example, upside-down and
930 # corrupt displays). So if the ubuntu-2d session is available,
931 # choose it explicitly.
932 return [session_wrapper, "/usr/bin/gnome-session --session=ubuntu-2d"]
933 else:
934 # Use the session wrapper by itself, and let the system choose a
935 # session.
936 return [session_wrapper]
937 return None
938
939
940class ParentProcessLogger(object):
941 """Redirects logs to the parent process, until the host is ready or quits.
942
943 This class creates a pipe to allow logging from the daemon process to be
944 copied to the parent process. The daemon process adds a log-handler that
945 directs logging output to the pipe. The parent process reads from this pipe
946 and writes the content to stderr. When the pipe is no longer needed (for
947 example, the host signals successful launch or permanent failure), the daemon
948 removes the log-handler and closes the pipe, causing the the parent process
949 to reach end-of-file while reading the pipe and exit.
950
951 The file descriptor for the pipe to the parent process should be passed to
952 the constructor. The (grand-)child process should call start_logging() when
953 it starts, and then use logging.* to issue log statements, as usual. When the
954 child has either succesfully started the host or terminated, it must call
955 release_parent() to allow the parent to exit.
956 """
957
958 __instance = None
959
960 def __init__(self, write_fd):
961 """Constructor.
962
963 Constructs the singleton instance of ParentProcessLogger. This should be
964 called at most once.
965
966 write_fd: The write end of the pipe created by the parent process. If
967 write_fd is not a valid file descriptor, the constructor will
968 throw either IOError or OSError.
969 """
970 # Ensure write_pipe is closed on exec, otherwise it will be kept open by
971 # child processes (X, host), preventing the read pipe from EOF'ing.
972 old_flags = fcntl.fcntl(write_fd, fcntl.F_GETFD)
973 fcntl.fcntl(write_fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
974 self._write_file = os.fdopen(write_fd, 'w')
975 self._logging_handler = None
976 ParentProcessLogger.__instance = self
977
978 def _start_logging(self):
979 """Installs a logging handler that sends log entries to a pipe, prefixed
980 with the string 'MSG:'. This allows them to be distinguished by the parent
981 process from commands sent over the same pipe.
982
983 Must be called by the child process.
984 """
985 self._logging_handler = logging.StreamHandler(self._write_file)
986 self._logging_handler.setFormatter(logging.Formatter(fmt='MSG:%(message)s'))
987 logging.getLogger().addHandler(self._logging_handler)
988
989 def _release_parent(self, success):
990 """Uninstalls logging handler and closes the pipe, releasing the parent.
991
992 Must be called by the child process.
993
994 success: If true, write a "host ready" message to the parent process before
995 closing the pipe.
996 """
997 if self._logging_handler:
998 logging.getLogger().removeHandler(self._logging_handler)
999 self._logging_handler = None
1000 if not self._write_file.closed:
1001 if success:
1002 try:
1003 self._write_file.write("READY\n")
1004 self._write_file.flush()
1005 except IOError:
1006 # A "broken pipe" IOError can happen if the receiving process
1007 # (remoting_user_session) has exited (probably due to timeout waiting
1008 # for the host to start).
1009 # Trapping the error here means the host can continue running.
1010 logging.info("Caught IOError writing READY message.")
1011 self._write_file.close()
1012
1013 @staticmethod
1014 def try_start_logging(write_fd):
1015 """Attempt to initialize ParentProcessLogger and start forwarding log
1016 messages.
1017
1018 Returns False if the file descriptor was invalid (safe to ignore).
1019 """
1020 try:
1021 ParentProcessLogger(USER_SESSION_MESSAGE_FD)._start_logging()
1022 return True
1023 except (IOError, OSError):
1024 # One of these will be thrown if the file descriptor is invalid, such as
1025 # if the the fd got closed by the login shell. In that case, just continue
1026 # without sending log messages.
1027 return False
1028
1029 @staticmethod
1030 def release_parent_if_connected(success):
1031 """If ParentProcessLogger is active, stop logging and release the parent.
1032
1033 success: If true, signal to the parent that the script was successful.
1034 """
1035 instance = ParentProcessLogger.__instance
1036 if instance is not None:
1037 instance._release_parent(success)
1038
1039
1040def run_command_with_group(command, group):
1041 """Run a command with a different primary group."""
1042
1043 # This is implemented using sg, which is an odd character and will try to
1044 # prompt for a password if it can't verify the user is a member of the given
1045 # group, along with in a few other corner cases. (It will prompt in the
1046 # non-member case even if the group doesn't have a password set.)
1047 #
1048 # To prevent sg from prompting the user for a password that doesn't exist,
1049 # redirect stdin and detach sg from the TTY. It will still print something
1050 # like "Password: crypt: Invalid argument", so redirect stdout and stderr, as
1051 # well. Finally, have the shell unredirect them when executing user-session.
1052 #
1053 # It is also desirable to have some way to tell whether any errors are
1054 # from sg or the command, which is done using a pipe.
1055
1056 def pre_exec(read_fd, write_fd):
1057 os.close(read_fd)
1058
1059 # /bin/sh may be dash, which only allows redirecting file descriptors 0-9,
1060 # the minimum required by POSIX. Since there may be files open elsewhere,
1061 # move the relevant file descriptors to specific numbers under that limit.
1062 # Because this runs in the child process, it doesn't matter if existing file
1063 # descriptors are closed in the process. After, stdio will be redirected to
1064 # /dev/null, write_fd will be moved to 6, and the old stdio will be moved
1065 # to 7, 8, and 9.
1066 if (write_fd != 6):
1067 os.dup2(write_fd, 6)
1068 os.close(write_fd)
1069 os.dup2(0, 7)
1070 os.dup2(1, 8)
1071 os.dup2(2, 9)
1072 devnull = os.open(os.devnull, os.O_RDWR)
1073 os.dup2(devnull, 0)
1074 os.dup2(devnull, 1)
1075 os.dup2(devnull, 2)
1076 os.close(devnull)
1077
1078 # os.setsid will detach subprocess from the TTY
1079 os.setsid()
1080
1081 # Pipe to check whether sg successfully ran our command.
1082 read_fd, write_fd = os.pipe()
1083 try:
1084 # sg invokes the provided argument using /bin/sh. In that shell, first write
1085 # "success\n" to the pipe, which is checked later to determine whether sg
1086 # itself succeeded, and then restore stdio, close the extra file
1087 # descriptors, and exec the provided command.
1088 process = subprocess.Popen(
1089 ["sg", group,
1090 "echo success >&6; exec {command} "
1091 # Restore original stdio
1092 "0<&7 1>&8 2>&9 "
1093 # Close no-longer-needed file descriptors
1094 "6>&- 7<&- 8>&- 9>&-"
1095 .format(command=" ".join(map(pipes.quote, command)))],
1096 preexec_fn=lambda: pre_exec(read_fd, write_fd))
1097 result = process.wait()
1098 except OSError as e:
1099 logging.error("Failed to execute sg: {}".format(e.strerror))
1100 if e.errno == errno.ENOENT:
1101 result = COMMAND_NOT_FOUND_EXIT_CODE
1102 else:
1103 result = COMMAND_NOT_EXECUTABLE_EXIT_CODE
1104 # Skip pipe check, since sg was never executed.
1105 os.close(read_fd)
1106 return result
1107 except KeyboardInterrupt:
1108 # Because sg is in its own session, it won't have gotten the interrupt.
1109 try:
1110 os.killpg(os.getpgid(process.pid), signal.SIGINT)
1111 result = process.wait()
1112 except OSError:
1113 logging.warning("Command may still be running")
1114 result = 1
1115 finally:
1116 os.close(write_fd)
1117
1118 with os.fdopen(read_fd) as read_file:
1119 contents = read_file.read()
1120 if contents != "success\n":
1121 # No success message means sg didn't execute the command. (Maybe the user
1122 # is not a member of the group?)
1123 logging.error("Failed to access {} group. Is the user a member?"
1124 .format(group))
1125 result = COMMAND_NOT_EXECUTABLE_EXIT_CODE
1126
1127 return result
1128
1129
1130def start_via_user_session(foreground):
1131 # We need to invoke user-session
1132 command = [USER_SESSION_PATH, "start"]
1133 if foreground:
1134 command += ["--foreground"]
1135 command += ["--"] + sys.argv[1:]
1136 try:
1137 process = subprocess.Popen(command)
1138 result = process.wait()
1139 except OSError as e:
1140 if e.errno == errno.EACCES:
1141 # User may have just been added to the CRD group, in which case they
1142 # won't be able to execute user-session directly until they log out and
1143 # back in. In the mean time, we can try to switch to the CRD group and
1144 # execute user-session.
1145 result = run_command_with_group(command, CHROME_REMOTING_GROUP_NAME)
1146 else:
1147 logging.error("Could not execute {}: {}"
1148 .format(USER_SESSION_PATH, e.strerror))
1149 if e.errno == errno.ENOENT:
1150 result = COMMAND_NOT_FOUND_EXIT_CODE
1151 else:
1152 result = COMMAND_NOT_EXECUTABLE_EXIT_CODE
1153 except KeyboardInterrupt:
1154 # Child will have also gotten the interrupt. Wait for it to exit.
1155 result = process.wait()
1156
1157 return result
1158
1159
1160def cleanup():
1161 logging.info("Cleanup.")
1162
1163 global g_desktop
1164 if g_desktop is not None:
1165 g_desktop.shutdown_all_procs()
1166 if g_desktop.xorg_conf is not None:
1167 os.remove(g_desktop.xorg_conf)
1168
1169 g_desktop = None
1170 ParentProcessLogger.release_parent_if_connected(False)
1171
1172class SignalHandler:
1173 """Reload the config file on SIGHUP. Since we pass the configuration to the
1174 host processes via stdin, they can't reload it, so terminate them. They will
1175 be relaunched automatically with the new config."""
1176
1177 def __init__(self, host_config):
1178 self.host_config = host_config
1179
1180 def __call__(self, signum, _stackframe):
1181 if signum == signal.SIGHUP:
1182 logging.info("SIGHUP caught, restarting host.")
1183 try:
1184 self.host_config.load()
1185 except (IOError, ValueError) as e:
1186 logging.error("Failed to load config: " + str(e))
1187 if g_desktop is not None and g_desktop.host_proc:
1188 g_desktop.host_proc.send_signal(signal.SIGTERM)
1189 else:
1190 # Exit cleanly so the atexit handler, cleanup(), gets called.
1191 raise SystemExit
1192
1193
1194class RelaunchInhibitor:
1195 """Helper class for inhibiting launch of a child process before a timeout has
1196 elapsed.
1197
1198 A managed process can be in one of these states:
1199 running, not inhibited (running == True)
1200 stopped and inhibited (running == False and is_inhibited() == True)
1201 stopped but not inhibited (running == False and is_inhibited() == False)
1202
1203 Attributes:
1204 label: Name of the tracked process. Only used for logging.
1205 running: Whether the process is currently running.
1206 earliest_relaunch_time: Time before which the process should not be
1207 relaunched, or 0 if there is no limit.
1208 failures: The number of times that the process ran for less than a
1209 specified timeout, and had to be inhibited. This count is reset to 0
1210 whenever the process has run for longer than the timeout.
1211 """
1212
1213 def __init__(self, label):
1214 self.label = label
1215 self.running = False
1216 self.earliest_relaunch_time = 0
1217 self.earliest_successful_termination = 0
1218 self.failures = 0
1219
1220 def is_inhibited(self):
1221 return (not self.running) and (time.time() < self.earliest_relaunch_time)
1222
1223 def record_started(self, minimum_lifetime, relaunch_delay):
1224 """Record that the process was launched, and set the inhibit time to
1225 |timeout| seconds in the future."""
1226 self.earliest_relaunch_time = time.time() + relaunch_delay
1227 self.earliest_successful_termination = time.time() + minimum_lifetime
1228 self.running = True
1229
1230 def record_stopped(self, expected):
1231 """Record that the process was stopped, and adjust the failure count
1232 depending on whether the process ran long enough. If the process was
1233 intentionally stopped (expected is True), the failure count will not be
1234 incremented."""
1235 self.running = False
1236 if time.time() >= self.earliest_successful_termination:
1237 self.failures = 0
1238 elif not expected:
1239 self.failures += 1
1240 logging.info("Failure count for '%s' is now %d", self.label, self.failures)
1241
1242
1243def relaunch_self():
1244 """Relaunches the session to pick up any changes to the session logic in case
1245 Chrome Remote Desktop has been upgraded. We return a special exit code to
1246 inform user-session that it should relaunch.
1247 """
1248
1249 # cleanup run via atexit
1250 sys.exit(RELAUNCH_EXIT_CODE)
1251
1252
1253def waitpid_with_timeout(pid, deadline):
1254 """Wrapper around os.waitpid() which waits until either a child process dies
1255 or the deadline elapses.
1256
1257 Args:
1258 pid: Process ID to wait for, or -1 to wait for any child process.
1259 deadline: Waiting stops when time.time() exceeds this value.
1260
1261 Returns:
1262 (pid, status): Same as for os.waitpid(), except that |pid| is 0 if no child
1263 changed state within the timeout.
1264
1265 Raises:
1266 Same as for os.waitpid().
1267 """
1268 while time.time() < deadline:
1269 pid, status = os.waitpid(pid, os.WNOHANG)
1270 if pid != 0:
1271 return (pid, status)
1272 time.sleep(1)
1273 return (0, 0)
1274
1275
1276def waitpid_handle_exceptions(pid, deadline):
1277 """Wrapper around os.waitpid()/waitpid_with_timeout(), which waits until
1278 either a child process exits or the deadline elapses, and retries if certain
1279 exceptions occur.
1280
1281 Args:
1282 pid: Process ID to wait for, or -1 to wait for any child process.
1283 deadline: If non-zero, waiting stops when time.time() exceeds this value.
1284 If zero, waiting stops when a child process exits.
1285
1286 Returns:
1287 (pid, status): Same as for waitpid_with_timeout(). |pid| is non-zero if and
1288 only if a child exited during the wait.
1289
1290 Raises:
1291 Same as for os.waitpid(), except:
1292 OSError with errno==EINTR causes the wait to be retried (this can happen,
1293 for example, if this parent process receives SIGHUP).
1294 OSError with errno==ECHILD means there are no child processes, and so
1295 this function sleeps until |deadline|. If |deadline| is zero, this is an
1296 error and the OSError exception is raised in this case.
1297 """
1298 while True:
1299 try:
1300 if deadline == 0:
1301 pid_result, status = os.waitpid(pid, 0)
1302 else:
1303 pid_result, status = waitpid_with_timeout(pid, deadline)
1304 return (pid_result, status)
1305 except OSError as e:
1306 if e.errno == errno.EINTR:
1307 continue
1308 elif e.errno == errno.ECHILD:
1309 now = time.time()
1310 if deadline == 0:
1311 # No time-limit and no child processes. This is treated as an error
1312 # (see docstring).
1313 raise
1314 elif deadline > now:
1315 time.sleep(deadline - now)
1316 return (0, 0)
1317 else:
1318 # Anything else is an unexpected error.
1319 raise
1320
1321
1322def watch_for_resolution_changes(initial_size):
1323 """Watches for any resolution-changes which set the maximum screen resolution,
1324 and resets the initial size if this happens.
1325
1326 The Ubuntu desktop has a component (the 'xrandr' plugin of
1327 unity-settings-daemon) which often changes the screen resolution to the
1328 first listed mode. This is the built-in mode for the maximum screen size,
1329 which can trigger excessive CPU usage in some situations. So this is a hack
1330 which waits for any such events, and undoes the change if it occurs.
1331
1332 Sometimes, the user might legitimately want to use the maximum available
1333 resolution, so this monitoring is limited to a short time-period.
1334 """
1335 for _ in range(30):
1336 time.sleep(1)
1337
1338 xrandr_output = subprocess.Popen(["xrandr"],
1339 stdout=subprocess.PIPE).communicate()[0]
1340 matches = re.search(r'current (\d+) x (\d+), maximum (\d+) x (\d+)',
1341 xrandr_output)
1342
1343 # No need to handle ValueError. If xrandr fails to give valid output,
1344 # there's no point in continuing to monitor.
1345 current_size = (int(matches.group(1)), int(matches.group(2)))
1346 maximum_size = (int(matches.group(3)), int(matches.group(4)))
1347
1348 if current_size != initial_size:
1349 # Resolution change detected.
1350 if current_size == maximum_size:
1351 # This was probably an automated change from unity-settings-daemon, so
1352 # undo it.
1353 label = "%dx%d" % initial_size
1354 args = ["xrandr", "-s", label]
1355 subprocess.call(args)
1356 args = ["xrandr", "--dpi", "96"]
1357 subprocess.call(args)
1358
1359 # Stop monitoring after any change was detected.
1360 break
1361
1362
1363def main():
1364 EPILOG = """This script is not intended for use by end-users. To configure
1365Chrome Remote Desktop, please install the app from the Chrome
1366Web Store: https://chrome.google.com/remotedesktop"""
1367 parser = argparse.ArgumentParser(
1368 usage="Usage: %(prog)s [options] [ -- [ X server options ] ]",
1369 epilog=EPILOG)
1370 parser.add_argument("-s", "--size", dest="size", action="append",
1371 help="Dimensions of virtual desktop. This can be "
1372 "specified multiple times to make multiple screen "
1373 "resolutions available (if the X server supports this).")
1374 parser.add_argument("-f", "--foreground", dest="foreground", default=False,
1375 action="store_true",
1376 help="Don't run as a background daemon.")
1377 parser.add_argument("--start", dest="start", default=False,
1378 action="store_true",
1379 help="Start the host.")
1380 parser.add_argument("-k", "--stop", dest="stop", default=False,
1381 action="store_true",
1382 help="Stop the daemon currently running.")
1383 parser.add_argument("--get-status", dest="get_status", default=False,
1384 action="store_true",
1385 help="Prints host status")
1386 parser.add_argument("--check-running", dest="check_running",
1387 default=False, action="store_true",
1388 help="Return 0 if the daemon is running, or 1 otherwise.")
1389 parser.add_argument("--config", dest="config", action="store",
1390 help="Use the specified configuration file.")
1391 parser.add_argument("--reload", dest="reload", default=False,
1392 action="store_true",
1393 help="Signal currently running host to reload the "
1394 "config.")
1395 parser.add_argument("--add-user", dest="add_user", default=False,
1396 action="store_true",
1397 help="Add current user to the chrome-remote-desktop "
1398 "group.")
1399 parser.add_argument("--add-user-as-root", dest="add_user_as_root",
1400 action="store", metavar="USER",
1401 help="Adds the specified user to the "
1402 "chrome-remote-desktop group (must be run as root).")
1403 # The script is being run as a child process under the user-session binary.
1404 # Don't daemonize and use the inherited environment.
1405 parser.add_argument("--child-process", dest="child_process", default=False,
1406 action="store_true",
1407 help=argparse.SUPPRESS)
1408 parser.add_argument("--watch-resolution", dest="watch_resolution",
1409 type=int, nargs=2, default=False, action="store",
1410 help=argparse.SUPPRESS)
1411 parser.add_argument("--skip-config-upgrade", dest="skip_config_upgrade",
1412 default=False, action="store_true",
1413 help="Skip running the config upgrade tool.")
1414 parser.add_argument(dest="args", nargs="*", help=argparse.SUPPRESS)
1415 options = parser.parse_args()
1416
1417 # Determine the filename of the host configuration.
1418 if options.config:
1419 config_file = options.config
1420 else:
1421 config_file = os.path.join(CONFIG_DIR, "host#%s.json" % g_host_hash)
1422 config_file = os.path.realpath(config_file)
1423
1424 # Check for a modal command-line option (start, stop, etc.)
1425 if options.get_status:
1426 proc = get_daemon_proc(config_file)
1427 if proc is not None:
1428 print("STARTED")
1429 elif is_supported_platform():
1430 print("STOPPED")
1431 else:
1432 print("NOT_IMPLEMENTED")
1433 return 0
1434
1435 # TODO(sergeyu): Remove --check-running once NPAPI plugin and NM host are
1436 # updated to always use get-status flag instead.
1437 if options.check_running:
1438 proc = get_daemon_proc(config_file)
1439 return 1 if proc is None else 0
1440
1441 if options.stop:
1442 proc = get_daemon_proc(config_file)
1443 if proc is None:
1444 print("The daemon is not currently running")
1445 else:
1446 print("Killing process %s" % proc.pid)
1447 proc.terminate()
1448 try:
1449 proc.wait(timeout=30)
1450 except psutil.TimeoutExpired:
1451 print("Timed out trying to kill daemon process")
1452 return 1
1453 return 0
1454
1455 if options.reload:
1456 proc = get_daemon_proc(config_file)
1457 if proc is None:
1458 return 1
1459 proc.send_signal(signal.SIGHUP)
1460 return 0
1461
1462 if options.add_user:
1463 user = getpass.getuser()
1464
1465 try:
1466 if user in grp.getgrnam(CHROME_REMOTING_GROUP_NAME).gr_mem:
1467 logging.info("User '%s' is already a member of '%s'." %
1468 (user, CHROME_REMOTING_GROUP_NAME))
1469 return 0
1470 except KeyError:
1471 logging.info("Group '%s' not found." % CHROME_REMOTING_GROUP_NAME)
1472
1473 command = [SCRIPT_PATH, '--add-user-as-root', user]
1474 if os.getenv("DISPLAY"):
1475 # TODO(rickyz): Add a Polkit policy that includes a more friendly message
1476 # about what this command does.
1477 command = ["/usr/bin/pkexec"] + command
1478 else:
1479 command = ["/usr/bin/sudo", "-k", "--"] + command
1480
1481 # Run with an empty environment out of paranoia, though if an attacker
1482 # controls the environment this script is run under, we're already screwed
1483 # anyway.
1484 os.execve(command[0], command, {})
1485 return 1
1486
1487 if options.add_user_as_root is not None:
1488 if os.getuid() != 0:
1489 logging.error("--add-user-as-root can only be specified as root.")
1490 return 1;
1491
1492 user = options.add_user_as_root
1493 try:
1494 pwd.getpwnam(user)
1495 except KeyError:
1496 logging.error("user '%s' does not exist." % user)
1497 return 1
1498
1499 try:
1500 subprocess.check_call(["/usr/sbin/groupadd", "-f",
1501 CHROME_REMOTING_GROUP_NAME])
1502 subprocess.check_call(["/usr/bin/gpasswd", "--add", user,
1503 CHROME_REMOTING_GROUP_NAME])
1504 except (ValueError, OSError, subprocess.CalledProcessError) as e:
1505 logging.error("Command failed: " + str(e))
1506 return 1
1507
1508 return 0
1509
1510 if options.watch_resolution:
1511 watch_for_resolution_changes(options.watch_resolution)
1512 return 0
1513
1514 if not options.start:
1515 # If no modal command-line options specified, print an error and exit.
1516 print(EPILOG, file=sys.stderr)
1517 return 1
1518
1519 # Determine whether a desktop is already active for the specified host
1520 # configuration.
1521 if get_daemon_proc(config_file, options.child_process) is not None:
1522 # Debian policy requires that services should "start" cleanly and return 0
1523 # if they are already running.
1524 if options.child_process:
1525 # If the script is running under user-session, try to relay the message.
1526 ParentProcessLogger.try_start_logging(USER_SESSION_MESSAGE_FD)
1527 logging.info("Service already running.")
1528 ParentProcessLogger.release_parent_if_connected(True)
1529 return 0
1530
1531 if config_file != options.config:
1532 # --config was either not specified or isn't a canonical absolute path.
1533 # Replace it with the canonical path so get_daemon_proc can find us.
1534 sys.argv = ([sys.argv[0], "--config=" + config_file] +
1535 parse_config_arg(sys.argv[1:])[1])
1536 if options.child_process:
1537 os.execvp(sys.argv[0], sys.argv)
1538
1539 if not options.child_process:
1540 return start_via_user_session(options.foreground)
1541
1542 # Start logging to user-session messaging pipe if it exists.
1543 ParentProcessLogger.try_start_logging(USER_SESSION_MESSAGE_FD)
1544
1545 if USE_XORG_ENV_VAR in os.environ:
1546 default_sizes = DEFAULT_SIZES_XORG
1547 else:
1548 default_sizes = DEFAULT_SIZES
1549
1550 # Collate the list of sizes that XRANDR should support.
1551 if not options.size:
1552 if DEFAULT_SIZES_ENV_VAR in os.environ:
1553 default_sizes = os.environ[DEFAULT_SIZES_ENV_VAR]
1554 options.size = default_sizes.split(",")
1555
1556 sizes = []
1557 for size in options.size:
1558 size_components = size.split("x")
1559 if len(size_components) != 2:
1560 parser.error("Incorrect size format '%s', should be WIDTHxHEIGHT" % size)
1561
1562 try:
1563 width = int(size_components[0])
1564 height = int(size_components[1])
1565
1566 # Enforce minimum desktop size, as a sanity-check. The limit of 100 will
1567 # detect typos of 2 instead of 3 digits.
1568 if width < 100 or height < 100:
1569 raise ValueError
1570 except ValueError:
1571 parser.error("Width and height should be 100 pixels or greater")
1572
1573 sizes.append((width, height))
1574
1575 # Register an exit handler to clean up session process and the PID file.
1576 atexit.register(cleanup)
1577
1578 # Run the config upgrade tool, to update the refresh token if needed.
1579 # TODO(lambroslambrou): Respect CHROME_REMOTE_DESKTOP_HOST_EXTRA_PARAMS
1580 # and the GOOGLE_CLIENT... variables, and fix the tool to work in a
1581 # test environment.
1582 if not options.skip_config_upgrade:
1583 args = [HOST_BINARY_PATH, "--upgrade-token",
1584 "--host-config=%s" % config_file]
1585 subprocess.check_call(args);
1586
1587 # Load the initial host configuration.
1588 host_config = Config(config_file)
1589 try:
1590 host_config.load()
1591 except (IOError, ValueError) as e:
1592 print("Failed to load config: " + str(e), file=sys.stderr)
1593 return 1
1594
1595 # Register handler to re-load the configuration in response to signals.
1596 for s in [signal.SIGHUP, signal.SIGINT, signal.SIGTERM]:
1597 signal.signal(s, SignalHandler(host_config))
1598
1599 # Verify that the initial host configuration has the necessary fields.
1600 auth = Authentication()
1601 auth_config_valid = auth.copy_from(host_config)
1602 host = Host()
1603 host_config_valid = host.copy_from(host_config)
1604 if not host_config_valid or not auth_config_valid:
1605 logging.error("Failed to load host configuration.")
1606 return 1
1607
1608 if host.host_id:
1609 logging.info("Using host_id: " + host.host_id)
1610 if host.gcd_device_id:
1611 logging.info("Using gcd_device_id: " + host.gcd_device_id)
1612
1613 desktop = Desktop(sizes)
1614
1615 # Keep track of the number of consecutive failures of any child process to
1616 # run for longer than a set period of time. The script will exit after a
1617 # threshold is exceeded.
1618 # There is no point in tracking the X session process separately, since it is
1619 # launched at (roughly) the same time as the X server, and the termination of
1620 # one of these triggers the termination of the other.
1621 x_server_inhibitor = RelaunchInhibitor("X server")
1622 session_inhibitor = RelaunchInhibitor("session")
1623 host_inhibitor = RelaunchInhibitor("host")
1624 all_inhibitors = [
1625 (x_server_inhibitor, HOST_OFFLINE_REASON_X_SERVER_RETRIES_EXCEEDED),
1626 (session_inhibitor, HOST_OFFLINE_REASON_SESSION_RETRIES_EXCEEDED),
1627 (host_inhibitor, HOST_OFFLINE_REASON_HOST_RETRIES_EXCEEDED)
1628 ]
1629
1630 # Whether we are tearing down because the X server and/or session exited.
1631 # This keeps us from counting processes exiting because we've terminated them
1632 # as errors.
1633 tear_down = False
1634
1635 while True:
1636 # If the session process or X server stops running (e.g. because the user
1637 # logged out), terminate all processes. The session will be restarted once
1638 # everything has exited.
1639 if tear_down:
1640 desktop.shutdown_all_procs()
1641
1642 failure_count = 0
1643 for inhibitor, _ in all_inhibitors:
1644 if inhibitor.running:
1645 inhibitor.record_stopped(True)
1646 failure_count += inhibitor.failures
1647
1648 tear_down = False
1649
1650 if (failure_count == 0):
1651 # Since the user's desktop is already gone at this point, there's no
1652 # state to lose and now is a good time to pick up any updates to this
1653 # script that might have been installed.
1654 logging.info("Relaunching self")
1655 relaunch_self()
1656 else:
1657 # If there is a non-zero |failures| count, restarting the whole script
1658 # would lose this information, so just launch the session as normal,
1659 # below.
1660 pass
1661
1662 relaunch_times = []
1663
1664 # Set the backoff interval and exit if a process failed too many times.
1665 backoff_time = SHORT_BACKOFF_TIME
1666 for inhibitor, offline_reason in all_inhibitors:
1667 if inhibitor.failures >= MAX_LAUNCH_FAILURES:
1668 logging.error("Too many launch failures of '%s', exiting."
1669 % inhibitor.label)
1670 desktop.report_offline_reason(host_config, offline_reason)
1671 return 1
1672 elif inhibitor.failures >= SHORT_BACKOFF_THRESHOLD:
1673 backoff_time = LONG_BACKOFF_TIME
1674
1675 if inhibitor.is_inhibited():
1676 relaunch_times.append(inhibitor.earliest_relaunch_time)
1677
1678 if relaunch_times:
1679 # We want to wait until everything is ready to start so we don't end up
1680 # launching things in the wrong order due to differing relaunch times.
1681 logging.info("Waiting before relaunching")
1682 else:
1683 if desktop.x_proc is None and desktop.session_proc is None:
1684 logging.info("Launching X server and X session.")
1685 desktop.launch_session(options.args)
1686 x_server_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME,
1687 backoff_time)
1688 session_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME,
1689 backoff_time)
1690 if desktop.host_proc is None:
1691 logging.info("Launching host process")
1692
1693 extra_start_host_args = []
1694 if HOST_EXTRA_PARAMS_ENV_VAR in os.environ:
1695 extra_start_host_args = \
1696 re.split('\s+', os.environ[HOST_EXTRA_PARAMS_ENV_VAR].strip())
1697 desktop.launch_host(host_config, extra_start_host_args)
1698
1699 host_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME, backoff_time)
1700
1701 deadline = max(relaunch_times) if relaunch_times else 0
1702 pid, status = waitpid_handle_exceptions(-1, deadline)
1703 if pid == 0:
1704 continue
1705
1706 logging.info("wait() returned (%s,%s)" % (pid, status))
1707
1708 # When a process has terminated, and we've reaped its exit-code, any Popen
1709 # instance for that process is no longer valid. Reset any affected instance
1710 # to None.
1711 if desktop.x_proc is not None and pid == desktop.x_proc.pid:
1712 logging.info("X server process terminated")
1713 desktop.x_proc = None
1714 x_server_inhibitor.record_stopped(False)
1715 tear_down = True
1716
1717 if desktop.session_proc is not None and pid == desktop.session_proc.pid:
1718 logging.info("Session process terminated")
1719 desktop.session_proc = None
1720 # The session may have exited on its own or been brought down by the X
1721 # server dying. Check if the X server is still running so we know whom
1722 # to penalize.
1723 if desktop.check_x_responding():
1724 session_inhibitor.record_stopped(False)
1725 else:
1726 x_server_inhibitor.record_stopped(False)
1727 # Either way, we want to tear down the session.
1728 tear_down = True
1729
1730 if desktop.host_proc is not None and pid == desktop.host_proc.pid:
1731 logging.info("Host process terminated")
1732 desktop.host_proc = None
1733 desktop.host_ready = False
1734
1735 # These exit-codes must match the ones used by the host.
1736 # See remoting/host/host_error_codes.h.
1737 # Delete the host or auth configuration depending on the returned error
1738 # code, so the next time this script is run, a new configuration
1739 # will be created and registered.
1740 if os.WIFEXITED(status):
1741 if os.WEXITSTATUS(status) == 100:
1742 logging.info("Host configuration is invalid - exiting.")
1743 return 0
1744 elif os.WEXITSTATUS(status) == 101:
1745 logging.info("Host ID has been deleted - exiting.")
1746 host_config.clear()
1747 host_config.save_and_log_errors()
1748 return 0
1749 elif os.WEXITSTATUS(status) == 102:
1750 logging.info("OAuth credentials are invalid - exiting.")
1751 return 0
1752 elif os.WEXITSTATUS(status) == 103:
1753 logging.info("Host domain is blocked by policy - exiting.")
1754 return 0
1755 # Nothing to do for Mac-only status 104 (login screen unsupported)
1756 elif os.WEXITSTATUS(status) == 105:
1757 logging.info("Username is blocked by policy - exiting.")
1758 return 0
1759 elif os.WEXITSTATUS(status) == 106:
1760 logging.info("Host has been deleted - exiting.")
1761 return 0
1762 else:
1763 logging.info("Host exited with status %s." % os.WEXITSTATUS(status))
1764 elif os.WIFSIGNALED(status):
1765 logging.info("Host terminated by signal %s." % os.WTERMSIG(status))
1766
1767 # The host may have exited on it's own or been brought down by the X
1768 # server dying. Check if the X server is still running so we know whom to
1769 # penalize.
1770 if desktop.check_x_responding():
1771 host_inhibitor.record_stopped(False)
1772 else:
1773 x_server_inhibitor.record_stopped(False)
1774 # Only tear down if the X server isn't responding.
1775 tear_down = True
1776
1777
1778if __name__ == "__main__":
1779 logging.basicConfig(level=logging.DEBUG,
1780 format="%(asctime)s:%(levelname)s:%(message)s")
1781 sys.exit(main())