Chromium Code Reviews| Index: remoting/tools/remoting.py |
| diff --git a/remoting/tools/remoting.py b/remoting/tools/remoting.py |
| new file mode 100755 |
| index 0000000000000000000000000000000000000000..2bb135551cb48a0df960c0b7913bf3a915af6310 |
| --- /dev/null |
| +++ b/remoting/tools/remoting.py |
| @@ -0,0 +1,279 @@ |
| +#!/usr/bin/env python |
| +# Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| +# Use of this source code is governed by a BSD-style license that can be |
| +# found in the LICENSE file. |
| + |
|
Wez
2011/11/11 01:32:32
Add a description of what this script is for.
Wez
2011/11/11 01:32:32
I think the script name should describe what it do
Lambros
2011/11/17 01:07:05
Done.
Lambros
2011/11/17 01:07:05
Done.
|
| +import atexit |
| +import getpass |
| +import json |
| +import logging |
| +import os |
| +import random |
| +import signal |
| +import socket |
| +import subprocess |
| +import sys |
| +import time |
| +import urllib2 |
| + |
| +# Local modules |
| +import gaia_auth |
| +import keygen |
| + |
| +REMOTING_COMMAND = ["../../out/Debug/remoting_me2me_host"] |
|
Sergey Ulanov
2011/11/11 05:33:02
Can we detect this path similar to how keygen modu
Lambros
2011/11/17 01:07:05
Done.
|
| + |
| +CONFIG_DIR = os.path.expanduser("~/.config/chrome-remote-desktop") |
| + |
| +X_LOCK_FILE_TEMPLATE = "/tmp/.X%d-lock" |
| +FIRST_X_DISPLAY_NUMBER = 20 |
| + |
| +X_AUTH_FILE = os.path.expanduser("~/.Xauthority") |
| +os.environ["XAUTHORITY"] = X_AUTH_FILE |
| + |
| +g_desktops = [] |
| + |
| +class Authentication: |
| + """Manage authentication tokens for Chromoting/xmpp""" |
| + def __init__(self, config_file): |
| + self.config_file = config_file |
| + |
| + def refresh_tokens(self): |
| + print "Email:", |
| + self.login = raw_input() |
| + password = getpass.getpass("Password: ") |
| + |
| + chromoting_auth = gaia_auth.GaiaAuthenticator('chromoting') |
| + self.chromoting_auth_token = chromoting_auth.authenticate(self.login, |
| + password) |
| + |
| + xmpp_authenticator = gaia_auth.GaiaAuthenticator('chromiumsync') |
| + self.xmpp_auth_token = xmpp_authenticator.authenticate(self.login, |
| + password) |
| + |
| + def load_config(self): |
|
Wez
2011/11/11 01:32:32
nit: Why load/write_config rather than read/write_
Lambros
2011/11/17 01:07:05
Done (load/save).
|
| + try: |
| + settings_file = open(self.config_file, 'r') |
| + data = json.load(settings_file) |
| + settings_file.close() |
| + self.login = data["xmpp_login"] |
| + self.chromoting_auth_token = data["chromoting_auth_token"] |
| + self.xmpp_auth_token = data["xmpp_auth_token"] |
| + except: |
| + return False |
| + return True |
| + |
| + def write_config(self): |
| + data = { |
| + "xmpp_login": self.login, |
| + "chromoting_auth_token": self.chromoting_auth_token, |
| + "xmpp_auth_token": self.xmpp_auth_token, |
| + } |
| + os.umask(0066) # Set permission mask for created file. |
|
Wez
2011/11/11 01:32:32
Does this fail with an error return code, or an ex
Lambros
2011/11/17 01:07:05
umask() cannot fail, but the open/write/close call
|
| + settings_file = open(self.config_file, 'w') |
| + settings_file.write(json.dumps(data, indent=2)) |
| + settings_file.close() |
| + |
| + |
| +class Host: |
| + """Create a new, or manage an existing, host registration.""" |
|
Wez
2011/11/11 01:32:32
Surely this object effectively _is_ a virtual Me2M
Lambros
2011/11/17 01:07:05
Reworded comment.
|
| + |
| + server = 'www.googleapis.com' |
| + url = 'https://' + server + '/chromoting/v1/@me/hosts' |
| + |
| + def __init__(self, config_file): |
| + self.config_file = config_file |
| + |
| + @staticmethod |
| + def random_uuid(): |
| + return ("%04x%04x-%04x-%04x-%04x-%04x%04x%04x" % |
| + tuple(map(lambda x: random.randrange(0, 0x10000), range(8)))) |
|
Wez
2011/11/11 01:32:32
Replace this with uuid.uuid1()?
Lambros
2011/11/17 01:07:05
Done.
|
| + |
| + def register_new(self, auth): |
|
Wez
2011/11/11 01:32:32
Split this method down into generate_config(), reg
Wez
2011/11/11 01:32:32
Rename this method create_host() or create_config(
Lambros
2011/11/17 01:07:05
Done.
|
| + self.host_id = self.random_uuid() |
| + logging.info("HostId: " + self.host_id) |
| + self.host_name = socket.gethostname() |
| + logging.info("HostName: " + self.host_name) |
| + |
| + logging.info("Generating RSA key pair...") |
| + (self.private_key, public_key) = keygen.generateRSAKeyPair() |
| + logging.info("Done") |
| + |
| + json_data = { |
| + "data": { |
| + "hostId": self.host_id, |
| + "hostName": self.host_name, |
| + "publicKey": public_key, |
| + } |
| + } |
| + params = json.dumps(json_data) |
| + headers = { |
| + "Authorization": "GoogleLogin auth=" + auth.chromoting_auth_token, |
| + "Content-Type": "application/json", |
| + } |
| + |
| + request = urllib2.Request(self.url, params, headers) |
| + opener = urllib2.OpenerDirector() |
| + opener.add_handler(urllib2.HTTPDefaultErrorHandler()) |
| + |
| + logging.info("Registering host with directory service...") |
| + try: |
| + res = urllib2.urlopen(request) |
| + data = res.read() |
| + except urllib2.HTTPError, err: |
| + logging.error("Directory returned error: " + str(err)) |
| + logging.error(err.read()) |
| + sys.exit(1) |
| + logging.info("Done") |
| + |
| + def load_config(self): |
|
Wez
2011/11/11 01:32:32
load vs read / save vs write
Lambros
2011/11/17 01:07:05
Done.
|
| + try: |
| + settings_file = open(self.config_file, 'r') |
| + data = json.load(settings_file) |
| + settings_file.close() |
| + self.host_id = data["host_id"] |
| + self.host_name = data["host_name"] |
| + self.private_key = data["private_key"] |
| + except: |
| + return False |
| + return True |
| + |
| + def write_config(self): |
| + data = { |
| + "host_id": self.host_id, |
| + "host_name": self.host_name, |
| + "private_key": self.private_key, |
| + } |
| + os.umask(0066) # Set permission mask for created file. |
| + settings_file = open(self.config_file, 'w') |
| + settings_file.write(json.dumps(data, indent=2)) |
| + settings_file.close() |
| + |
| + |
| +def cleanup(): |
| + logging.info("Cleanup.") |
| + |
| + for desktop in g_desktops: |
| + if desktop.x_proc: |
| + logging.info("Terminating Xvfb") |
| + desktop.x_proc.terminate() |
| + |
| +def signal_handler(signum, stackframe): |
| + # Exit cleanly so the atexit handler, cleanup(), gets called. |
| + raise SystemExit |
| + |
| + |
| +class Desktop: |
| + """Manage a single virtual desktop""" |
| + def __init__(self): |
| + self.x_proc = None |
| + g_desktops.append(self) |
| + |
| + @staticmethod |
| + def get_unused_display_number(): |
| + """Return a candidate display number for which there is currently no |
| + X Server lock file""" |
| + display = FIRST_X_DISPLAY_NUMBER |
| + while os.path.exists(X_LOCK_FILE_TEMPLATE % display): |
| + display += 1 |
| + return display |
| + |
| + def launch_x_server(self): |
| + display = self.get_unused_display_number() |
| + ret_code = subprocess.call("xauth add :%d . `mcookie`" % display, |
| + shell=True) |
| + if ret_code != 0: |
| + logging.error("xauth failed with code %d" % ret_code) |
| + return 1 |
| + logging.info("Starting Xvfb on display :%d" % display); |
| + self.x_proc = subprocess.Popen(["Xvfb", ":%d" % display, |
| + "-auth", X_AUTH_FILE, |
| + "-nolisten", "tcp", |
| + "-screen", "0", "1024x768x24", |
| + ]) |
| + if not self.x_proc.pid: |
| + logging.info("Could not start Xvfb.") |
| + return 1 |
| + |
| + # Create clean environment for new session, so it is cleanly separated from |
| + # the user's console X session. |
| + self.child_env = {"DISPLAY": ":%d" % display} |
| + for key in ["HOME", "PATH"]: |
| + self.child_env[key] = os.environ[key] |
|
Wez
2011/11/11 01:32:32
Are these really all we need for a valid environme
Lambros
2011/11/17 01:07:05
This is essentially what GNOME Display Manager doe
|
| + |
| + # Wait for X to be active. |
| + for test in range(5): |
| + proc = subprocess.Popen("xdpyinfo > /dev/null", env=self.child_env, |
| + shell=True) |
| + pid, retcode = os.waitpid(proc.pid, 0) |
| + if retcode == 0: |
| + break |
| + time.sleep(0.5) |
| + if retcode != 0: |
| + logging.info("Could not connect to Xvfb.") |
| + return 1 |
| + else: |
| + logging.info("Xvfb is active.") |
| + |
| + def launch_x_session(self): |
| + # Start desktop session |
| + session_proc = subprocess.Popen("/etc/X11/Xsession", |
| + cwd=os.environ["HOME"], |
| + env=self.child_env) |
| + if not session_proc.pid: |
| + logging.info("Could not start X session") |
| + return 1 |
| + |
| + def launch_host(self): |
| + # Start remoting host |
| + self.host_proc = subprocess.Popen(REMOTING_COMMAND, env=self.child_env) |
| + if not self.host_proc.pid: |
| + logging.info("Could not start remoting host") |
| + return 1 |
| + |
| + |
| +def main(): |
| + atexit.register(cleanup) |
|
Wez
2011/11/11 01:32:32
This means at present that killing the script will
Lambros
2011/11/17 01:07:05
Future CL - a bit complex to include in here. Not
Wez
2011/11/17 02:19:28
That page is really talking about how self-daemoni
|
| + |
| + for s in ( |
| + signal.SIGINT, |
| + signal.SIGTERM, |
| + ): |
|
Wez
2011/11/11 01:32:32
Could this be a single line? It looks strange lai
Lambros
2011/11/17 01:07:05
Done. I think that list is exhaustive. Trapping
|
| + signal.signal(s, signal_handler) |
| + |
| + # Ensure full path to config directory exists. |
| + if not os.path.exists(CONFIG_DIR): |
| + os.makedirs(CONFIG_DIR, mode=0700) |
| + |
| + auth = Authentication(os.path.join(CONFIG_DIR, "auth.json")) |
| + if not auth.load_config(): |
| + auth.refresh_tokens() |
| + auth.write_config() |
| + |
| + host = Host(os.path.join(CONFIG_DIR, "host.json")) |
| + |
| + if not host.load_config(): |
| + host.register_new(auth) |
| + host.write_config() |
|
Wez
2011/11/11 01:32:32
Support for multiple Host configurations, and re-s
Lambros
2011/11/17 01:07:05
I think so.
|
| + |
| + logging.info("Using host_id: " + host.host_id) |
| + |
| + desktop = Desktop() |
| + desktop.launch_x_server() |
| + desktop.launch_x_session() |
| + desktop.launch_host() |
|
Wez
2011/11/11 01:32:32
If one of these fails, it looks like you'll get er
Lambros
2011/11/17 01:07:05
Turned these into Exceptions, which will kill the
|
| + |
| + while True: |
| + pid, status = os.wait() |
| + logging.info("wait() returned (%s,%s)" % (pid, status)) |
| + |
| + if pid == desktop.x_proc.pid: |
| + logging.info("X server process terminated with code %d", status) |
| + break |
| + |
| + if pid == desktop.host_proc.pid: |
| + logging.info("Host process terminated, relaunching") |
| + desktop.launch_host() |
| + |
| +if __name__ == "__main__": |
| + logging.basicConfig(level=logging.DEBUG) |
| + sys.exit(main()) |