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