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
3// found in the LICENSE file.
4
5#import "remoting/host/mac/me2me_preference_pane.h"
6
7#import <Cocoa/Cocoa.h>
8#include <CommonCrypto/CommonHMAC.h>
9#include <errno.h>
10#include <launch.h>
11#import <PreferencePanes/PreferencePanes.h>
12#import <SecurityInterface/SFAuthorizationView.h>
13#include <stdlib.h>
14#include <unistd.h>
15
16#include <fstream>
17
18#include "base/mac/scoped_launch_data.h"
19#include "base/memory/scoped_ptr.h"
20#include "base/posix/eintr_wrapper.h"
21#include "remoting/host/constants_mac.h"
22#include "remoting/host/host_config.h"
23#import "remoting/host/mac/me2me_preference_pane_confirm_pin.h"
24#import "remoting/host/mac/me2me_preference_pane_disable.h"
25#include "third_party/jsoncpp/source/include/json/reader.h"
26#include "third_party/jsoncpp/source/include/json/writer.h"
27#include "third_party/modp_b64/modp_b64.h"
28
29namespace {
30
31bool GetTemporaryConfigFilePath(std::string* path) {
32  NSString* filename = NSTemporaryDirectory();
33  if (filename == nil)
34    return false;
35
36  *path = [[NSString stringWithFormat:@"%@/%s",
37            filename, remoting::kHostConfigFileName] UTF8String];
38  return true;
39}
40
41bool IsConfigValid(const remoting::JsonHostConfig* config) {
42  std::string value;
43  return (config->GetString(remoting::kHostIdConfigPath, &value) &&
44          config->GetString(remoting::kHostSecretHashConfigPath, &value) &&
45          config->GetString(remoting::kXmppLoginConfigPath, &value));
46}
47
48bool IsPinValid(const std::string& pin, const std::string& host_id,
49                const std::string& host_secret_hash) {
50  // TODO(lambroslambrou): Once the "base" target supports building for 64-bit
51  // on Mac OS X, remove this code and replace it with |VerifyHostPinHash()|
52  // from host/pin_hash.h.
53  size_t separator = host_secret_hash.find(':');
54  if (separator == std::string::npos)
55    return false;
56
57  std::string method = host_secret_hash.substr(0, separator);
58  if (method != "hmac") {
59    NSLog(@"Authentication method '%s' not supported", method.c_str());
60    return false;
61  }
62
63  std::string hash_base64 = host_secret_hash.substr(separator + 1);
64
65  // Convert |hash_base64| to |hash|, based on code from base/base64.cc.
66  int hash_base64_size = static_cast<int>(hash_base64.size());
67  std::string hash;
68  hash.resize(modp_b64_decode_len(hash_base64_size));
69
70  // modp_b64_decode_len() returns at least 1, so hash[0] is safe here.
71  int hash_size = modp_b64_decode(&(hash[0]), hash_base64.data(),
72                                  hash_base64_size);
73  if (hash_size < 0) {
74    NSLog(@"Failed to parse host_secret_hash");
75    return false;
76  }
77  hash.resize(hash_size);
78
79  std::string computed_hash;
80  computed_hash.resize(CC_SHA256_DIGEST_LENGTH);
81
82  CCHmac(kCCHmacAlgSHA256,
83         host_id.data(), host_id.size(),
84         pin.data(), pin.size(),
85         &(computed_hash[0]));
86
87  // Normally, a constant-time comparison function would be used, but it is
88  // unnecessary here as the "secret" is already readable by the user
89  // supplying input to this routine.
90  return computed_hash == hash;
91}
92
93}  // namespace
94
95// These methods are copied from base/mac, but with the logging changed to use
96// NSLog().
97//
98// TODO(lambroslambrou): Once the "base" target supports building for 64-bit
99// on Mac OS X, remove these implementations and use the ones in base/mac.
100namespace base {
101namespace mac {
102
103// MessageForJob sends a single message to launchd with a simple dictionary
104// mapping |operation| to |job_label|, and returns the result of calling
105// launch_msg to send that message. On failure, returns NULL. The caller
106// assumes ownership of the returned launch_data_t object.
107launch_data_t MessageForJob(const std::string& job_label,
108                            const char* operation) {
109  // launch_data_alloc returns something that needs to be freed.
110  ScopedLaunchData message(launch_data_alloc(LAUNCH_DATA_DICTIONARY));
111  if (!message) {
112    NSLog(@"launch_data_alloc");
113    return NULL;
114  }
115
116  // launch_data_new_string returns something that needs to be freed, but
117  // the dictionary will assume ownership when launch_data_dict_insert is
118  // called, so put it in a scoper and .release() it when given to the
119  // dictionary.
120  ScopedLaunchData job_label_launchd(launch_data_new_string(job_label.c_str()));
121  if (!job_label_launchd) {
122    NSLog(@"launch_data_new_string");
123    return NULL;
124  }
125
126  if (!launch_data_dict_insert(message,
127                               job_label_launchd.release(),
128                               operation)) {
129    return NULL;
130  }
131
132  return launch_msg(message);
133}
134
135pid_t PIDForJob(const std::string& job_label) {
136  ScopedLaunchData response(MessageForJob(job_label, LAUNCH_KEY_GETJOB));
137  if (!response) {
138    return -1;
139  }
140
141  launch_data_type_t response_type = launch_data_get_type(response);
142  if (response_type != LAUNCH_DATA_DICTIONARY) {
143    if (response_type == LAUNCH_DATA_ERRNO) {
144      NSLog(@"PIDForJob: error %d", launch_data_get_errno(response));
145    } else {
146      NSLog(@"PIDForJob: expected dictionary, got %d", response_type);
147    }
148    return -1;
149  }
150
151  launch_data_t pid_data = launch_data_dict_lookup(response,
152                                                   LAUNCH_JOBKEY_PID);
153  if (!pid_data)
154    return 0;
155
156  if (launch_data_get_type(pid_data) != LAUNCH_DATA_INTEGER) {
157    NSLog(@"PIDForJob: expected integer");
158    return -1;
159  }
160
161  return launch_data_get_integer(pid_data);
162}
163
164OSStatus ExecuteWithPrivilegesAndGetPID(AuthorizationRef authorization,
165                                        const char* tool_path,
166                                        AuthorizationFlags options,
167                                        const char** arguments,
168                                        FILE** pipe,
169                                        pid_t* pid) {
170  // pipe may be NULL, but this function needs one.  In that case, use a local
171  // pipe.
172  FILE* local_pipe;
173  FILE** pipe_pointer;
174  if (pipe) {
175    pipe_pointer = pipe;
176  } else {
177    pipe_pointer = &local_pipe;
178  }
179
180  // AuthorizationExecuteWithPrivileges wants |char* const*| for |arguments|,
181  // but it doesn't actually modify the arguments, and that type is kind of
182  // silly and callers probably aren't dealing with that.  Put the cast here
183  // to make things a little easier on callers.
184  OSStatus status = AuthorizationExecuteWithPrivileges(authorization,
185                                                       tool_path,
186                                                       options,
187                                                       (char* const*)arguments,
188                                                       pipe_pointer);
189  if (status != errAuthorizationSuccess) {
190    return status;
191  }
192
193  long line_pid = -1;
194  size_t line_length = 0;
195  char* line_c = fgetln(*pipe_pointer, &line_length);
196  if (line_c) {
197    if (line_length > 0 && line_c[line_length - 1] == '\n') {
198      // line_c + line_length is the start of the next line if there is one.
199      // Back up one character.
200      --line_length;
201    }
202    std::string line(line_c, line_length);
203
204    // The version in base/mac used base::StringToInt() here.
205    line_pid = strtol(line.c_str(), NULL, 10);
206    if (line_pid == 0) {
207      NSLog(@"ExecuteWithPrivilegesAndGetPid: funny line: %s", line.c_str());
208      line_pid = -1;
209    }
210  } else {
211    NSLog(@"ExecuteWithPrivilegesAndGetPid: no line");
212  }
213
214  if (!pipe) {
215    fclose(*pipe_pointer);
216  }
217
218  if (pid) {
219    *pid = line_pid;
220  }
221
222  return status;
223}
224
225}  // namespace mac
226}  // namespace base
227
228namespace remoting {
229
230JsonHostConfig::JsonHostConfig(const std::string& filename)
231    : filename_(filename) {
232}
233
234JsonHostConfig::~JsonHostConfig() {
235}
236
237bool JsonHostConfig::Read() {
238  std::ifstream file(filename_.c_str());
239  Json::Reader reader;
240  return reader.parse(file, config_, false /* ignore comments */);
241}
242
243bool JsonHostConfig::GetString(const std::string& path,
244                               std::string* out_value) const {
245  if (!config_.isObject())
246    return false;
247
248  if (!config_.isMember(path))
249    return false;
250
251  Json::Value value = config_[path];
252  if (!value.isString())
253    return false;
254
255  *out_value = value.asString();
256  return true;
257}
258
259std::string JsonHostConfig::GetSerializedData() const {
260  Json::FastWriter writer;
261  return writer.write(config_);
262}
263
264}  // namespace remoting
265
266@implementation Me2MePreferencePane
267
268- (void)mainViewDidLoad {
269  [authorization_view_ setDelegate:self];
270  [authorization_view_ setString:kAuthorizationRightExecute];
271  [authorization_view_ setAutoupdate:YES
272                            interval:60];
273  confirm_pin_view_ = [[Me2MePreferencePaneConfirmPin alloc] init];
274  [confirm_pin_view_ setDelegate:self];
275  disable_view_ = [[Me2MePreferencePaneDisable alloc] init];
276  [disable_view_ setDelegate:self];
277}
278
279- (void)willSelect {
280  have_new_config_ = NO;
281  awaiting_service_stop_ = NO;
282
283  NSDistributedNotificationCenter* center =
284      [NSDistributedNotificationCenter defaultCenter];
285  [center addObserver:self
286             selector:@selector(onNewConfigFile:)
287                 name:[NSString stringWithUTF8String:remoting::kServiceName]
288               object:nil];
289
290  service_status_timer_ =
291      [[NSTimer scheduledTimerWithTimeInterval:2.0
292                                        target:self
293                                      selector:@selector(refreshServiceStatus:)
294                                      userInfo:nil
295                                       repeats:YES] retain];
296  [self updateServiceStatus];
297  [self updateAuthorizationStatus];
298
299  [self checkInstalledVersion];
300  if (!restart_pending_or_canceled_)
301    [self readNewConfig];
302
303  [self updateUI];
304}
305
306- (void)didSelect {
307  [self checkInstalledVersion];
308}
309
310- (void)willUnselect {
311  NSDistributedNotificationCenter* center =
312      [NSDistributedNotificationCenter defaultCenter];
313  [center removeObserver:self];
314
315  [service_status_timer_ invalidate];
316  [service_status_timer_ release];
317  service_status_timer_ = nil;
318
319  [self notifyPlugin:UPDATE_FAILED_NOTIFICATION_NAME];
320}
321
322- (void)applyConfiguration:(id)sender
323                       pin:(NSString*)pin {
324  if (!have_new_config_) {
325    // It shouldn't be possible to hit the button if there is no config to
326    // apply, but check anyway just in case it happens somehow.
327    return;
328  }
329
330  // Ensure the authorization token is up-to-date before using it.
331  [self updateAuthorizationStatus];
332  [self updateUI];
333
334  std::string pin_utf8 = [pin UTF8String];
335  std::string host_id, host_secret_hash;
336  bool result = (config_->GetString(remoting::kHostIdConfigPath, &host_id) &&
337                 config_->GetString(remoting::kHostSecretHashConfigPath,
338                                    &host_secret_hash));
339  if (!result) {
340    [self showError];
341    return;
342  }
343  if (!IsPinValid(pin_utf8, host_id, host_secret_hash)) {
344    [self showIncorrectPinMessage];
345    return;
346  }
347
348  [self applyNewServiceConfig];
349  [self updateUI];
350}
351
352- (void)onDisable:(id)sender {
353  // Ensure the authorization token is up-to-date before using it.
354  [self updateAuthorizationStatus];
355  [self updateUI];
356  if (!is_pane_unlocked_)
357    return;
358
359  if (![self runHelperAsRootWithCommand:"--disable"
360                              inputData:""]) {
361    NSLog(@"Failed to run the helper tool");
362    [self showError];
363    [self notifyPlugin:UPDATE_FAILED_NOTIFICATION_NAME];
364    return;
365  }
366
367  // Stop the launchd job.  This cannot easily be done by the helper tool,
368  // since the launchd job runs in the current user's context.
369  [self sendJobControlMessage:LAUNCH_KEY_STOPJOB];
370  awaiting_service_stop_ = YES;
371}
372
373- (void)onNewConfigFile:(NSNotification*)notification {
374  [self checkInstalledVersion];
375  if (!restart_pending_or_canceled_)
376    [self readNewConfig];
377
378  [self updateUI];
379}
380
381- (void)refreshServiceStatus:(NSTimer*)timer {
382  BOOL was_running = is_service_running_;
383  [self updateServiceStatus];
384  if (awaiting_service_stop_ && !is_service_running_) {
385    awaiting_service_stop_ = NO;
386    [self notifyPlugin:UPDATE_SUCCEEDED_NOTIFICATION_NAME];
387  }
388
389  if (was_running != is_service_running_)
390    [self updateUI];
391}
392
393- (void)authorizationViewDidAuthorize:(SFAuthorizationView*)view {
394  [self updateAuthorizationStatus];
395  [self updateUI];
396}
397
398- (void)authorizationViewDidDeauthorize:(SFAuthorizationView*)view {
399  [self updateAuthorizationStatus];
400  [self updateUI];
401}
402
403- (void)updateServiceStatus {
404  pid_t job_pid = base::mac::PIDForJob(remoting::kServiceName);
405  is_service_running_ = (job_pid > 0);
406}
407
408- (void)updateAuthorizationStatus {
409  is_pane_unlocked_ = [authorization_view_ updateStatus:authorization_view_];
410}
411
412- (void)readNewConfig {
413  std::string file;
414  if (!GetTemporaryConfigFilePath(&file)) {
415    NSLog(@"Failed to get path of configuration data.");
416    [self showError];
417    return;
418  }
419  if (access(file.c_str(), F_OK) != 0)
420    return;
421
422  scoped_ptr<remoting::JsonHostConfig> new_config_(
423      new remoting::JsonHostConfig(file));
424  if (!new_config_->Read()) {
425    // Report the error, because the file exists but couldn't be read.  The
426    // case of non-existence is normal and expected.
427    NSLog(@"Error reading configuration data from %s", file.c_str());
428    [self showError];
429    return;
430  }
431  remove(file.c_str());
432  if (!IsConfigValid(new_config_.get())) {
433    NSLog(@"Invalid configuration data read.");
434    [self showError];
435    return;
436  }
437
438  config_.swap(new_config_);
439  have_new_config_ = YES;
440
441  [confirm_pin_view_ resetPin];
442}
443
444- (void)updateUI {
445  if (have_new_config_) {
446    [box_ setContentView:[confirm_pin_view_ view]];
447  } else {
448    [box_ setContentView:[disable_view_ view]];
449  }
450
451  // TODO(lambroslambrou): Show "enabled" and "disabled" in bold font.
452  NSString* message;
453  if (is_service_running_) {
454    if (have_new_config_) {
455      message = @"Please confirm your new PIN.";
456    } else {
457      message = @"Remote connections to this computer are enabled.";
458    }
459  } else {
460    if (have_new_config_) {
461      message = @"Remote connections to this computer are disabled. To enable "
462          "remote connections you must confirm your PIN.";
463    } else {
464      message = @"Remote connections to this computer are disabled.";
465    }
466  }
467  [status_message_ setStringValue:message];
468
469  std::string email;
470  if (config_.get()) {
471    bool result =
472        config_->GetString(remoting::kHostOwnerEmailConfigPath, &email);
473    if (!result) {
474      result = config_->GetString(remoting::kHostOwnerConfigPath, &email);
475      if (!result) {
476        result = config_->GetString(remoting::kXmppLoginConfigPath, &email);
477
478        // The config has already been checked by |IsConfigValid|.
479        if (!result) {
480          [self showError];
481          return;
482        }
483      }
484    }
485  }
486  [disable_view_ setEnabled:(is_pane_unlocked_ && is_service_running_ &&
487                             !restart_pending_or_canceled_)];
488  [confirm_pin_view_ setEnabled:(is_pane_unlocked_ &&
489                                 !restart_pending_or_canceled_)];
490  [confirm_pin_view_ setEmail:[NSString stringWithUTF8String:email.c_str()]];
491  NSString* applyButtonText = is_service_running_ ? @"Confirm" : @"Enable";
492  [confirm_pin_view_ setButtonText:applyButtonText];
493
494  if (restart_pending_or_canceled_)
495    [authorization_view_ setEnabled:NO];
496}
497
498- (void)showError {
499  NSAlert* alert = [[NSAlert alloc] init];
500  [alert setMessageText:@"An unexpected error occurred."];
501  [alert setInformativeText:@"Check the system log for more information."];
502  [alert setAlertStyle:NSWarningAlertStyle];
503  [alert beginSheetModalForWindow:[[self mainView] window]
504                    modalDelegate:nil
505                   didEndSelector:nil
506                      contextInfo:nil];
507  [alert release];
508}
509
510- (void)showIncorrectPinMessage {
511  NSAlert* alert = [[NSAlert alloc] init];
512  [alert setMessageText:@"Incorrect PIN entered."];
513  [alert setAlertStyle:NSWarningAlertStyle];
514  [alert beginSheetModalForWindow:[[self mainView] window]
515                    modalDelegate:nil
516                   didEndSelector:nil
517                      contextInfo:nil];
518  [alert release];
519}
520
521- (void)applyNewServiceConfig {
522  [self updateServiceStatus];
523  std::string serialized_config = config_->GetSerializedData();
524  const char* command = is_service_running_ ? "--save-config" : "--enable";
525  if (![self runHelperAsRootWithCommand:command
526                              inputData:serialized_config]) {
527    NSLog(@"Failed to run the helper tool");
528    [self showError];
529    return;
530  }
531
532  have_new_config_ = NO;
533
534  // Ensure the service is started.
535  if (!is_service_running_) {
536    [self sendJobControlMessage:LAUNCH_KEY_STARTJOB];
537  }
538
539  // Broadcast a distributed notification to inform the plugin that the
540  // configuration has been applied.
541  [self notifyPlugin:UPDATE_SUCCEEDED_NOTIFICATION_NAME];
542}
543
544- (BOOL)runHelperAsRootWithCommand:(const char*)command
545                         inputData:(const std::string&)input_data {
546  AuthorizationRef authorization =
547      [[authorization_view_ authorization] authorizationRef];
548  if (!authorization) {
549    NSLog(@"Failed to obtain authorizationRef");
550    return NO;
551  }
552
553  // TODO(lambroslambrou): Replace the deprecated ExecuteWithPrivileges
554  // call with a launchd-based helper tool, which is more secure.
555  // http://crbug.com/120903
556  const char* arguments[] = { command, NULL };
557  FILE* pipe = NULL;
558  pid_t pid;
559  OSStatus status = base::mac::ExecuteWithPrivilegesAndGetPID(
560      authorization,
561      remoting::kHostHelperScriptPath,
562      kAuthorizationFlagDefaults,
563      arguments,
564      &pipe,
565      &pid);
566  if (status != errAuthorizationSuccess) {
567    NSLog(@"AuthorizationExecuteWithPrivileges: %s (%d)",
568          GetMacOSStatusErrorString(status), static_cast<int>(status));
569    return NO;
570  }
571  if (pid == -1) {
572    NSLog(@"Failed to get child PID");
573    if (pipe)
574      fclose(pipe);
575
576    return NO;
577  }
578  if (!pipe) {
579    NSLog(@"Unexpected NULL pipe");
580    return NO;
581  }
582
583  // Some cleanup is needed (closing the pipe and waiting for the child
584  // process), so flag any errors before returning.
585  BOOL error = NO;
586
587  if (!input_data.empty()) {
588    size_t bytes_written = fwrite(input_data.data(), sizeof(char),
589                                  input_data.size(), pipe);
590    // According to the fwrite manpage, a partial count is returned only if a
591    // write error has occurred.
592    if (bytes_written != input_data.size()) {
593      NSLog(@"Failed to write data to child process");
594      error = YES;
595    }
596  }
597
598  // In all cases, fclose() should be called with the returned FILE*.  In the
599  // case of sending data to the child, this needs to be done before calling
600  // waitpid(), since the child reads until EOF on its stdin, so calling
601  // waitpid() first would result in deadlock.
602  if (fclose(pipe) != 0) {
603    NSLog(@"fclose failed with error %d", errno);
604    error = YES;
605  }
606
607  int exit_status;
608  pid_t wait_result = HANDLE_EINTR(waitpid(pid, &exit_status, 0));
609  if (wait_result != pid) {
610    NSLog(@"waitpid failed with error %d", errno);
611    error = YES;
612  }
613
614  // No more cleanup needed.
615  if (error)
616    return NO;
617
618  if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) {
619    return YES;
620  } else {
621    NSLog(@"%s failed with exit status %d", remoting::kHostHelperScriptPath,
622          exit_status);
623    return NO;
624  }
625}
626
627- (BOOL)sendJobControlMessage:(const char*)launch_key {
628  base::mac::ScopedLaunchData response(
629      base::mac::MessageForJob(remoting::kServiceName, launch_key));
630  if (!response) {
631    NSLog(@"Failed to send message to launchd");
632    [self showError];
633    return NO;
634  }
635
636  // Expect a response of type LAUNCH_DATA_ERRNO.
637  launch_data_type_t type = launch_data_get_type(response.get());
638  if (type != LAUNCH_DATA_ERRNO) {
639    NSLog(@"launchd returned unexpected type: %d", type);
640    [self showError];
641    return NO;
642  }
643
644  int error = launch_data_get_errno(response.get());
645  if (error) {
646    NSLog(@"launchd returned error: %d", error);
647    [self showError];
648    return NO;
649  }
650  return YES;
651}
652
653- (void)notifyPlugin:(const char*)message {
654  NSDistributedNotificationCenter* center =
655      [NSDistributedNotificationCenter defaultCenter];
656  NSString* name = [NSString stringWithUTF8String:message];
657  [center postNotificationName:name
658                        object:nil
659                      userInfo:nil];
660}
661
662- (void)checkInstalledVersion {
663  // There's no point repeating the check if the pane has already been disabled
664  // from a previous call to this method.  The pane only gets disabled when a
665  // version-mismatch has been detected here, so skip the check, but continue to
666  // handle the version-mismatch case.
667  if (!restart_pending_or_canceled_) {
668    NSBundle* this_bundle = [NSBundle bundleForClass:[self class]];
669    NSDictionary* this_plist = [this_bundle infoDictionary];
670    NSString* this_version = [this_plist objectForKey:@"CFBundleVersion"];
671
672    NSString* bundle_path = [this_bundle bundlePath];
673    NSString* plist_path =
674        [bundle_path stringByAppendingString:@"/Contents/Info.plist"];
675    NSDictionary* disk_plist =
676        [NSDictionary dictionaryWithContentsOfFile:plist_path];
677    NSString* disk_version = [disk_plist objectForKey:@"CFBundleVersion"];
678
679    if (disk_version == nil) {
680      NSLog(@"Failed to get installed version information");
681      [self showError];
682      return;
683    }
684
685    if ([this_version isEqualToString:disk_version])
686      return;
687
688    restart_pending_or_canceled_ = YES;
689    [self updateUI];
690  }
691
692  NSWindow* window = [[self mainView] window];
693  if (window == nil) {
694    // Defer the alert until |didSelect| is called, which happens just after
695    // the window is created.
696    return;
697  }
698
699  // This alert appears as a sheet over the top of the Chromoting pref-pane,
700  // underneath the title, so it's OK to refer to "this preference pane" rather
701  // than repeat the title "Chromoting" here.
702  NSAlert* alert = [[NSAlert alloc] init];
703  [alert setMessageText:@"System update detected"];
704  [alert setInformativeText:@"To use this preference pane, System Preferences "
705      "needs to be restarted"];
706  [alert addButtonWithTitle:@"OK"];
707  NSButton* cancel_button = [alert addButtonWithTitle:@"Cancel"];
708  [cancel_button setKeyEquivalent:@"\e"];
709  [alert setAlertStyle:NSWarningAlertStyle];
710  [alert beginSheetModalForWindow:window
711                    modalDelegate:self
712                   didEndSelector:@selector(
713                       mismatchAlertDidEnd:returnCode:contextInfo:)
714                      contextInfo:nil];
715  [alert release];
716}
717
718- (void)mismatchAlertDidEnd:(NSAlert*)alert
719                 returnCode:(NSInteger)returnCode
720                contextInfo:(void*)contextInfo {
721  if (returnCode == NSAlertFirstButtonReturn) {
722    // OK was pressed.
723
724    // Dismiss the alert window here, so that the application will respond to
725    // the NSApp terminate: message.
726    [[alert window] orderOut:nil];
727    [self restartSystemPreferences];
728  } else {
729    // Cancel was pressed.
730
731    // If there is a new config file, delete it and notify the web-app of
732    // failure to apply the config.  Otherwise, the web-app will remain in a
733    // spinning state until System Preferences eventually gets restarted and
734    // the user visits this pane again.
735    std::string file;
736    if (!GetTemporaryConfigFilePath(&file)) {
737      // There's no point in alerting the user here.  The same error would
738      // happen when the pane is eventually restarted, so the user would be
739      // alerted at that time.
740      NSLog(@"Failed to get path of configuration data.");
741      return;
742    }
743
744    remove(file.c_str());
745    [self notifyPlugin:UPDATE_FAILED_NOTIFICATION_NAME];
746  }
747}
748
749- (void)restartSystemPreferences {
750  NSTask* task = [[NSTask alloc] init];
751  NSString* command =
752      [NSString stringWithUTF8String:remoting::kHostHelperScriptPath];
753  NSArray* arguments = [NSArray arrayWithObjects:@"--relaunch-prefpane", nil];
754  [task setLaunchPath:command];
755  [task setArguments:arguments];
756  [task setStandardInput:[NSPipe pipe]];
757  [task launch];
758  [task release];
759  [NSApp terminate:nil];
760}
761
762@end
763