OLD | NEW |
---|---|
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "remoting/host/plugin/daemon_controller.h" | 5 #include "remoting/host/plugin/daemon_controller.h" |
6 | 6 |
7 #include <launch.h> | 7 #include <launch.h> |
8 #include <sys/types.h> | 8 #include <sys/types.h> |
9 | 9 |
10 #include "base/basictypes.h" | 10 #include "base/basictypes.h" |
11 #include "base/bind.h" | 11 #include "base/bind.h" |
12 #include "base/compiler_specific.h" | 12 #include "base/compiler_specific.h" |
13 #include "base/eintr_wrapper.h" | |
14 #include "base/file_path.h" | 13 #include "base/file_path.h" |
15 #include "base/file_util.h" | 14 #include "base/file_util.h" |
16 #include "base/json/json_writer.h" | 15 #include "base/json/json_writer.h" |
17 #include "base/logging.h" | 16 #include "base/logging.h" |
18 #include "base/mac/authorization_util.h" | 17 #include "base/mac/foundation_util.h" |
Jamie
2012/04/27 18:21:04
Given your comment below, is this still needed?
Lambros
2012/04/28 01:54:19
It's needed for base::mac::GetLocalDirectory().
| |
19 #include "base/mac/launchd.h" | 18 #include "base/mac/launchd.h" |
20 #include "base/mac/mac_logging.h" | 19 #include "base/mac/mac_logging.h" |
21 #include "base/mac/scoped_authorizationref.h" | 20 #include "base/mac/mac_util.h" |
22 #include "base/mac/scoped_launch_data.h" | 21 #include "base/mac/scoped_launch_data.h" |
23 #include "base/threading/thread.h" | 22 #include "base/threading/thread.h" |
24 #include "base/time.h" | 23 #include "base/time.h" |
25 #include "base/values.h" | 24 #include "base/values.h" |
26 #include "remoting/host/json_host_config.h" | 25 #include "remoting/host/json_host_config.h" |
27 | 26 |
28 namespace remoting { | 27 namespace remoting { |
29 | 28 |
30 namespace { | 29 namespace { |
31 | 30 |
31 // The NSSystemDirectories.h header has a conflicting definition of | |
32 // NSSearchPathDirectory with the one in base/mac/foundation_util.h. | |
33 // Foundation.h would work, but it can only be included from Objective-C files. | |
34 // Therefore, we define the needed constants here. | |
35 const int NSLibraryDirectory = 5; | |
Jamie
2012/04/27 18:21:04
It might be worth investigating the discrepancy as
Lambros
2012/04/28 01:54:19
The definition in base/mac/foundation_util.h is in
| |
36 | |
32 // The name of the Remoting Host service that is registered with launchd. | 37 // The name of the Remoting Host service that is registered with launchd. |
33 #define kServiceName "org.chromium.chromoting" | 38 #define kServiceName "org.chromium.chromoting" |
34 #define kConfigDir "/Library/PrivilegedHelperTools/" | 39 #define kConfigDir "/Library/PrivilegedHelperTools/" |
35 | 40 |
36 // This helper script is executed as root. It is passed a command-line option | 41 // This helper script is executed as root. It is passed a command-line option |
37 // (--enable or --disable), which causes it to create or remove a trigger file. | 42 // (--enable or --disable), which causes it to create or remove a trigger file. |
38 // The trigger file (defined in the service's plist file) informs launchd | 43 // The trigger file (defined in the service's plist file) informs launchd |
39 // whether the Host service should be running. Creating the trigger file causes | 44 // whether the Host service should be running. Creating the trigger file causes |
40 // launchd to immediately start the service. Deleting the trigger file has no | 45 // launchd to immediately start the service. Deleting the trigger file has no |
41 // immediate effect, but it prevents the service from being restarted if it | 46 // immediate effect, but it prevents the service from being restarted if it |
(...skipping 27 matching lines...) Expand all Loading... | |
69 private: | 74 private: |
70 void DoGetConfig(const GetConfigCallback& callback); | 75 void DoGetConfig(const GetConfigCallback& callback); |
71 void DoSetConfigAndStart(scoped_ptr<base::DictionaryValue> config, | 76 void DoSetConfigAndStart(scoped_ptr<base::DictionaryValue> config, |
72 const CompletionCallback& done_callback); | 77 const CompletionCallback& done_callback); |
73 void DoUpdateConfig(scoped_ptr<base::DictionaryValue> config, | 78 void DoUpdateConfig(scoped_ptr<base::DictionaryValue> config, |
74 const CompletionCallback& done_callback); | 79 const CompletionCallback& done_callback); |
75 void DoStop(const CompletionCallback& done_callback); | 80 void DoStop(const CompletionCallback& done_callback); |
76 void NotifyWhenStopped(const CompletionCallback& done_callback, | 81 void NotifyWhenStopped(const CompletionCallback& done_callback, |
77 int tries_remaining, | 82 int tries_remaining, |
78 const base::TimeDelta& sleep); | 83 const base::TimeDelta& sleep); |
84 bool ShowPreferencePane(const std::string& config_data); | |
79 | 85 |
80 bool RunToolScriptAsRoot(const char* command, const std::string& input_data); | |
81 bool StopService(); | |
82 | |
83 // The API for gaining root privileges is blocking (it prompts the user for | |
84 // a password). Since Start() and Stop() must not block the main thread, they | |
85 // need to post their tasks to a separate thread. | |
86 base::Thread auth_thread_; | 86 base::Thread auth_thread_; |
87 | 87 |
88 DISALLOW_COPY_AND_ASSIGN(DaemonControllerMac); | 88 DISALLOW_COPY_AND_ASSIGN(DaemonControllerMac); |
89 }; | 89 }; |
90 | 90 |
91 DaemonControllerMac::DaemonControllerMac() | 91 DaemonControllerMac::DaemonControllerMac() |
92 : auth_thread_("Auth thread") { | 92 : auth_thread_("Auth thread") { |
93 auth_thread_.Start(); | 93 auth_thread_.Start(); |
94 } | 94 } |
95 | 95 |
96 DaemonControllerMac::~DaemonControllerMac() { | 96 DaemonControllerMac::~DaemonControllerMac() { |
97 // This will block if the thread is waiting on a root password prompt. There | |
98 // doesn't seem to be an easy solution for this, other than to spawn a | |
99 // separate process to do the root elevation. | |
100 | |
101 // TODO(lambroslambrou): Improve this, either by finding a way to terminate | |
102 // the thread, or by moving to a separate process. | |
103 auth_thread_.Stop(); | 97 auth_thread_.Stop(); |
104 } | 98 } |
105 | 99 |
106 DaemonController::State DaemonControllerMac::GetState() { | 100 DaemonController::State DaemonControllerMac::GetState() { |
107 pid_t job_pid = base::mac::PIDForJob(kServiceName); | 101 pid_t job_pid = base::mac::PIDForJob(kServiceName); |
108 if (job_pid < 0) { | 102 if (job_pid < 0) { |
109 return DaemonController::STATE_NOT_INSTALLED; | 103 return DaemonController::STATE_NOT_INSTALLED; |
110 } else if (job_pid == 0) { | 104 } else if (job_pid == 0) { |
111 // Service is stopped, or a start attempt failed. | 105 // Service is stopped, or a start attempt failed. |
112 return DaemonController::STATE_STOPPED; | 106 return DaemonController::STATE_STOPPED; |
(...skipping 51 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
164 if (host_config.GetString(kXmppLoginConfigPath, &value)) | 158 if (host_config.GetString(kXmppLoginConfigPath, &value)) |
165 config.get()->SetString(kXmppLoginConfigPath, value); | 159 config.get()->SetString(kXmppLoginConfigPath, value); |
166 } | 160 } |
167 | 161 |
168 callback.Run(config.Pass()); | 162 callback.Run(config.Pass()); |
169 } | 163 } |
170 | 164 |
171 void DaemonControllerMac::DoSetConfigAndStart( | 165 void DaemonControllerMac::DoSetConfigAndStart( |
172 scoped_ptr<base::DictionaryValue> config, | 166 scoped_ptr<base::DictionaryValue> config, |
173 const CompletionCallback& done_callback) { | 167 const CompletionCallback& done_callback) { |
174 std::string file_content; | 168 std::string config_data; |
175 base::JSONWriter::Write(config.get(), &file_content); | 169 base::JSONWriter::Write(config.get(), &config_data); |
176 | 170 |
177 // Creating the trigger file causes launchd to start the service, so the | 171 bool result = ShowPreferencePane(config_data); |
178 // extra step performed in DoStop() is not necessary here. | |
179 bool result = RunToolScriptAsRoot("--enable", file_content); | |
180 done_callback.Run(result ? RESULT_OK : RESULT_FAILED); | 172 done_callback.Run(result ? RESULT_OK : RESULT_FAILED); |
181 } | 173 } |
182 | 174 |
183 void DaemonControllerMac::DoUpdateConfig( | 175 void DaemonControllerMac::DoUpdateConfig( |
184 scoped_ptr<base::DictionaryValue> config, | 176 scoped_ptr<base::DictionaryValue> config, |
185 const CompletionCallback& done_callback) { | 177 const CompletionCallback& done_callback) { |
186 FilePath config_file_path(kHostConfigFile); | 178 FilePath config_file_path(kHostConfigFile); |
187 JsonHostConfig config_file(config_file_path); | 179 JsonHostConfig config_file(config_file_path); |
188 if (!config_file.Read()) { | 180 if (!config_file.Read()) { |
189 done_callback.Run(RESULT_FAILED); | 181 done_callback.Run(RESULT_FAILED); |
190 } | 182 } |
191 for (DictionaryValue::key_iterator key(config->begin_keys()); | 183 for (DictionaryValue::key_iterator key(config->begin_keys()); |
192 key != config->end_keys(); ++key) { | 184 key != config->end_keys(); ++key) { |
193 std::string value; | 185 std::string value; |
194 if (!config->GetString(*key, &value)) { | 186 if (!config->GetString(*key, &value)) { |
195 LOG(ERROR) << *key << " is not a string."; | 187 LOG(ERROR) << *key << " is not a string."; |
196 done_callback.Run(RESULT_FAILED); | 188 done_callback.Run(RESULT_FAILED); |
197 } | 189 } |
198 config_file.SetString(*key, value); | 190 config_file.SetString(*key, value); |
199 } | 191 } |
200 | 192 |
201 std::string file_content = config_file.GetSerializedData(); | 193 std::string config_data = config_file.GetSerializedData(); |
202 bool success = RunToolScriptAsRoot("--save-config", file_content); | 194 bool success = ShowPreferencePane(config_data); |
203 | 195 |
204 done_callback.Run(success ? RESULT_OK : RESULT_FAILED); | 196 done_callback.Run(success ? RESULT_OK : RESULT_FAILED); |
205 pid_t job_pid = base::mac::PIDForJob(kServiceName); | 197 } |
206 if (job_pid > 0) { | 198 |
207 kill(job_pid, SIGHUP); | 199 bool DaemonControllerMac::ShowPreferencePane(const std::string& config_data) { |
200 if (!config_data.empty()) { | |
201 FilePath config_path; | |
202 if (!file_util::GetTempDir(&config_path)) { | |
203 LOG(ERROR) << "Failed to get filename for saving configuration data."; | |
204 return false; | |
205 } | |
206 config_path = config_path.Append(kServiceName ".json"); | |
207 | |
208 int written = file_util::WriteFile(config_path, config_data.data(), | |
209 config_data.size()); | |
210 if (written != static_cast<int>(config_data.size())) { | |
211 LOG(ERROR) << "Failed to save configuration data to: " | |
212 << config_path.value(); | |
213 return false; | |
214 } | |
208 } | 215 } |
216 | |
217 FilePath pane_path; | |
218 // TODO(lambroslambrou): Use NSPreferencePanesDirectory once we start | |
219 // building against SDK 10.6. | |
220 if (!base::mac::GetLocalDirectory(NSLibraryDirectory, &pane_path)) { | |
221 LOG(ERROR) << "Failed to get directory for local preference panes."; | |
222 return false; | |
223 } | |
224 pane_path = pane_path.Append("PreferencePanes") | |
225 .Append(kServiceName ".prefPane"); | |
226 | |
227 FSRef pane_path_ref; | |
228 if (!base::mac::FSRefFromPath(pane_path.value(), &pane_path_ref)) { | |
229 LOG(ERROR) << "Failed to create FSRef"; | |
230 return false; | |
231 } | |
232 OSStatus status = LSOpenFSRef(&pane_path_ref, NULL); | |
233 if (status != noErr) { | |
234 OSSTATUS_LOG(ERROR, status) << "LSOpenFSRef failed for path: " | |
235 << pane_path.value(); | |
236 return false; | |
237 } | |
238 | |
239 CFNotificationCenterRef center = | |
240 CFNotificationCenterGetDistributedCenter(); | |
241 CFNotificationCenterPostNotification(center, CFSTR(kServiceName), NULL, NULL, | |
242 TRUE); | |
243 return true; | |
209 } | 244 } |
210 | 245 |
211 void DaemonControllerMac::DoStop(const CompletionCallback& done_callback) { | 246 void DaemonControllerMac::DoStop(const CompletionCallback& done_callback) { |
212 if (!RunToolScriptAsRoot("--disable", "")) { | 247 if (!ShowPreferencePane("")) { |
213 done_callback.Run(RESULT_FAILED); | 248 done_callback.Run(RESULT_FAILED); |
214 return; | 249 return; |
215 } | 250 } |
216 | 251 |
217 // Deleting the trigger file does not cause launchd to stop the service. | |
218 // Since the service is running for the local user's desktop (not as root), | |
219 // it has to be stopped for that user. This cannot easily be done in the | |
220 // shell-script running as root, so it is done here instead. | |
221 if (!StopService()) { | |
222 done_callback.Run(RESULT_FAILED); | |
223 return; | |
224 } | |
225 | |
226 // StopService does not wait for the stop to take effect, so we can't return | |
227 // immediately. Instead, we wait up to 10s. | |
228 NotifyWhenStopped(done_callback, | 252 NotifyWhenStopped(done_callback, |
229 kStopWaitRetryLimit, | 253 kStopWaitRetryLimit, |
230 base::TimeDelta::FromMilliseconds(kStopWaitTimeout)); | 254 base::TimeDelta::FromMilliseconds(kStopWaitTimeout)); |
231 } | 255 } |
232 | 256 |
233 void DaemonControllerMac::NotifyWhenStopped( | 257 void DaemonControllerMac::NotifyWhenStopped( |
234 const CompletionCallback& done_callback, | 258 const CompletionCallback& done_callback, |
235 int tries_remaining, | 259 int tries_remaining, |
236 const base::TimeDelta& sleep) { | 260 const base::TimeDelta& sleep) { |
237 if (GetState() == DaemonController::STATE_STOPPED) { | 261 if (GetState() == DaemonController::STATE_STOPPED) { |
238 done_callback.Run(RESULT_OK); | 262 done_callback.Run(RESULT_OK); |
239 } else if (tries_remaining == 0) { | 263 } else if (tries_remaining == 0) { |
240 done_callback.Run(RESULT_FAILED); | 264 done_callback.Run(RESULT_FAILED); |
241 } else { | 265 } else { |
242 auth_thread_.message_loop_proxy()->PostDelayedTask( | 266 auth_thread_.message_loop_proxy()->PostDelayedTask( |
243 FROM_HERE, | 267 FROM_HERE, |
244 base::Bind(&DaemonControllerMac::NotifyWhenStopped, | 268 base::Bind(&DaemonControllerMac::NotifyWhenStopped, |
245 base::Unretained(this), | 269 base::Unretained(this), |
246 done_callback, | 270 done_callback, |
247 tries_remaining - 1, | 271 tries_remaining - 1, |
248 sleep), | 272 sleep), |
249 sleep); | 273 sleep); |
250 } | 274 } |
251 } | 275 } |
252 | 276 |
253 bool DaemonControllerMac::RunToolScriptAsRoot(const char* command, | |
254 const std::string& input_data) { | |
255 // TODO(lambroslambrou): Supply a localized prompt string here. | |
256 base::mac::ScopedAuthorizationRef authorization( | |
257 base::mac::AuthorizationCreateToRunAsRoot(CFSTR(""))); | |
258 if (!authorization) { | |
259 LOG(ERROR) << "Failed to get root privileges."; | |
260 return false; | |
261 } | |
262 | |
263 if (!file_util::VerifyPathControlledByAdmin(FilePath(kStartStopTool))) { | |
264 LOG(ERROR) << "Security check failed for: " << kStartStopTool; | |
265 return false; | |
266 } | |
267 | |
268 // TODO(lambroslambrou): Use sandbox-exec to minimize exposure - | |
269 // http://crbug.com/120903 | |
270 const char* arguments[] = { command, NULL }; | |
271 FILE* pipe = NULL; | |
272 pid_t pid; | |
273 OSStatus status = base::mac::ExecuteWithPrivilegesAndGetPID( | |
274 authorization.get(), | |
275 kStartStopTool, | |
276 kAuthorizationFlagDefaults, | |
277 arguments, | |
278 &pipe, | |
279 &pid); | |
280 if (status != errAuthorizationSuccess) { | |
281 OSSTATUS_LOG(ERROR, status) << "AuthorizationExecuteWithPrivileges"; | |
282 return false; | |
283 } | |
284 if (pid == -1) { | |
285 LOG(ERROR) << "Failed to get child PID"; | |
286 return false; | |
287 } | |
288 | |
289 DCHECK(pipe); | |
290 if (!input_data.empty()) { | |
291 size_t bytes_written = fwrite(input_data.data(), sizeof(char), | |
292 input_data.size(), pipe); | |
293 // According to the fwrite manpage, a partial count is returned only if a | |
294 // write error has occurred. | |
295 if (bytes_written != input_data.size()) { | |
296 LOG(ERROR) << "Failed to write data to child process"; | |
297 } | |
298 // Need to close, since the child waits for EOF on its stdin. | |
299 if (fclose(pipe) != 0) { | |
300 PLOG(ERROR) << "fclose"; | |
301 } | |
302 } | |
303 | |
304 int exit_status; | |
305 pid_t wait_result = HANDLE_EINTR(waitpid(pid, &exit_status, 0)); | |
306 if (wait_result != pid) { | |
307 PLOG(ERROR) << "waitpid"; | |
308 return false; | |
309 } | |
310 if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) { | |
311 return true; | |
312 } else { | |
313 LOG(ERROR) << kStartStopTool << " failed with exit status " << exit_status; | |
314 return false; | |
315 } | |
316 } | |
317 | |
318 bool DaemonControllerMac::StopService() { | |
319 base::mac::ScopedLaunchData response( | |
320 base::mac::MessageForJob(kServiceName, LAUNCH_KEY_STOPJOB)); | |
321 if (!response) { | |
322 LOG(ERROR) << "Failed to send message to launchd"; | |
323 return false; | |
324 } | |
325 | |
326 // Got a response, so check if launchd sent a non-zero error code, otherwise | |
327 // assume the command was successful. | |
328 if (launch_data_get_type(response.get()) == LAUNCH_DATA_ERRNO) { | |
329 int error = launch_data_get_errno(response.get()); | |
330 if (error) { | |
331 LOG(ERROR) << "launchd returned error " << error; | |
332 return false; | |
333 } | |
334 } | |
335 return true; | |
336 } | |
337 | |
338 } // namespace | 277 } // namespace |
339 | 278 |
340 scoped_ptr<DaemonController> remoting::DaemonController::Create() { | 279 scoped_ptr<DaemonController> remoting::DaemonController::Create() { |
341 return scoped_ptr<DaemonController>(new DaemonControllerMac()); | 280 return scoped_ptr<DaemonController>(new DaemonControllerMac()); |
342 } | 281 } |
343 | 282 |
344 } // namespace remoting | 283 } // namespace remoting |
OLD | NEW |