Initial check-in of Linux virtual Me2Me.

This CL includes a Python script, remoting.py, that sets up and runs a virtual X session and host process.  It also includes a remoting_me2me_host executable target, which is a simplified version of remoting_simple_host, using the config files that remoting.py sets up.

remoting.py uses Xvfb which needs to be installed (sudo apt-get install xvfb).


BUG=None
TEST=None


Review URL: http://codereview.chromium.org/8511029

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@110597 0039d316-1c4b-4281-b951-d872f2087c98
parent 9ab22690
// 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.
//
// This file implements a standalone host process for Me2Me, which is currently
// used for the Linux-only Virtual Me2Me build.
#include <stdlib.h>
#include <string>
#include "base/at_exit.h"
#include "base/bind.h"
#include "base/callback.h"
#include "base/command_line.h"
#include "base/file_path.h"
#include "base/file_util.h"
#include "base/logging.h"
#include "base/message_loop.h"
#include "base/threading/thread.h"
#include "build/build_config.h"
#include "crypto/nss_util.h"
#include "remoting/base/constants.h"
#include "remoting/host/chromoting_host.h"
#include "remoting/host/chromoting_host_context.h"
#include "remoting/host/desktop_environment.h"
#include "remoting/host/event_executor.h"
#include "remoting/host/heartbeat_sender.h"
#include "remoting/host/host_config.h"
#include "remoting/host/json_host_config.h"
#include "remoting/host/self_access_verifier.h"
#if defined(TOOLKIT_USES_GTK)
#include "ui/gfx/gtk_util.h"
#endif
namespace {
// These are used for parsing the config-file locations from the command line,
// and for defining the default locations if the switches are not present.
const char kAuthConfigSwitchName[] = "auth-config";
const char kHostConfigSwitchName[] = "host-config";
const FilePath::CharType kDefaultConfigDir[] =
FILE_PATH_LITERAL(".config/chrome-remote-desktop");
const FilePath::CharType kDefaultAuthConfigFile[] =
FILE_PATH_LITERAL("auth.json");
const FilePath::CharType kDefaultHostConfigFile[] =
FILE_PATH_LITERAL("host.json");
}
namespace remoting {
class HostProcess {
public:
HostProcess() {}
void InitWithCommandLine(const CommandLine* cmd_line) {
FilePath default_config_dir =
file_util::GetHomeDir().Append(kDefaultConfigDir);
if (cmd_line->HasSwitch(kAuthConfigSwitchName)) {
auth_config_path_ = cmd_line->GetSwitchValuePath(kAuthConfigSwitchName);
} else {
auth_config_path_ = default_config_dir.Append(kDefaultAuthConfigFile);
}
if (cmd_line->HasSwitch(kHostConfigSwitchName)) {
host_config_path_ = cmd_line->GetSwitchValuePath(kHostConfigSwitchName);
} else {
host_config_path_ = default_config_dir.Append(kDefaultHostConfigFile);
}
}
int Run() {
// |message_loop| is declared early so that any code we call into which
// requires a current message-loop won't complain.
// It needs to be a UI message loop to keep runloops spinning on the Mac.
MessageLoop message_loop(MessageLoop::TYPE_UI);
remoting::ChromotingHostContext context(base::MessageLoopProxy::current());
context.Start();
base::Thread file_io_thread("FileIO");
file_io_thread.Start();
if (!LoadConfig(file_io_thread.message_loop_proxy())) {
context.Stop();
return 1;
}
// Initialize AccessVerifier.
scoped_ptr<remoting::SelfAccessVerifier> self_access_verifier(
new remoting::SelfAccessVerifier());
if (!self_access_verifier->Init(host_config_)) {
context.Stop();
return 1;
}
// Create the DesktopEnvironment and ChromotingHost.
scoped_ptr<DesktopEnvironment> desktop_environment(
DesktopEnvironment::Create(&context));
host_ = ChromotingHost::Create(&context, host_config_,
desktop_environment.get(),
self_access_verifier.release(), false);
// Initialize HeartbeatSender.
scoped_ptr<remoting::HeartbeatSender> heartbeat_sender(
new remoting::HeartbeatSender(context.network_message_loop(),
host_config_));
if (!heartbeat_sender->Init()) {
context.Stop();
return 1;
}
host_->AddStatusObserver(heartbeat_sender.get());
// Run the ChromotingHost until the shutdown task is executed.
host_->Start();
message_loop.MessageLoop::Run();
// And then stop the chromoting context.
context.Stop();
file_io_thread.Stop();
host_ = NULL;
return 0;
}
private:
// Read Host config from disk, returning true if successful.
bool LoadConfig(base::MessageLoopProxy* message_loop_proxy) {
host_config_ =
new remoting::JsonHostConfig(host_config_path_, message_loop_proxy);
scoped_refptr<remoting::JsonHostConfig> auth_config =
new remoting::JsonHostConfig(auth_config_path_, message_loop_proxy);
std::string failed_path;
if (!host_config_->Read()) {
failed_path = host_config_path_.value();
} else if (!auth_config->Read()) {
failed_path = auth_config_path_.value();
}
if (!failed_path.empty()) {
LOG(ERROR) << "Failed to read configuration file " << failed_path;
return false;
}
// Copy the needed keys from |auth_config| into |host_config|.
std::string value;
auth_config->GetString(kXmppAuthTokenConfigPath, &value);
host_config_->SetString(kXmppAuthTokenConfigPath, value);
auth_config->GetString(kXmppLoginConfigPath, &value);
host_config_->SetString(kXmppLoginConfigPath, value);
// For the Me2Me host, we assume we always use the ClientLogin token for
// chromiumsync because we do not have an HTTP stack with which we can
// easily request an OAuth2 access token even if we had a RefreshToken for
// the account.
host_config_->SetString(kXmppAuthServiceConfigPath,
kChromotingTokenDefaultServiceName);
return true;
}
FilePath auth_config_path_;
FilePath host_config_path_;
scoped_refptr<remoting::JsonHostConfig> host_config_;
scoped_refptr<ChromotingHost> host_;
};
} // namespace remoting
int main(int argc, char** argv) {
CommandLine::Init(argc, argv);
// This object instance is required by Chrome code (for example,
// LazyInstance, MessageLoop).
base::AtExitManager exit_manager;
const CommandLine* cmd_line = CommandLine::ForCurrentProcess();
#if defined(TOOLKIT_USES_GTK)
// Required for any calls into GTK functions, such as the Disconnect and
// Continue windows, though these should not be used for the Me2Me case
// (crbug.com/104377).
gfx::GtkInitFromCommandLine(*cmd_line);
#endif // TOOLKIT_USES_GTK
remoting::HostProcess me2me_host;
me2me_host.InitWithCommandLine(cmd_line);
return me2me_host.Run();
}
...@@ -195,6 +195,28 @@ ...@@ -195,6 +195,28 @@
}], # 'linux_dump_symbols==1' }], # 'linux_dump_symbols==1'
], # end of 'conditions' ], # end of 'conditions'
}, # end of target 'linux_symbols' }, # end of target 'linux_symbols'
{
'target_name': 'remoting_me2me_host',
'type': 'executable',
'dependencies': [
'remoting_base',
'remoting_host',
'remoting_jingle_glue',
'../base/base.gyp:base',
'../base/base.gyp:base_i18n',
'../media/media.gyp:media',
],
'sources': [
# TODO(lambroslambrou): Remove the dependencies on the Disconnect
# and Continue windows for the Me2Me case - crbug.com/104377.
'host/continue_window.h',
'host/continue_window_linux.cc',
'host/disconnect_window_linux.cc',
'host/remoting_me2me_host.cc',
],
}, # end of target 'remoting_me2me_host'
], # end of 'targets' ], # end of 'targets'
}], # 'OS=="linux"' }], # 'OS=="linux"'
], # end of 'conditions' ], # end of 'conditions'
......
#!/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.
# Virtual Me2Me implementation. This script runs and manages the processes
# required for a Virtual Me2Me desktop, which are: X server, X desktop
# session, and Host process.
# This script is intended to run continuously as a background daemon
# process, running under an ordinary (non-root) user account.
import atexit
import getpass
import json
import logging
import os
import random
import signal
import socket
import subprocess
import sys
import time
import urllib2
import uuid
# Local modules
import gaia_auth
import keygen
REMOTING_COMMAND = "remoting_me2me_host"
SCRIPT_PATH = os.path.dirname(sys.argv[0])
if not SCRIPT_PATH:
SCRIPT_PATH = os.getcwd()
# These are relative to SCRIPT_PATH.
EXE_PATHS_TO_TRY = [
".",
"../../out/Debug",
"../../out/Release"
]
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
def locate_executable(exe_name):
for path in EXE_PATHS_TO_TRY:
exe_path = os.path.join(SCRIPT_PATH, path, exe_name)
if os.path.exists(exe_path):
return exe_path
raise Exception("Could not locate executable '%s'" % exe_name)
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):
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 save_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.
settings_file = open(self.config_file, 'w')
settings_file.write(json.dumps(data, indent=2))
settings_file.close()
class Host:
"""This manages the configuration for a host.
Callers should instantiate a Host object (passing in a filename where the
config will be kept), then should call either of the methods:
* create_config(auth): Create a new Host configuration and register it with
the Directory Service (the "auth" parameter is used to authenticate with the
Service).
* load_config(): Load a config from disk, with details of an existing Host
registration.
After calling create_config() (or making any config changes) the method
save_config() should be called to save the details to disk.
"""
server = 'www.googleapis.com'
url = 'https://' + server + '/chromoting/v1/@me/hosts'
def __init__(self, config_file):
self.config_file = config_file
def create_config(self, auth):
self.host_id = str(uuid.uuid1())
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):
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 save_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:
raise Exception("xauth failed with code %d" % ret_code)
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:
raise Exception("Could not start Xvfb.")
# 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",
"LOGNAME",
"PATH",
"SHELL",
"USER",
"USERNAME"]:
if os.environ.has_key(key):
self.child_env[key] = os.environ[key]
# 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:
raise Exception("Could not connect to Xvfb.")
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:
raise Exception("Could not start X session")
def launch_host(self):
# Start remoting host
command = locate_executable(REMOTING_COMMAND)
self.host_proc = subprocess.Popen(command, env=self.child_env)
if not self.host_proc.pid:
raise Exception("Could not start remoting host")
def main():
atexit.register(cleanup)
for s in [signal.SIGINT, signal.SIGTERM]:
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():
try:
auth.refresh_tokens()
except:
logging.error("Authentication failed.")
return 1
auth.save_config()
host = Host(os.path.join(CONFIG_DIR, "host.json"))
if not host.load_config():
host.create_config(auth)
host.save_config()
logging.info("Using host_id: " + host.host_id)
desktop = Desktop()
desktop.launch_x_server()
desktop.launch_x_session()
desktop.launch_host()
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())
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment