Index: remoting/host/plugin/daemon_controller_mac.cc |
diff --git a/remoting/host/plugin/daemon_controller_mac.cc b/remoting/host/plugin/daemon_controller_mac.cc |
index 0aa7f956ad8b50fd0c74a250ff789e06747f2170..3cd165dafc0ae279fe5ab0ff1854f6ab2b031368 100644 |
--- a/remoting/host/plugin/daemon_controller_mac.cc |
+++ b/remoting/host/plugin/daemon_controller_mac.cc |
@@ -4,17 +4,53 @@ |
#include "remoting/host/plugin/daemon_controller.h" |
+#include <launch.h> |
+#include <sys/types.h> |
+ |
#include "base/basictypes.h" |
+#include "base/bind.h" |
#include "base/compiler_specific.h" |
+#include "base/file_path.h" |
+#include "base/file_util.h" |
+#include "base/json/json_writer.h" |
#include "base/logging.h" |
+#include "base/mac/authorization_util.h" |
+#include "base/mac/launchd.h" |
+#include "base/mac/mac_logging.h" |
+#include "base/mac/scoped_authorizationref.h" |
+#include "base/mac/scoped_launch_data.h" |
+#include "base/synchronization/lock.h" |
+#include "base/threading/thread.h" |
+#include "base/values.h" |
+#include "remoting/host/json_host_config.h" |
namespace remoting { |
namespace { |
+// The name of the Remoting Host service that is registered with launchd. |
+#define kServiceName "org.chromium.chromoting" |
+#define kConfigDir "/Library/PrivilegedHelperTools/" |
+ |
+// This helper script is executed as root. It is passed a command-line option |
+// (--enable or --disable), which causes it to create or remove a trigger file. |
+// The trigger file (defined in the service's plist file) informs launchd |
+// whether the Host service should be running. Creating the trigger file causes |
+// launchd to immediately start the service. Deleting the trigger file has no |
+// immediate effect, but it prevents the service from being restarted if it |
+// becomes stopped. |
+const char kStartStopTool[] = kConfigDir kServiceName ".me2me.sh"; |
+ |
+// Use a single configuration file, instead of separate "auth" and "host" files. |
+// This is because the SetConfigAndStart() API only provides a single |
+// dictionary, and splitting this into two dictionaries would require |
+// knowledge of which keys belong in which files. |
+const char kHostConfigFile[] = kConfigDir kServiceName ".json"; |
+ |
class DaemonControllerMac : public remoting::DaemonController { |
public: |
DaemonControllerMac(); |
+ virtual ~DaemonControllerMac(); |
virtual State GetState() OVERRIDE; |
virtual void GetConfig(const GetConfigCallback& callback) OVERRIDE; |
@@ -24,23 +60,78 @@ class DaemonControllerMac : public remoting::DaemonController { |
virtual void Stop() OVERRIDE; |
private: |
+ void DoGetConfig(const GetConfigCallback& callback); |
+ void DoSetConfigAndStart(scoped_ptr<base::DictionaryValue> config); |
+ void DoStop(); |
+ |
+ bool RunToolScriptAsRoot(const char* command); |
+ bool StopService(); |
+ |
+ // The API for gaining root privileges is blocking (it prompts the user for |
+ // a password). Since Start() and Stop() must not block the main thread, they |
+ // need to post their tasks to a separate thread. |
+ base::Thread auth_thread_; |
+ |
+ // This distinguishes between STATE_START_FAILED and STATE_STOPPED in |
+ // GetState(). |
+ bool start_failed_; |
+ base::Lock start_failed_lock_; |
+ |
DISALLOW_COPY_AND_ASSIGN(DaemonControllerMac); |
}; |
-DaemonControllerMac::DaemonControllerMac() { |
+DaemonControllerMac::DaemonControllerMac() |
+ : auth_thread_("Auth thread"), |
+ start_failed_(false) { |
+ auth_thread_.Start(); |
+} |
+ |
+DaemonControllerMac::~DaemonControllerMac() { |
+ // This will block if the thread is waiting on a root password prompt. There |
+ // doesn't seem to be an easy solution for this, other than to spawn a |
+ // separate process to do the root elevation. |
+ |
+ // TODO(lambroslambrou): Improve this, either by finding a way to terminate |
+ // the thread, or by moving to a separate process. |
+ auth_thread_.Stop(); |
} |
DaemonController::State DaemonControllerMac::GetState() { |
- return DaemonController::STATE_NOT_IMPLEMENTED; |
+ pid_t job_pid = base::mac::PIDForJob(kServiceName); |
+ if (job_pid < 0) { |
+ // TODO(lambroslambrou): Change this to STATE_NOT_INSTALLED when the |
+ // installation process is implemented. |
+ return DaemonController::STATE_NOT_IMPLEMENTED; |
+ } else if (job_pid == 0) { |
+ // Service is stopped, or a start attempt failed. |
+ base::AutoLock lock(start_failed_lock_); |
+ return start_failed_ ? DaemonController::STATE_START_FAILED : |
+ DaemonController::STATE_STOPPED; |
+ } else { |
+ return DaemonController::STATE_STARTED; |
+ } |
} |
void DaemonControllerMac::GetConfig(const GetConfigCallback& callback) { |
- NOTIMPLEMENTED(); |
+ // base::Unretained() is safe, since this object owns the thread and therefore |
+ // outlives it. |
+ auth_thread_.message_loop_proxy()->PostTask( |
+ FROM_HERE, |
+ base::Bind(&DaemonControllerMac::DoGetConfig, base::Unretained(this), |
+ callback)); |
} |
void DaemonControllerMac::SetConfigAndStart( |
scoped_ptr<base::DictionaryValue> config) { |
- NOTIMPLEMENTED(); |
+ { |
+ base::AutoLock lock(start_failed_lock_); |
+ start_failed_ = false; |
+ } |
+ |
+ auth_thread_.message_loop_proxy()->PostTask( |
+ FROM_HERE, |
+ base::Bind(&DaemonControllerMac::DoSetConfigAndStart, |
+ base::Unretained(this), base::Passed(&config))); |
} |
void DaemonControllerMac::SetPin(const std::string& pin) { |
@@ -48,7 +139,116 @@ void DaemonControllerMac::SetPin(const std::string& pin) { |
} |
void DaemonControllerMac::Stop() { |
- NOTIMPLEMENTED(); |
+ auth_thread_.message_loop_proxy()->PostTask( |
+ FROM_HERE, |
+ base::Bind(&DaemonControllerMac::DoStop, base::Unretained(this))); |
+} |
+ |
+void DaemonControllerMac::DoGetConfig(const GetConfigCallback& callback) { |
+ JsonHostConfig host_config(FilePath(kHostConfigFile), |
+ base::MessageLoopProxy::current()); |
+ host_config.Read(); |
+ |
+ scoped_ptr<base::DictionaryValue> config(new base::DictionaryValue()); |
+ |
+ const char* key = "host_id"; |
+ std::string value; |
+ if (host_config.GetString(key, &value)) |
+ config.get()->SetString(key, value); |
+ key = "xmpp_login"; |
+ if (host_config.GetString(key, &value)) |
+ config.get()->SetString(key, value); |
+ |
+ callback.Run(config.Pass()); |
+} |
+ |
+void DaemonControllerMac::DoSetConfigAndStart( |
+ scoped_ptr<base::DictionaryValue> config) { |
+ // JsonHostConfig doesn't provide a way to save on the current thread, wait |
+ // for completion, and know whether the save succeeded. Instead, use |
+ // base::JSONWriter directly. |
+ |
+ // TODO(lambroslambrou): Improve the JsonHostConfig interface. |
+ std::string file_content; |
+ base::JSONWriter::Write(config.get(), &file_content); |
+ if (file_util::WriteFile(FilePath(kHostConfigFile), file_content.c_str(), |
+ file_content.size()) != |
+ static_cast<int>(file_content.size())) { |
+ LOG(ERROR) << "Failed to write config file: " << kHostConfigFile; |
+ base::AutoLock lock(start_failed_lock_); |
+ start_failed_ = true; |
+ return; |
+ } |
+ |
+ // Creating the trigger file causes launchd to start the service, so the |
+ // extra step performed in DoStop() is not necessary here. |
+ if (!RunToolScriptAsRoot("--enable")) { |
+ base::AutoLock lock(start_failed_lock_); |
+ start_failed_ = true; |
+ } |
+} |
+ |
+void DaemonControllerMac::DoStop() { |
+ if (!RunToolScriptAsRoot("--disable")) |
+ return; |
+ |
+ // Deleting the trigger file does not cause launchd to stop the service. |
+ // Since the service is running for the local user's desktop (not as root), |
+ // it has to be stopped for that user. This cannot easily be done in the |
+ // shell-script running as root, so it is done here instead. |
+ StopService(); |
+} |
+ |
+bool DaemonControllerMac::RunToolScriptAsRoot(const char* command) { |
+ // TODO(lambroslambrou): Supply a localized prompt string here. |
+ base::mac::ScopedAuthorizationRef authorization( |
+ base::mac::AuthorizationCreateToRunAsRoot(CFSTR(""))); |
+ if (!authorization) { |
+ LOG(ERROR) << "Failed to get root privileges."; |
+ return false; |
+ } |
+ |
+ // TODO(lambroslambrou): Check permissions and ownership of tool script, and |
+ // use sandbox-exec to minimize exposure - http://crbug.com/120903 |
+ const char* arguments[] = { command, NULL }; |
+ int exit_status; |
+ OSStatus status = base::mac::ExecuteWithPrivilegesAndWait( |
+ authorization.get(), |
+ kStartStopTool, |
+ kAuthorizationFlagDefaults, |
+ arguments, |
+ NULL, |
+ &exit_status); |
+ if (status != errAuthorizationSuccess) { |
+ OSSTATUS_LOG(ERROR, status) << "AuthorizationExecuteWithPrivileges"; |
+ return false; |
+ } |
+ if (exit_status != 0) { |
+ LOG(ERROR) << kStartStopTool << " failed with exit status " << exit_status; |
+ return false; |
+ } |
+ |
+ return true; |
+} |
+ |
+bool DaemonControllerMac::StopService() { |
+ base::mac::ScopedLaunchData response( |
+ base::mac::MessageForJob(kServiceName, LAUNCH_KEY_STOPJOB)); |
+ if (!response) { |
+ LOG(ERROR) << "Failed to send message to launchd"; |
+ return false; |
+ } |
+ |
+ // Got a response, so check if launchd sent a non-zero error code, otherwise |
+ // assume the command was successful. |
+ if (launch_data_get_type(response.get()) == LAUNCH_DATA_ERRNO) { |
+ int error = launch_data_get_errno(response.get()); |
+ if (error) { |
+ LOG(ERROR) << "launchd returned error " << error; |
+ return false; |
+ } |
+ } |
+ return true; |
} |
} // namespace |