Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / remoting / host / linux / linux_me2me_host.py
1 #!/usr/bin/python
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
12 import atexit
13 import errno
14 import fcntl
15 import getpass
16 import grp
17 import hashlib
18 import json
19 import logging
20 import optparse
21 import os
22 import pipes
23 import platform
24 import psutil
25 import platform
26 import signal
27 import socket
28 import subprocess
29 import sys
30 import tempfile
31 import time
32 import uuid
33
34 LOG_FILE_ENV_VAR = "CHROME_REMOTE_DESKTOP_LOG_FILE"
35
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"
40
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"
46
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"
50
51 SCRIPT_PATH = sys.path[0]
52
53 IS_INSTALLED = (os.path.basename(sys.argv[0]) != 'linux_me2me_host.py')
54
55 if IS_INSTALLED:
56   HOST_BINARY_NAME = "chrome-remote-desktop-host"
57 else:
58   HOST_BINARY_NAME = "remoting_me2me_host"
59
60 CHROME_REMOTING_GROUP_NAME = "chrome-remote-desktop"
61
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"
66
67 X_LOCK_FILE_TEMPLATE = "/tmp/.X%d-lock"
68 FIRST_X_DISPLAY_NUMBER = 20
69
70 # Amount of time to wait between relaunching processes.
71 SHORT_BACKOFF_TIME = 5
72 LONG_BACKOFF_TIME = 60
73
74 # How long a process must run in order not to be counted against the restart
75 # thresholds.
76 MINIMUM_PROCESS_LIFETIME = 60
77
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
82
83 # Globals needed by the atexit cleanup() handler.
84 g_desktops = []
85 g_host_hash = hashlib.md5(socket.gethostname()).hexdigest()
86
87
88 def is_supported_platform():
89   # Always assume that the system is supported if the config directory or
90   # session file exist.
91   if (os.path.isdir(CONFIG_DIR) or os.path.isfile(SESSION_FILE_PATH) or
92       os.path.isfile(SYSTEM_SESSION_FILE_PATH)):
93     return True
94
95   # The host has been tested only on Ubuntu.
96   distribution = platform.linux_distribution()
97   return (distribution[0]).lower() == 'ubuntu'
98
99
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."""
103   try:
104     xvfb = "/usr/bin/Xvfb-randr"
105     if not os.path.exists(xvfb):
106       xvfb = locate_executable("Xvfb-randr")
107     return xvfb
108   except Exception:
109     return None
110
111
112 class Config:
113   def __init__(self, path):
114     self.path = path
115     self.data = {}
116     self.changed = False
117
118   def load(self):
119     """Loads the config from file.
120
121     Raises:
122       IOError: Error reading data
123       ValueError: Error parsing JSON
124     """
125     settings_file = open(self.path, 'r')
126     self.data = json.load(settings_file)
127     self.changed = False
128     settings_file.close()
129
130   def save(self):
131     """Saves the config to file.
132
133     Raises:
134       IOError: Error writing data
135       TypeError: Error serialising JSON
136     """
137     if not self.changed:
138       return
139     old_umask = os.umask(0066)
140     try:
141       settings_file = open(self.path, 'w')
142       settings_file.write(json.dumps(self.data, indent=2))
143       settings_file.close()
144       self.changed = False
145     finally:
146       os.umask(old_umask)
147
148   def save_and_log_errors(self):
149     """Calls self.save(), trapping and logging any errors."""
150     try:
151       self.save()
152     except (IOError, TypeError) as e:
153       logging.error("Failed to save config: " + str(e))
154
155   def get(self, key):
156     return self.data.get(key)
157
158   def __getitem__(self, key):
159     return self.data[key]
160
161   def __setitem__(self, key, value):
162     self.data[key] = value
163     self.changed = True
164
165   def clear(self):
166     self.data = {}
167     self.changed = True
168
169
170 class Authentication:
171   """Manage authentication tokens for Chromoting/xmpp"""
172
173   def __init__(self):
174     self.login = None
175     self.oauth_refresh_token = None
176
177   def copy_from(self, config):
178     """Loads the config and returns false if the config is invalid."""
179     try:
180       self.login = config["xmpp_login"]
181       self.oauth_refresh_token = config["oauth_refresh_token"]
182     except KeyError:
183       return False
184     return True
185
186   def copy_to(self, config):
187     config["xmpp_login"] = self.login
188     config["oauth_refresh_token"] = self.oauth_refresh_token
189
190
191 class Host:
192   """This manages the configuration for a host."""
193
194   def __init__(self):
195     self.host_id = str(uuid.uuid1())
196     self.host_name = socket.gethostname()
197     self.host_secret_hash = None
198     self.private_key = None
199
200   def copy_from(self, config):
201     try:
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"]
206     except KeyError:
207       return False
208     return True
209
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
215
216
217 class Desktop:
218   """Manage a single virtual desktop"""
219
220   def __init__(self, sizes):
221     self.x_proc = None
222     self.session_proc = None
223     self.host_proc = None
224     self.child_env = None
225     self.sizes = sizes
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)
231
232   @staticmethod
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):
238       display += 1
239     return display
240
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.
244     self.child_env = {}
245
246     for key in [
247         "HOME",
248         "LANG",
249         "LOGNAME",
250         "PATH",
251         "SHELL",
252         "USER",
253         "USERNAME",
254         LOG_FILE_ENV_VAR]:
255       if os.environ.has_key(key):
256         self.child_env[key] = os.environ[key]
257
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() })
265
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
269     # configuration.
270     env_filename = "/etc/environment"
271     try:
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
283     except IOError:
284       logging.info("Failed to read %s, skipping." % env_filename)
285
286   def _setup_pulseaudio(self):
287     self.pulseaudio_pipe = None
288
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.")
298       return False
299
300     sink_name = "chrome_remote_desktop_session"
301     pipe_name = os.path.join(pulse_path, "fifo_output")
302
303     try:
304       if not os.path.exists(pulse_path):
305         os.mkdir(pulse_path)
306     except IOError, e:
307       logging.error("Failed to create pulseaudio pipe: " + str(e))
308       return False
309
310     try:
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")
315       pulse_config.close()
316
317       pulse_script = open(os.path.join(pulse_path, "default.pa"), "w")
318       pulse_script.write("load-module module-native-protocol-unix\n")
319       pulse_script.write(
320           ("load-module module-pipe-sink sink_name=%s file=\"%s\" " +
321            "rate=48000 channels=2 format=s16le\n") %
322           (sink_name, pipe_name))
323       pulse_script.close()
324     except IOError, e:
325       logging.error("Failed to write pulseaudio config: " + str(e))
326       return False
327
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
333
334     return True
335
336   def _setup_gnubby(self):
337     self.ssh_auth_sockname = ("/tmp/chromoting.%s.ssh_auth_sock" %
338                               os.environ["USER"])
339
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()
345
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)
350     if ret_code != 0:
351       raise Exception("xauth failed with code %d" % ret_code)
352
353     max_width = max([width for width, height in self.sizes])
354     max_height = max([height for width, height in self.sizes])
355
356     xvfb = get_randr_supporting_x_server()
357     if xvfb:
358       self.server_supports_exact_resize = True
359     else:
360       xvfb = "Xvfb"
361       self.server_supports_exact_resize = False
362
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"])
370
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,
376          "-nolisten", "tcp",
377          "-noreset",
378          "-screen", "0", screen_option
379         ] + extra_x_args)
380     if not self.x_proc.pid:
381       raise Exception("Could not start Xvfb.")
382
383     self.child_env["DISPLAY"] = ":%d" % display
384     self.child_env["CHROME_REMOTE_DESKTOP_SESSION"] = "1"
385
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
391
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
395
396     # Wait for X to be active.
397     for _test in range(20):
398       retcode = subprocess.call("xdpyinfo", env=self.child_env, stdout=devnull)
399       if retcode == 0:
400         break
401       time.sleep(0.5)
402     if retcode != 0:
403       raise Exception("Could not connect to Xvfb.")
404     else:
405       logging.info("Xvfb is active.")
406
407     # The remoting host expects the server to use "evdev" keycodes, but Xvfb
408     # starts configured to use the "base" ruleset, resulting in XKB configuring
409     # for "xfree86" keycodes, and screwing up some keys. See crbug.com/119013.
410     # Reconfigure the X server to use "evdev" keymap rules.  The X server must
411     # be started with -noreset otherwise it'll reset as soon as the command
412     # completes, since there are no other X clients running yet.
413     retcode = subprocess.call("setxkbmap -rules evdev", env=self.child_env,
414                               shell=True)
415     if retcode != 0:
416       logging.error("Failed to set XKB to 'evdev'")
417
418     # Register the screen sizes if the X server's RANDR extension supports it.
419     # Errors here are non-fatal; the X server will continue to run with the
420     # dimensions from the "-screen" option.
421     for width, height in self.sizes:
422       label = "%dx%d" % (width, height)
423       args = ["xrandr", "--newmode", label, "0", str(width), "0", "0", "0",
424               str(height), "0", "0", "0"]
425       subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
426       args = ["xrandr", "--addmode", "screen", label]
427       subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
428
429     # Set the initial mode to the first size specified, otherwise the X server
430     # would default to (max_width, max_height), which might not even be in the
431     # list.
432     label = "%dx%d" % self.sizes[0]
433     args = ["xrandr", "-s", label]
434     subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
435
436     # Set the physical size of the display so that the initial mode is running
437     # at approximately 96 DPI, since some desktops require the DPI to be set to
438     # something realistic.
439     args = ["xrandr", "--dpi", "96"]
440     subprocess.call(args, env=self.child_env, stdout=devnull, stderr=devnull)
441
442     devnull.close()
443
444   def _launch_x_session(self):
445     # Start desktop session.
446     # The /dev/null input redirection is necessary to prevent the X session
447     # reading from stdin.  If this code runs as a shell background job in a
448     # terminal, any reading from stdin causes the job to be suspended.
449     # Daemonization would solve this problem by separating the process from the
450     # controlling terminal.
451     xsession_command = choose_x_session()
452     if xsession_command is None:
453       raise Exception("Unable to choose suitable X session command.")
454
455     logging.info("Launching X session: %s" % xsession_command)
456     self.session_proc = subprocess.Popen(xsession_command,
457                                          stdin=open(os.devnull, "r"),
458                                          cwd=HOME_DIR,
459                                          env=self.child_env)
460     if not self.session_proc.pid:
461       raise Exception("Could not start X session")
462
463   def launch_session(self, x_args):
464     self._init_child_env()
465     self._setup_pulseaudio()
466     self._setup_gnubby()
467     self._launch_x_server(x_args)
468     self._launch_x_session()
469
470   def launch_host(self, host_config):
471     # Start remoting host
472     args = [locate_executable(HOST_BINARY_NAME), "--host-config=-"]
473     if self.pulseaudio_pipe:
474       args.append("--audio-pipe-name=%s" % self.pulseaudio_pipe)
475     if self.server_supports_exact_resize:
476       args.append("--server-supports-exact-resize")
477     if self.ssh_auth_sockname:
478       args.append("--ssh-auth-sockname=%s" % self.ssh_auth_sockname)
479
480     # Have the host process use SIGUSR1 to signal a successful start.
481     def sigusr1_handler(signum, frame):
482       _ = signum, frame
483       logging.info("Host ready to receive connections.")
484       self.host_ready = True
485       if (ParentProcessLogger.instance() and
486           False not in [desktop.host_ready for desktop in g_desktops]):
487         ParentProcessLogger.instance().release_parent()
488
489     signal.signal(signal.SIGUSR1, sigusr1_handler)
490     args.append("--signal-parent")
491
492     self.host_proc = subprocess.Popen(args, env=self.child_env,
493                                       stdin=subprocess.PIPE)
494     logging.info(args)
495     if not self.host_proc.pid:
496       raise Exception("Could not start Chrome Remote Desktop host")
497     self.host_proc.stdin.write(json.dumps(host_config.data))
498     self.host_proc.stdin.close()
499
500
501 def get_daemon_proc():
502   """Checks if there is already an instance of this script running, and returns
503   a psutil.Process instance for it.
504
505   Returns:
506     A Process instance for the existing daemon process, or None if the daemon
507     is not running.
508   """
509
510   uid = os.getuid()
511   this_pid = os.getpid()
512
513   # Support new & old psutil API. This is the right way to check, according to
514   # http://grodola.blogspot.com/2014/01/psutil-20-porting.html
515   if psutil.version_info >= (2, 0):
516     psget = lambda x: x()
517   else:
518     psget = lambda x: x
519
520   for process in psutil.process_iter():
521     # Skip any processes that raise an exception, as processes may terminate
522     # during iteration over the list.
523     try:
524       # Skip other users' processes.
525       if psget(process.uids).real != uid:
526         continue
527
528       # Skip the process for this instance.
529       if process.pid == this_pid:
530         continue
531
532       # |cmdline| will be [python-interpreter, script-file, other arguments...]
533       cmdline = psget(process.cmdline)
534       if len(cmdline) < 2:
535         continue
536       if cmdline[0] == sys.executable and cmdline[1] == sys.argv[0]:
537         return process
538     except (psutil.NoSuchProcess, psutil.AccessDenied):
539       continue
540
541   return None
542
543
544 def choose_x_session():
545   """Chooses the most appropriate X session command for this system.
546
547   Returns:
548     A string containing the command to run, or a list of strings containing
549     the executable program and its arguments, which is suitable for passing as
550     the first parameter of subprocess.Popen().  If a suitable session cannot
551     be found, returns None.
552   """
553   XSESSION_FILES = [
554     SESSION_FILE_PATH,
555     SYSTEM_SESSION_FILE_PATH ]
556   for startup_file in XSESSION_FILES:
557     startup_file = os.path.expanduser(startup_file)
558     if os.path.exists(startup_file):
559       if os.access(startup_file, os.X_OK):
560         # "/bin/sh -c" is smart about how to execute the session script and
561         # works in cases where plain exec() fails (for example, if the file is
562         # marked executable, but is a plain script with no shebang line).
563         return ["/bin/sh", "-c", pipes.quote(startup_file)]
564       else:
565         # If this is a system-wide session script, it should be run using the
566         # system shell, ignoring any login shell that might be set for the
567         # current user.
568         return ["/bin/sh", startup_file]
569
570   # Choose a session wrapper script to run the session. On some systems,
571   # /etc/X11/Xsession fails to load the user's .profile, so look for an
572   # alternative wrapper that is more likely to match the script that the
573   # system actually uses for console desktop sessions.
574   SESSION_WRAPPERS = [
575     "/usr/sbin/lightdm-session",
576     "/etc/gdm/Xsession",
577     "/etc/X11/Xsession" ]
578   for session_wrapper in SESSION_WRAPPERS:
579     if os.path.exists(session_wrapper):
580       if os.path.exists("/usr/bin/unity-2d-panel"):
581         # On Ubuntu 12.04, the default session relies on 3D-accelerated
582         # hardware. Trying to run this with a virtual X display produces
583         # weird results on some systems (for example, upside-down and
584         # corrupt displays).  So if the ubuntu-2d session is available,
585         # choose it explicitly.
586         return [session_wrapper, "/usr/bin/gnome-session --session=ubuntu-2d"]
587       else:
588         # Use the session wrapper by itself, and let the system choose a
589         # session.
590         return [session_wrapper]
591   return None
592
593
594 def locate_executable(exe_name):
595   if IS_INSTALLED:
596     # If the script is running from its installed location, search the host
597     # binary only in the same directory.
598     paths_to_try = [ SCRIPT_PATH ]
599   else:
600     paths_to_try = map(lambda p: os.path.join(SCRIPT_PATH, p),
601                        [".", "../../../out/Debug", "../../../out/Release" ])
602   for path in paths_to_try:
603     exe_path = os.path.join(path, exe_name)
604     if os.path.exists(exe_path):
605       return exe_path
606
607   raise Exception("Could not locate executable '%s'" % exe_name)
608
609
610 class ParentProcessLogger(object):
611   """Redirects logs to the parent process, until the host is ready or quits.
612
613   This class creates a pipe to allow logging from the daemon process to be
614   copied to the parent process. The daemon process adds a log-handler that
615   directs logging output to the pipe. The parent process reads from this pipe
616   until and writes the content to stderr.  When the pipe is no longer needed
617   (for example, the host signals successful launch or permanent failure), the
618   daemon removes the log-handler and closes the pipe, causing the the parent
619   process to reach end-of-file while reading the pipe and exit.
620
621   The (singleton) logger should be instantiated before forking. The parent
622   process should call wait_for_logs() before exiting. The (grand-)child process
623   should call start_logging() when it starts, and then use logging.* to issue
624   log statements, as usual. When the child has either succesfully started the
625   host or terminated, it must call release_parent() to allow the parent to exit.
626   """
627
628   __instance = None
629
630   def __init__(self):
631     """Constructor. Must be called before forking."""
632     read_pipe, write_pipe = os.pipe()
633     # Ensure write_pipe is closed on exec, otherwise it will be kept open by
634     # child processes (X, host), preventing the read pipe from EOF'ing.
635     old_flags = fcntl.fcntl(write_pipe, fcntl.F_GETFD)
636     fcntl.fcntl(write_pipe, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC)
637     self._read_file = os.fdopen(read_pipe, 'r')
638     self._write_file = os.fdopen(write_pipe, 'a')
639     self._logging_handler = None
640     ParentProcessLogger.__instance = self
641
642   def start_logging(self):
643     """Installs a logging handler that sends log entries to a pipe.
644
645     Must be called by the child process.
646     """
647     self._read_file.close()
648     self._logging_handler = logging.StreamHandler(self._write_file)
649     logging.getLogger().addHandler(self._logging_handler)
650
651   def release_parent(self):
652     """Uninstalls logging handler and closes the pipe, releasing the parent.
653
654     Must be called by the child process.
655     """
656     if self._logging_handler:
657       logging.getLogger().removeHandler(self._logging_handler)
658       self._logging_handler = None
659     if not self._write_file.closed:
660       self._write_file.close()
661
662   def wait_for_logs(self):
663     """Waits and prints log lines from the daemon until the pipe is closed.
664
665     Must be called by the parent process.
666     """
667     # If Ctrl-C is pressed, inform the user that the daemon is still running.
668     # This signal will cause the read loop below to stop with an EINTR IOError.
669     def sigint_handler(signum, frame):
670       _ = signum, frame
671       print >> sys.stderr, ("Interrupted. The daemon is still running in the "
672                             "background.")
673
674     signal.signal(signal.SIGINT, sigint_handler)
675
676     # Install a fallback timeout to release the parent process, in case the
677     # daemon never responds (e.g. host crash-looping, daemon killed).
678     # This signal will cause the read loop below to stop with an EINTR IOError.
679     def sigalrm_handler(signum, frame):
680       _ = signum, frame
681       print >> sys.stderr, ("No response from daemon. It may have crashed, or "
682                             "may still be running in the background.")
683
684     signal.signal(signal.SIGALRM, sigalrm_handler)
685     signal.alarm(30)
686
687     self._write_file.close()
688
689     # Print lines as they're logged to the pipe until EOF is reached or readline
690     # is interrupted by one of the signal handlers above.
691     try:
692       for line in iter(self._read_file.readline, ''):
693         sys.stderr.write(line)
694     except IOError as e:
695       if e.errno != errno.EINTR:
696         raise
697     print >> sys.stderr, "Log file: %s" % os.environ[LOG_FILE_ENV_VAR]
698
699   @staticmethod
700   def instance():
701     """Returns the singleton instance, if it exists."""
702     return ParentProcessLogger.__instance
703
704
705 def daemonize():
706   """Background this process and detach from controlling terminal, redirecting
707   stdout/stderr to a log file."""
708
709   # TODO(lambroslambrou): Having stdout/stderr redirected to a log file is not
710   # ideal - it could create a filesystem DoS if the daemon or a child process
711   # were to write excessive amounts to stdout/stderr.  Ideally, stdout/stderr
712   # should be redirected to a pipe or socket, and a process at the other end
713   # should consume the data and write it to a logging facility which can do
714   # data-capping or log-rotation. The 'logger' command-line utility could be
715   # used for this, but it might cause too much syslog spam.
716
717   # Create new (temporary) file-descriptors before forking, so any errors get
718   # reported to the main process and set the correct exit-code.
719   # The mode is provided, since Python otherwise sets a default mode of 0777,
720   # which would result in the new file having permissions of 0777 & ~umask,
721   # possibly leaving the executable bits set.
722   if not os.environ.has_key(LOG_FILE_ENV_VAR):
723     log_file_prefix = "chrome_remote_desktop_%s_" % time.strftime(
724         '%Y%m%d_%H%M%S', time.localtime(time.time()))
725     log_file = tempfile.NamedTemporaryFile(prefix=log_file_prefix, delete=False)
726     os.environ[LOG_FILE_ENV_VAR] = log_file.name
727     log_fd = log_file.file.fileno()
728   else:
729     log_fd = os.open(os.environ[LOG_FILE_ENV_VAR],
730                      os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0600)
731
732   devnull_fd = os.open(os.devnull, os.O_RDONLY)
733
734   parent_logger = ParentProcessLogger()
735
736   pid = os.fork()
737
738   if pid == 0:
739     # Child process
740     os.setsid()
741
742     # The second fork ensures that the daemon isn't a session leader, so that
743     # it doesn't acquire a controlling terminal.
744     pid = os.fork()
745
746     if pid == 0:
747       # Grandchild process
748       pass
749     else:
750       # Child process
751       os._exit(0)  # pylint: disable=W0212
752   else:
753     # Parent process
754     parent_logger.wait_for_logs()
755     os._exit(0)  # pylint: disable=W0212
756
757   logging.info("Daemon process started in the background, logging to '%s'" %
758                os.environ[LOG_FILE_ENV_VAR])
759
760   os.chdir(HOME_DIR)
761
762   parent_logger.start_logging()
763
764   # Copy the file-descriptors to create new stdin, stdout and stderr.  Note
765   # that dup2(oldfd, newfd) closes newfd first, so this will close the current
766   # stdin, stdout and stderr, detaching from the terminal.
767   os.dup2(devnull_fd, sys.stdin.fileno())
768   os.dup2(log_fd, sys.stdout.fileno())
769   os.dup2(log_fd, sys.stderr.fileno())
770
771   # Close the temporary file-descriptors.
772   os.close(devnull_fd)
773   os.close(log_fd)
774
775
776 def cleanup():
777   logging.info("Cleanup.")
778
779   global g_desktops
780   for desktop in g_desktops:
781     for proc, name in [(desktop.x_proc, "Xvfb"),
782                        (desktop.session_proc, "session"),
783                        (desktop.host_proc, "host")]:
784       if proc is not None:
785         logging.info("Terminating " + name)
786         try:
787           psutil_proc = psutil.Process(proc.pid)
788           psutil_proc.terminate()
789
790           # Use a short timeout, to avoid delaying service shutdown if the
791           # process refuses to die for some reason.
792           psutil_proc.wait(timeout=10)
793         except psutil.TimeoutExpired:
794           logging.error("Timed out - sending SIGKILL")
795           psutil_proc.kill()
796         except psutil.Error:
797           logging.error("Error terminating process")
798
799   g_desktops = []
800   if ParentProcessLogger.instance():
801     ParentProcessLogger.instance().release_parent()
802
803 class SignalHandler:
804   """Reload the config file on SIGHUP. Since we pass the configuration to the
805   host processes via stdin, they can't reload it, so terminate them. They will
806   be relaunched automatically with the new config."""
807
808   def __init__(self, host_config):
809     self.host_config = host_config
810
811   def __call__(self, signum, _stackframe):
812     if signum == signal.SIGHUP:
813       logging.info("SIGHUP caught, restarting host.")
814       try:
815         self.host_config.load()
816       except (IOError, ValueError) as e:
817         logging.error("Failed to load config: " + str(e))
818       for desktop in g_desktops:
819         if desktop.host_proc:
820           desktop.host_proc.send_signal(signal.SIGTERM)
821     else:
822       # Exit cleanly so the atexit handler, cleanup(), gets called.
823       raise SystemExit
824
825
826 class RelaunchInhibitor:
827   """Helper class for inhibiting launch of a child process before a timeout has
828   elapsed.
829
830   A managed process can be in one of these states:
831     running, not inhibited (running == True)
832     stopped and inhibited (running == False and is_inhibited() == True)
833     stopped but not inhibited (running == False and is_inhibited() == False)
834
835   Attributes:
836     label: Name of the tracked process. Only used for logging.
837     running: Whether the process is currently running.
838     earliest_relaunch_time: Time before which the process should not be
839       relaunched, or 0 if there is no limit.
840     failures: The number of times that the process ran for less than a
841       specified timeout, and had to be inhibited.  This count is reset to 0
842       whenever the process has run for longer than the timeout.
843   """
844
845   def __init__(self, label):
846     self.label = label
847     self.running = False
848     self.earliest_relaunch_time = 0
849     self.earliest_successful_termination = 0
850     self.failures = 0
851
852   def is_inhibited(self):
853     return (not self.running) and (time.time() < self.earliest_relaunch_time)
854
855   def record_started(self, minimum_lifetime, relaunch_delay):
856     """Record that the process was launched, and set the inhibit time to
857     |timeout| seconds in the future."""
858     self.earliest_relaunch_time = time.time() + relaunch_delay
859     self.earliest_successful_termination = time.time() + minimum_lifetime
860     self.running = True
861
862   def record_stopped(self):
863     """Record that the process was stopped, and adjust the failure count
864     depending on whether the process ran long enough."""
865     self.running = False
866     if time.time() < self.earliest_successful_termination:
867       self.failures += 1
868     else:
869       self.failures = 0
870     logging.info("Failure count for '%s' is now %d", self.label, self.failures)
871
872
873 def relaunch_self():
874   cleanup()
875   os.execvp(sys.argv[0], sys.argv)
876
877
878 def waitpid_with_timeout(pid, deadline):
879   """Wrapper around os.waitpid() which waits until either a child process dies
880   or the deadline elapses.
881
882   Args:
883     pid: Process ID to wait for, or -1 to wait for any child process.
884     deadline: Waiting stops when time.time() exceeds this value.
885
886   Returns:
887     (pid, status): Same as for os.waitpid(), except that |pid| is 0 if no child
888     changed state within the timeout.
889
890   Raises:
891     Same as for os.waitpid().
892   """
893   while time.time() < deadline:
894     pid, status = os.waitpid(pid, os.WNOHANG)
895     if pid != 0:
896       return (pid, status)
897     time.sleep(1)
898   return (0, 0)
899
900
901 def waitpid_handle_exceptions(pid, deadline):
902   """Wrapper around os.waitpid()/waitpid_with_timeout(), which waits until
903   either a child process exits or the deadline elapses, and retries if certain
904   exceptions occur.
905
906   Args:
907     pid: Process ID to wait for, or -1 to wait for any child process.
908     deadline: If non-zero, waiting stops when time.time() exceeds this value.
909       If zero, waiting stops when a child process exits.
910
911   Returns:
912     (pid, status): Same as for waitpid_with_timeout(). |pid| is non-zero if and
913     only if a child exited during the wait.
914
915   Raises:
916     Same as for os.waitpid(), except:
917       OSError with errno==EINTR causes the wait to be retried (this can happen,
918       for example, if this parent process receives SIGHUP).
919       OSError with errno==ECHILD means there are no child processes, and so
920       this function sleeps until |deadline|. If |deadline| is zero, this is an
921       error and the OSError exception is raised in this case.
922   """
923   while True:
924     try:
925       if deadline == 0:
926         pid_result, status = os.waitpid(pid, 0)
927       else:
928         pid_result, status = waitpid_with_timeout(pid, deadline)
929       return (pid_result, status)
930     except OSError, e:
931       if e.errno == errno.EINTR:
932         continue
933       elif e.errno == errno.ECHILD:
934         now = time.time()
935         if deadline == 0:
936           # No time-limit and no child processes. This is treated as an error
937           # (see docstring).
938           raise
939         elif deadline > now:
940           time.sleep(deadline - now)
941         return (0, 0)
942       else:
943         # Anything else is an unexpected error.
944         raise
945
946
947 def main():
948   EPILOG = """This script is not intended for use by end-users.  To configure
949 Chrome Remote Desktop, please install the app from the Chrome
950 Web Store: https://chrome.google.com/remotedesktop"""
951   parser = optparse.OptionParser(
952       usage="Usage: %prog [options] [ -- [ X server options ] ]",
953       epilog=EPILOG)
954   parser.add_option("-s", "--size", dest="size", action="append",
955                     help="Dimensions of virtual desktop. This can be specified "
956                     "multiple times to make multiple screen resolutions "
957                     "available (if the Xvfb server supports this).")
958   parser.add_option("-f", "--foreground", dest="foreground", default=False,
959                     action="store_true",
960                     help="Don't run as a background daemon.")
961   parser.add_option("", "--start", dest="start", default=False,
962                     action="store_true",
963                     help="Start the host.")
964   parser.add_option("-k", "--stop", dest="stop", default=False,
965                     action="store_true",
966                     help="Stop the daemon currently running.")
967   parser.add_option("", "--get-status", dest="get_status", default=False,
968                     action="store_true",
969                     help="Prints host status")
970   parser.add_option("", "--check-running", dest="check_running", default=False,
971                     action="store_true",
972                     help="Return 0 if the daemon is running, or 1 otherwise.")
973   parser.add_option("", "--config", dest="config", action="store",
974                     help="Use the specified configuration file.")
975   parser.add_option("", "--reload", dest="reload", default=False,
976                     action="store_true",
977                     help="Signal currently running host to reload the config.")
978   parser.add_option("", "--add-user", dest="add_user", default=False,
979                     action="store_true",
980                     help="Add current user to the chrome-remote-desktop group.")
981   parser.add_option("", "--host-version", dest="host_version", default=False,
982                     action="store_true",
983                     help="Prints version of the host.")
984   (options, args) = parser.parse_args()
985
986   # Determine the filename of the host configuration and PID files.
987   if not options.config:
988     options.config = os.path.join(CONFIG_DIR, "host#%s.json" % g_host_hash)
989
990   # Check for a modal command-line option (start, stop, etc.)
991
992   if options.get_status:
993     proc = get_daemon_proc()
994     if proc is not None:
995       print "STARTED"
996     elif is_supported_platform():
997       print "STOPPED"
998     else:
999       print "NOT_IMPLEMENTED"
1000     return 0
1001
1002   # TODO(sergeyu): Remove --check-running once NPAPI plugin and NM host are
1003   # updated to always use get-status flag instead.
1004   if options.check_running:
1005     proc = get_daemon_proc()
1006     return 1 if proc is None else 0
1007
1008   if options.stop:
1009     proc = get_daemon_proc()
1010     if proc is None:
1011       print "The daemon is not currently running"
1012     else:
1013       print "Killing process %s" % proc.pid
1014       proc.terminate()
1015       try:
1016         proc.wait(timeout=30)
1017       except psutil.TimeoutExpired:
1018         print "Timed out trying to kill daemon process"
1019         return 1
1020     return 0
1021
1022   if options.reload:
1023     proc = get_daemon_proc()
1024     if proc is None:
1025       return 1
1026     proc.send_signal(signal.SIGHUP)
1027     return 0
1028
1029   if options.add_user:
1030     user = getpass.getuser()
1031     try:
1032       if user in grp.getgrnam(CHROME_REMOTING_GROUP_NAME).gr_mem:
1033         logging.info("User '%s' is already a member of '%s'." %
1034                      (user, CHROME_REMOTING_GROUP_NAME))
1035         return 0
1036     except KeyError:
1037       logging.info("Group '%s' not found." % CHROME_REMOTING_GROUP_NAME)
1038
1039     if os.getenv("DISPLAY"):
1040       sudo_command = "gksudo --description \"Chrome Remote Desktop\""
1041     else:
1042       sudo_command = "sudo"
1043     command = ("sudo -k && exec %(sudo)s -- sh -c "
1044                "\"groupadd -f %(group)s && gpasswd --add %(user)s %(group)s\"" %
1045                { 'group': CHROME_REMOTING_GROUP_NAME,
1046                  'user': user,
1047                  'sudo': sudo_command })
1048     os.execv("/bin/sh", ["/bin/sh", "-c", command])
1049     return 1
1050
1051   if options.host_version:
1052     # TODO(sergeyu): Also check RPM package version once we add RPM package.
1053     return os.system(locate_executable(HOST_BINARY_NAME) + " --version") >> 8
1054
1055   if not options.start:
1056     # If no modal command-line options specified, print an error and exit.
1057     print >> sys.stderr, EPILOG
1058     return 1
1059
1060   # If a RANDR-supporting Xvfb is not available, limit the default size to
1061   # something more sensible.
1062   if get_randr_supporting_x_server():
1063     default_sizes = DEFAULT_SIZES
1064   else:
1065     default_sizes = DEFAULT_SIZE_NO_RANDR
1066
1067   # Collate the list of sizes that XRANDR should support.
1068   if not options.size:
1069     if os.environ.has_key(DEFAULT_SIZES_ENV_VAR):
1070       default_sizes = os.environ[DEFAULT_SIZES_ENV_VAR]
1071     options.size = default_sizes.split(",")
1072
1073   sizes = []
1074   for size in options.size:
1075     size_components = size.split("x")
1076     if len(size_components) != 2:
1077       parser.error("Incorrect size format '%s', should be WIDTHxHEIGHT" % size)
1078
1079     try:
1080       width = int(size_components[0])
1081       height = int(size_components[1])
1082
1083       # Enforce minimum desktop size, as a sanity-check.  The limit of 100 will
1084       # detect typos of 2 instead of 3 digits.
1085       if width < 100 or height < 100:
1086         raise ValueError
1087     except ValueError:
1088       parser.error("Width and height should be 100 pixels or greater")
1089
1090     sizes.append((width, height))
1091
1092   # Register an exit handler to clean up session process and the PID file.
1093   atexit.register(cleanup)
1094
1095   # Load the initial host configuration.
1096   host_config = Config(options.config)
1097   try:
1098     host_config.load()
1099   except (IOError, ValueError) as e:
1100     print >> sys.stderr, "Failed to load config: " + str(e)
1101     return 1
1102
1103   # Register handler to re-load the configuration in response to signals.
1104   for s in [signal.SIGHUP, signal.SIGINT, signal.SIGTERM]:
1105     signal.signal(s, SignalHandler(host_config))
1106
1107   # Verify that the initial host configuration has the necessary fields.
1108   auth = Authentication()
1109   auth_config_valid = auth.copy_from(host_config)
1110   host = Host()
1111   host_config_valid = host.copy_from(host_config)
1112   if not host_config_valid or not auth_config_valid:
1113     logging.error("Failed to load host configuration.")
1114     return 1
1115
1116   # Determine whether a desktop is already active for the specified host
1117   # host configuration.
1118   proc = get_daemon_proc()
1119   if proc is not None:
1120     # Debian policy requires that services should "start" cleanly and return 0
1121     # if they are already running.
1122     print "Service already running."
1123     return 0
1124
1125   # Detach a separate "daemon" process to run the session, unless specifically
1126   # requested to run in the foreground.
1127   if not options.foreground:
1128     daemonize()
1129
1130   logging.info("Using host_id: " + host.host_id)
1131
1132   desktop = Desktop(sizes)
1133
1134   # Keep track of the number of consecutive failures of any child process to
1135   # run for longer than a set period of time. The script will exit after a
1136   # threshold is exceeded.
1137   # There is no point in tracking the X session process separately, since it is
1138   # launched at (roughly) the same time as the X server, and the termination of
1139   # one of these triggers the termination of the other.
1140   x_server_inhibitor = RelaunchInhibitor("X server")
1141   host_inhibitor = RelaunchInhibitor("host")
1142   all_inhibitors = [x_server_inhibitor, host_inhibitor]
1143
1144   # Don't allow relaunching the script on the first loop iteration.
1145   allow_relaunch_self = False
1146
1147   while True:
1148     # Set the backoff interval and exit if a process failed too many times.
1149     backoff_time = SHORT_BACKOFF_TIME
1150     for inhibitor in all_inhibitors:
1151       if inhibitor.failures >= MAX_LAUNCH_FAILURES:
1152         logging.error("Too many launch failures of '%s', exiting."
1153                       % inhibitor.label)
1154         return 1
1155       elif inhibitor.failures >= SHORT_BACKOFF_THRESHOLD:
1156         backoff_time = LONG_BACKOFF_TIME
1157
1158     relaunch_times = []
1159
1160     # If the session process or X server stops running (e.g. because the user
1161     # logged out), kill the other. This will trigger the next conditional block
1162     # as soon as os.waitpid() reaps its exit-code.
1163     if desktop.session_proc is None and desktop.x_proc is not None:
1164       logging.info("Terminating X server")
1165       desktop.x_proc.terminate()
1166     elif desktop.x_proc is None and desktop.session_proc is not None:
1167       logging.info("Terminating X session")
1168       desktop.session_proc.terminate()
1169     elif desktop.x_proc is None and desktop.session_proc is None:
1170       # Both processes have terminated.
1171       if (allow_relaunch_self and x_server_inhibitor.failures == 0 and
1172           host_inhibitor.failures == 0):
1173         # Since the user's desktop is already gone at this point, there's no
1174         # state to lose and now is a good time to pick up any updates to this
1175         # script that might have been installed.
1176         logging.info("Relaunching self")
1177         relaunch_self()
1178       else:
1179         # If there is a non-zero |failures| count, restarting the whole script
1180         # would lose this information, so just launch the session as normal.
1181         if x_server_inhibitor.is_inhibited():
1182           logging.info("Waiting before launching X server")
1183           relaunch_times.append(x_server_inhibitor.earliest_relaunch_time)
1184         else:
1185           logging.info("Launching X server and X session.")
1186           desktop.launch_session(args)
1187           x_server_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME,
1188                                             backoff_time)
1189           allow_relaunch_self = True
1190
1191     if desktop.host_proc is None:
1192       if host_inhibitor.is_inhibited():
1193         logging.info("Waiting before launching host process")
1194         relaunch_times.append(host_inhibitor.earliest_relaunch_time)
1195       else:
1196         logging.info("Launching host process")
1197         desktop.launch_host(host_config)
1198         host_inhibitor.record_started(MINIMUM_PROCESS_LIFETIME,
1199                                       backoff_time)
1200
1201     deadline = min(relaunch_times) if relaunch_times else 0
1202     pid, status = waitpid_handle_exceptions(-1, deadline)
1203     if pid == 0:
1204       continue
1205
1206     logging.info("wait() returned (%s,%s)" % (pid, status))
1207
1208     # When a process has terminated, and we've reaped its exit-code, any Popen
1209     # instance for that process is no longer valid. Reset any affected instance
1210     # to None.
1211     if desktop.x_proc is not None and pid == desktop.x_proc.pid:
1212       logging.info("X server process terminated")
1213       desktop.x_proc = None
1214       x_server_inhibitor.record_stopped()
1215
1216     if desktop.session_proc is not None and pid == desktop.session_proc.pid:
1217       logging.info("Session process terminated")
1218       desktop.session_proc = None
1219
1220     if desktop.host_proc is not None and pid == desktop.host_proc.pid:
1221       logging.info("Host process terminated")
1222       desktop.host_proc = None
1223       desktop.host_ready = False
1224       host_inhibitor.record_stopped()
1225
1226       # These exit-codes must match the ones used by the host.
1227       # See remoting/host/host_error_codes.h.
1228       # Delete the host or auth configuration depending on the returned error
1229       # code, so the next time this script is run, a new configuration
1230       # will be created and registered.
1231       if os.WIFEXITED(status):
1232         if os.WEXITSTATUS(status) == 100:
1233           logging.info("Host configuration is invalid - exiting.")
1234           return 0
1235         elif os.WEXITSTATUS(status) == 101:
1236           logging.info("Host ID has been deleted - exiting.")
1237           host_config.clear()
1238           host_config.save_and_log_errors()
1239           return 0
1240         elif os.WEXITSTATUS(status) == 102:
1241           logging.info("OAuth credentials are invalid - exiting.")
1242           return 0
1243         elif os.WEXITSTATUS(status) == 103:
1244           logging.info("Host domain is blocked by policy - exiting.")
1245           return 0
1246         # Nothing to do for Mac-only status 104 (login screen unsupported)
1247         elif os.WEXITSTATUS(status) == 105:
1248           logging.info("Username is blocked by policy - exiting.")
1249           return 0
1250         else:
1251           logging.info("Host exited with status %s." % os.WEXITSTATUS(status))
1252       elif os.WIFSIGNALED(status):
1253         logging.info("Host terminated by signal %s." % os.WTERMSIG(status))
1254
1255
1256 if __name__ == "__main__":
1257   logging.basicConfig(level=logging.DEBUG,
1258                       format="%(asctime)s:%(levelname)s:%(message)s")
1259   sys.exit(main())