56cc235e847ee5920dc79c30f9be82a242e3adef
[platform/framework/web/crosswalk.git] / src / remoting / host / mac / me2me_preference_pane.mm
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
29 namespace {
30
31 bool 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
41 bool 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
48 bool 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.
100 namespace base {
101 namespace 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.
107 launch_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
135 pid_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
164 OSStatus 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
228 namespace remoting {
229
230 JsonHostConfig::JsonHostConfig(const std::string& filename)
231     : filename_(filename) {
232 }
233
234 JsonHostConfig::~JsonHostConfig() {
235 }
236
237 bool JsonHostConfig::Read() {
238   std::ifstream file(filename_.c_str());
239   Json::Reader reader;
240   return reader.parse(file, config_, false /* ignore comments */);
241 }
242
243 bool 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
259 std::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 = config_->GetString(remoting::kHostOwnerConfigPath, &email);
472     if (!result) {
473       result = config_->GetString(remoting::kXmppLoginConfigPath, &email);
474
475       // The config has already been checked by |IsConfigValid|.
476       if (!result) {
477         [self showError];
478         return;
479       }
480     }
481   }
482   [disable_view_ setEnabled:(is_pane_unlocked_ && is_service_running_ &&
483                              !restart_pending_or_canceled_)];
484   [confirm_pin_view_ setEnabled:(is_pane_unlocked_ &&
485                                  !restart_pending_or_canceled_)];
486   [confirm_pin_view_ setEmail:[NSString stringWithUTF8String:email.c_str()]];
487   NSString* applyButtonText = is_service_running_ ? @"Confirm" : @"Enable";
488   [confirm_pin_view_ setButtonText:applyButtonText];
489
490   if (restart_pending_or_canceled_)
491     [authorization_view_ setEnabled:NO];
492 }
493
494 - (void)showError {
495   NSAlert* alert = [[NSAlert alloc] init];
496   [alert setMessageText:@"An unexpected error occurred."];
497   [alert setInformativeText:@"Check the system log for more information."];
498   [alert setAlertStyle:NSWarningAlertStyle];
499   [alert beginSheetModalForWindow:[[self mainView] window]
500                     modalDelegate:nil
501                    didEndSelector:nil
502                       contextInfo:nil];
503   [alert release];
504 }
505
506 - (void)showIncorrectPinMessage {
507   NSAlert* alert = [[NSAlert alloc] init];
508   [alert setMessageText:@"Incorrect PIN entered."];
509   [alert setAlertStyle:NSWarningAlertStyle];
510   [alert beginSheetModalForWindow:[[self mainView] window]
511                     modalDelegate:nil
512                    didEndSelector:nil
513                       contextInfo:nil];
514   [alert release];
515 }
516
517 - (void)applyNewServiceConfig {
518   [self updateServiceStatus];
519   std::string serialized_config = config_->GetSerializedData();
520   const char* command = is_service_running_ ? "--save-config" : "--enable";
521   if (![self runHelperAsRootWithCommand:command
522                               inputData:serialized_config]) {
523     NSLog(@"Failed to run the helper tool");
524     [self showError];
525     return;
526   }
527
528   have_new_config_ = NO;
529
530   // Ensure the service is started.
531   if (!is_service_running_) {
532     [self sendJobControlMessage:LAUNCH_KEY_STARTJOB];
533   }
534
535   // Broadcast a distributed notification to inform the plugin that the
536   // configuration has been applied.
537   [self notifyPlugin:UPDATE_SUCCEEDED_NOTIFICATION_NAME];
538 }
539
540 - (BOOL)runHelperAsRootWithCommand:(const char*)command
541                          inputData:(const std::string&)input_data {
542   AuthorizationRef authorization =
543       [[authorization_view_ authorization] authorizationRef];
544   if (!authorization) {
545     NSLog(@"Failed to obtain authorizationRef");
546     return NO;
547   }
548
549   // TODO(lambroslambrou): Replace the deprecated ExecuteWithPrivileges
550   // call with a launchd-based helper tool, which is more secure.
551   // http://crbug.com/120903
552   const char* arguments[] = { command, NULL };
553   FILE* pipe = NULL;
554   pid_t pid;
555   OSStatus status = base::mac::ExecuteWithPrivilegesAndGetPID(
556       authorization,
557       remoting::kHostHelperScriptPath,
558       kAuthorizationFlagDefaults,
559       arguments,
560       &pipe,
561       &pid);
562   if (status != errAuthorizationSuccess) {
563     NSLog(@"AuthorizationExecuteWithPrivileges: %s (%d)",
564           GetMacOSStatusErrorString(status), static_cast<int>(status));
565     return NO;
566   }
567   if (pid == -1) {
568     NSLog(@"Failed to get child PID");
569     if (pipe)
570       fclose(pipe);
571
572     return NO;
573   }
574   if (!pipe) {
575     NSLog(@"Unexpected NULL pipe");
576     return NO;
577   }
578
579   // Some cleanup is needed (closing the pipe and waiting for the child
580   // process), so flag any errors before returning.
581   BOOL error = NO;
582
583   if (!input_data.empty()) {
584     size_t bytes_written = fwrite(input_data.data(), sizeof(char),
585                                   input_data.size(), pipe);
586     // According to the fwrite manpage, a partial count is returned only if a
587     // write error has occurred.
588     if (bytes_written != input_data.size()) {
589       NSLog(@"Failed to write data to child process");
590       error = YES;
591     }
592   }
593
594   // In all cases, fclose() should be called with the returned FILE*.  In the
595   // case of sending data to the child, this needs to be done before calling
596   // waitpid(), since the child reads until EOF on its stdin, so calling
597   // waitpid() first would result in deadlock.
598   if (fclose(pipe) != 0) {
599     NSLog(@"fclose failed with error %d", errno);
600     error = YES;
601   }
602
603   int exit_status;
604   pid_t wait_result = HANDLE_EINTR(waitpid(pid, &exit_status, 0));
605   if (wait_result != pid) {
606     NSLog(@"waitpid failed with error %d", errno);
607     error = YES;
608   }
609
610   // No more cleanup needed.
611   if (error)
612     return NO;
613
614   if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) {
615     return YES;
616   } else {
617     NSLog(@"%s failed with exit status %d", remoting::kHostHelperScriptPath,
618           exit_status);
619     return NO;
620   }
621 }
622
623 - (BOOL)sendJobControlMessage:(const char*)launch_key {
624   base::mac::ScopedLaunchData response(
625       base::mac::MessageForJob(remoting::kServiceName, launch_key));
626   if (!response) {
627     NSLog(@"Failed to send message to launchd");
628     [self showError];
629     return NO;
630   }
631
632   // Expect a response of type LAUNCH_DATA_ERRNO.
633   launch_data_type_t type = launch_data_get_type(response.get());
634   if (type != LAUNCH_DATA_ERRNO) {
635     NSLog(@"launchd returned unexpected type: %d", type);
636     [self showError];
637     return NO;
638   }
639
640   int error = launch_data_get_errno(response.get());
641   if (error) {
642     NSLog(@"launchd returned error: %d", error);
643     [self showError];
644     return NO;
645   }
646   return YES;
647 }
648
649 - (void)notifyPlugin:(const char*)message {
650   NSDistributedNotificationCenter* center =
651       [NSDistributedNotificationCenter defaultCenter];
652   NSString* name = [NSString stringWithUTF8String:message];
653   [center postNotificationName:name
654                         object:nil
655                       userInfo:nil];
656 }
657
658 - (void)checkInstalledVersion {
659   // There's no point repeating the check if the pane has already been disabled
660   // from a previous call to this method.  The pane only gets disabled when a
661   // version-mismatch has been detected here, so skip the check, but continue to
662   // handle the version-mismatch case.
663   if (!restart_pending_or_canceled_) {
664     NSBundle* this_bundle = [NSBundle bundleForClass:[self class]];
665     NSDictionary* this_plist = [this_bundle infoDictionary];
666     NSString* this_version = [this_plist objectForKey:@"CFBundleVersion"];
667
668     NSString* bundle_path = [this_bundle bundlePath];
669     NSString* plist_path =
670         [bundle_path stringByAppendingString:@"/Contents/Info.plist"];
671     NSDictionary* disk_plist =
672         [NSDictionary dictionaryWithContentsOfFile:plist_path];
673     NSString* disk_version = [disk_plist objectForKey:@"CFBundleVersion"];
674
675     if (disk_version == nil) {
676       NSLog(@"Failed to get installed version information");
677       [self showError];
678       return;
679     }
680
681     if ([this_version isEqualToString:disk_version])
682       return;
683
684     restart_pending_or_canceled_ = YES;
685     [self updateUI];
686   }
687
688   NSWindow* window = [[self mainView] window];
689   if (window == nil) {
690     // Defer the alert until |didSelect| is called, which happens just after
691     // the window is created.
692     return;
693   }
694
695   // This alert appears as a sheet over the top of the Chromoting pref-pane,
696   // underneath the title, so it's OK to refer to "this preference pane" rather
697   // than repeat the title "Chromoting" here.
698   NSAlert* alert = [[NSAlert alloc] init];
699   [alert setMessageText:@"System update detected"];
700   [alert setInformativeText:@"To use this preference pane, System Preferences "
701       "needs to be restarted"];
702   [alert addButtonWithTitle:@"OK"];
703   NSButton* cancel_button = [alert addButtonWithTitle:@"Cancel"];
704   [cancel_button setKeyEquivalent:@"\e"];
705   [alert setAlertStyle:NSWarningAlertStyle];
706   [alert beginSheetModalForWindow:window
707                     modalDelegate:self
708                    didEndSelector:@selector(
709                        mismatchAlertDidEnd:returnCode:contextInfo:)
710                       contextInfo:nil];
711   [alert release];
712 }
713
714 - (void)mismatchAlertDidEnd:(NSAlert*)alert
715                  returnCode:(NSInteger)returnCode
716                 contextInfo:(void*)contextInfo {
717   if (returnCode == NSAlertFirstButtonReturn) {
718     // OK was pressed.
719
720     // Dismiss the alert window here, so that the application will respond to
721     // the NSApp terminate: message.
722     [[alert window] orderOut:nil];
723     [self restartSystemPreferences];
724   } else {
725     // Cancel was pressed.
726
727     // If there is a new config file, delete it and notify the web-app of
728     // failure to apply the config.  Otherwise, the web-app will remain in a
729     // spinning state until System Preferences eventually gets restarted and
730     // the user visits this pane again.
731     std::string file;
732     if (!GetTemporaryConfigFilePath(&file)) {
733       // There's no point in alerting the user here.  The same error would
734       // happen when the pane is eventually restarted, so the user would be
735       // alerted at that time.
736       NSLog(@"Failed to get path of configuration data.");
737       return;
738     }
739
740     remove(file.c_str());
741     [self notifyPlugin:UPDATE_FAILED_NOTIFICATION_NAME];
742   }
743 }
744
745 - (void)restartSystemPreferences {
746   NSTask* task = [[NSTask alloc] init];
747   NSString* command =
748       [NSString stringWithUTF8String:remoting::kHostHelperScriptPath];
749   NSArray* arguments = [NSArray arrayWithObjects:@"--relaunch-prefpane", nil];
750   [task setLaunchPath:command];
751   [task setArguments:arguments];
752   [task setStandardInput:[NSPipe pipe]];
753   [task launch];
754   [task release];
755   [NSApp terminate:nil];
756 }
757
758 @end