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