1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include <CoreFoundation/CoreFoundation.h>
6
7#include "remoting/host/setup/daemon_controller_delegate_mac.h"
8
9#include <launch.h>
10#include <stdio.h>
11#include <sys/types.h>
12
13#include "base/basictypes.h"
14#include "base/bind.h"
15#include "base/compiler_specific.h"
16#include "base/files/file_path.h"
17#include "base/files/file_util.h"
18#include "base/json/json_writer.h"
19#include "base/logging.h"
20#include "base/mac/foundation_util.h"
21#include "base/mac/launchd.h"
22#include "base/mac/mac_logging.h"
23#include "base/mac/mac_util.h"
24#include "base/mac/scoped_launch_data.h"
25#include "base/time/time.h"
26#include "base/values.h"
27#include "remoting/host/constants_mac.h"
28#include "remoting/host/json_host_config.h"
29#include "remoting/host/usage_stats_consent.h"
30
31namespace remoting {
32
33DaemonControllerDelegateMac::DaemonControllerDelegateMac() {
34}
35
36DaemonControllerDelegateMac::~DaemonControllerDelegateMac() {
37  DeregisterForPreferencePaneNotifications();
38}
39
40DaemonController::State DaemonControllerDelegateMac::GetState() {
41  pid_t job_pid = base::mac::PIDForJob(kServiceName);
42  if (job_pid < 0) {
43    return DaemonController::STATE_NOT_INSTALLED;
44  } else if (job_pid == 0) {
45    // Service is stopped, or a start attempt failed.
46    return DaemonController::STATE_STOPPED;
47  } else {
48    return DaemonController::STATE_STARTED;
49  }
50}
51
52scoped_ptr<base::DictionaryValue> DaemonControllerDelegateMac::GetConfig() {
53  base::FilePath config_path(kHostConfigFilePath);
54  JsonHostConfig host_config(config_path);
55  scoped_ptr<base::DictionaryValue> config;
56
57  if (host_config.Read()) {
58    config.reset(new base::DictionaryValue());
59    std::string value;
60    if (host_config.GetString(kHostIdConfigPath, &value))
61      config.get()->SetString(kHostIdConfigPath, value);
62    if (host_config.GetString(kXmppLoginConfigPath, &value))
63      config.get()->SetString(kXmppLoginConfigPath, value);
64  }
65
66  return config.Pass();
67}
68
69void DaemonControllerDelegateMac::InstallHost(
70    const DaemonController::CompletionCallback& done) {
71  NOTREACHED();
72}
73
74void DaemonControllerDelegateMac::SetConfigAndStart(
75    scoped_ptr<base::DictionaryValue> config,
76    bool consent,
77    const DaemonController::CompletionCallback& done) {
78  config->SetBoolean(kUsageStatsConsentConfigPath, consent);
79  std::string config_data;
80  base::JSONWriter::Write(config.get(), &config_data);
81  ShowPreferencePane(config_data, done);
82}
83
84void DaemonControllerDelegateMac::UpdateConfig(
85    scoped_ptr<base::DictionaryValue> config,
86    const DaemonController::CompletionCallback& done) {
87  base::FilePath config_file_path(kHostConfigFilePath);
88  JsonHostConfig config_file(config_file_path);
89  if (!config_file.Read()) {
90    done.Run(DaemonController::RESULT_FAILED);
91    return;
92  }
93  if (!config_file.CopyFrom(config.get())) {
94    LOG(ERROR) << "Failed to update configuration.";
95    done.Run(DaemonController::RESULT_FAILED);
96    return;
97  }
98
99  std::string config_data = config_file.GetSerializedData();
100  ShowPreferencePane(config_data, done);
101}
102
103void DaemonControllerDelegateMac::Stop(
104    const DaemonController::CompletionCallback& done) {
105  ShowPreferencePane("", done);
106}
107
108void DaemonControllerDelegateMac::SetWindow(void* window_handle) {
109  // noop
110}
111
112std::string DaemonControllerDelegateMac::GetVersion() {
113  std::string version = "";
114  std::string command_line = remoting::kHostHelperScriptPath;
115  command_line += " --host-version";
116  FILE* script_output = popen(command_line.c_str(), "r");
117  if (script_output) {
118    char buffer[100];
119    char* result = fgets(buffer, sizeof(buffer), script_output);
120    pclose(script_output);
121    if (result) {
122      // The string is guaranteed to be null-terminated, but probably contains
123      // a newline character, which we don't want.
124      for (int i = 0; result[i]; ++i) {
125        if (result[i] < ' ') {
126          result[i] = 0;
127          break;
128        }
129      }
130      version = result;
131    }
132  }
133
134  return version;
135}
136
137DaemonController::UsageStatsConsent
138DaemonControllerDelegateMac::GetUsageStatsConsent() {
139  DaemonController::UsageStatsConsent consent;
140  consent.supported = true;
141  consent.allowed = false;
142  // set_by_policy is not yet supported.
143  consent.set_by_policy = false;
144
145  base::FilePath config_file_path(kHostConfigFilePath);
146  JsonHostConfig host_config(config_file_path);
147  if (host_config.Read()) {
148    host_config.GetBoolean(kUsageStatsConsentConfigPath, &consent.allowed);
149  }
150
151  return consent;
152}
153
154void DaemonControllerDelegateMac::ShowPreferencePane(
155    const std::string& config_data,
156    const DaemonController::CompletionCallback& done) {
157  if (DoShowPreferencePane(config_data)) {
158    RegisterForPreferencePaneNotifications(done);
159  } else {
160    done.Run(DaemonController::RESULT_FAILED);
161  }
162}
163
164// CFNotificationCenterAddObserver ties the thread on which distributed
165// notifications are received to the one on which it is first called.
166// This is safe because HostNPScriptObject::InvokeAsyncResultCallback
167// bounces the invocation to the correct thread, so it doesn't matter
168// which thread CompletionCallbacks are called on.
169void DaemonControllerDelegateMac::RegisterForPreferencePaneNotifications(
170    const DaemonController::CompletionCallback& done) {
171  // We can only have one callback registered at a time. This is enforced by the
172  // UX flow of the web-app.
173  DCHECK(current_callback_.is_null());
174  current_callback_ = done;
175
176  CFNotificationCenterAddObserver(
177      CFNotificationCenterGetDistributedCenter(),
178      this,
179      &DaemonControllerDelegateMac::PreferencePaneCallback,
180      CFSTR(UPDATE_SUCCEEDED_NOTIFICATION_NAME),
181      NULL,
182      CFNotificationSuspensionBehaviorDeliverImmediately);
183  CFNotificationCenterAddObserver(
184      CFNotificationCenterGetDistributedCenter(),
185      this,
186      &DaemonControllerDelegateMac::PreferencePaneCallback,
187      CFSTR(UPDATE_FAILED_NOTIFICATION_NAME),
188      NULL,
189      CFNotificationSuspensionBehaviorDeliverImmediately);
190}
191
192void DaemonControllerDelegateMac::DeregisterForPreferencePaneNotifications() {
193  CFNotificationCenterRemoveObserver(
194      CFNotificationCenterGetDistributedCenter(),
195      this,
196      CFSTR(UPDATE_SUCCEEDED_NOTIFICATION_NAME),
197      NULL);
198  CFNotificationCenterRemoveObserver(
199      CFNotificationCenterGetDistributedCenter(),
200      this,
201      CFSTR(UPDATE_FAILED_NOTIFICATION_NAME),
202      NULL);
203}
204
205void DaemonControllerDelegateMac::PreferencePaneCallbackDelegate(
206    CFStringRef name) {
207  DaemonController::AsyncResult result = DaemonController::RESULT_FAILED;
208  if (CFStringCompare(name, CFSTR(UPDATE_SUCCEEDED_NOTIFICATION_NAME), 0) ==
209          kCFCompareEqualTo) {
210    result = DaemonController::RESULT_OK;
211  } else if (CFStringCompare(name, CFSTR(UPDATE_FAILED_NOTIFICATION_NAME), 0) ==
212          kCFCompareEqualTo) {
213    result = DaemonController::RESULT_FAILED;
214  } else {
215    LOG(WARNING) << "Ignoring unexpected notification: " << name;
216    return;
217  }
218
219  DCHECK(!current_callback_.is_null());
220  DaemonController::CompletionCallback done = current_callback_;
221  current_callback_.Reset();
222  done.Run(result);
223
224  DeregisterForPreferencePaneNotifications();
225}
226
227// static
228bool DaemonControllerDelegateMac::DoShowPreferencePane(
229    const std::string& config_data) {
230  if (!config_data.empty()) {
231    base::FilePath config_path;
232    if (!base::GetTempDir(&config_path)) {
233      LOG(ERROR) << "Failed to get filename for saving configuration data.";
234      return false;
235    }
236    config_path = config_path.Append(kHostConfigFileName);
237
238    int written = base::WriteFile(config_path, config_data.data(),
239                                       config_data.size());
240    if (written != static_cast<int>(config_data.size())) {
241      LOG(ERROR) << "Failed to save configuration data to: "
242                 << config_path.value();
243      return false;
244    }
245  }
246
247  base::FilePath pane_path;
248  // TODO(lambroslambrou): Use NSPreferencePanesDirectory once we start
249  // building against SDK 10.6.
250  if (!base::mac::GetLocalDirectory(NSLibraryDirectory, &pane_path)) {
251    LOG(ERROR) << "Failed to get directory for local preference panes.";
252    return false;
253  }
254  pane_path = pane_path.Append("PreferencePanes").Append(kPrefPaneFileName);
255
256  FSRef pane_path_ref;
257  if (!base::mac::FSRefFromPath(pane_path.value(), &pane_path_ref)) {
258    LOG(ERROR) << "Failed to create FSRef";
259    return false;
260  }
261  OSStatus status = LSOpenFSRef(&pane_path_ref, NULL);
262  if (status != noErr) {
263    OSSTATUS_LOG(ERROR, status) << "LSOpenFSRef failed for path: "
264                                << pane_path.value();
265    return false;
266  }
267
268  CFNotificationCenterRef center =
269      CFNotificationCenterGetDistributedCenter();
270  base::ScopedCFTypeRef<CFStringRef> service_name(CFStringCreateWithCString(
271      kCFAllocatorDefault, remoting::kServiceName, kCFStringEncodingUTF8));
272  CFNotificationCenterPostNotification(center, service_name, NULL, NULL,
273                                       TRUE);
274  return true;
275}
276
277// static
278void DaemonControllerDelegateMac::PreferencePaneCallback(
279    CFNotificationCenterRef center,
280    void* observer,
281    CFStringRef name,
282    const void* object,
283    CFDictionaryRef user_info) {
284  DaemonControllerDelegateMac* self =
285      reinterpret_cast<DaemonControllerDelegateMac*>(observer);
286  if (!self) {
287    LOG(WARNING) << "Ignoring notification with NULL observer: " << name;
288    return;
289  }
290
291  self->PreferencePaneCallbackDelegate(name);
292}
293
294scoped_refptr<DaemonController> DaemonController::Create() {
295  scoped_ptr<DaemonController::Delegate> delegate(
296      new DaemonControllerDelegateMac());
297  return new DaemonController(delegate.Pass());
298}
299
300}  // namespace remoting
301