Clean up extension confirmation prompts and make them consistent between Views and...
[chromium-blink-merge.git] / remoting / host / mac / me2me_preference_pane.mm
blob9bcae544f86d6aaeb55e704cc3cf893d3a5be03f
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.
5 #import "remoting/host/mac/me2me_preference_pane.h"
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>
16 #include <fstream>
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"
29 namespace {
31 bool GetTemporaryConfigFilePath(std::string* path) {
32   NSString* filename = NSTemporaryDirectory();
33   if (filename == nil)
34     return false;
36   *path = [[NSString stringWithFormat:@"%@/%s",
37             filename, remoting::kHostConfigFileName] UTF8String];
38   return true;
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));
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;
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   }
63   std::string hash_base64 = host_secret_hash.substr(separator + 1);
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));
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);
79   std::string computed_hash;
80   computed_hash.resize(CC_SHA256_DIGEST_LENGTH);
82   CCHmac(kCCHmacAlgSHA256,
83          host_id.data(), host_id.size(),
84          pin.data(), pin.size(),
85          &(computed_hash[0]));
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;
93 }  // namespace
95 // These methods are copied from base/mac, but with the logging changed to use
96 // NSLog().
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 {
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 nullptr. 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 nullptr;
114   }
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 nullptr;
124   }
126   if (!launch_data_dict_insert(message,
127                                job_label_launchd.release(),
128                                operation)) {
129     return nullptr;
130   }
132   return launch_msg(message);
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   }
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   }
151   launch_data_t pid_data = launch_data_dict_lookup(response,
152                                                    LAUNCH_JOBKEY_PID);
153   if (!pid_data)
154     return 0;
156   if (launch_data_get_type(pid_data) != LAUNCH_DATA_INTEGER) {
157     NSLog(@"PIDForJob: expected integer");
158     return -1;
159   }
161   return launch_data_get_integer(pid_data);
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 nullptr, but this function needs one.  In that case, use a
171   // local pipe.
172   FILE* local_pipe;
173   FILE** pipe_pointer;
174   if (pipe) {
175     pipe_pointer = pipe;
176   } else {
177     pipe_pointer = &local_pipe;
178   }
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   }
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);
204     // The version in base/mac used base::StringToInt() here.
205     line_pid = strtol(line.c_str(), nullptr, 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   }
214   if (!pipe) {
215     fclose(*pipe_pointer);
216   }
218   if (pid) {
219     *pid = line_pid;
220   }
222   return status;
225 }  // namespace mac
226 }  // namespace base
228 namespace remoting {
230 JsonHostConfig::JsonHostConfig(const std::string& filename)
231     : filename_(filename) {
234 JsonHostConfig::~JsonHostConfig() {
237 bool JsonHostConfig::Read() {
238   std::ifstream file(filename_.c_str());
239   Json::Reader reader;
240   return reader.parse(file, config_, false /* ignore comments */);
243 bool JsonHostConfig::GetString(const std::string& path,
244                                std::string* out_value) const {
245   if (!config_.isObject())
246     return false;
248   if (!config_.isMember(path))
249     return false;
251   Json::Value value = config_[path];
252   if (!value.isString())
253     return false;
255   *out_value = value.asString();
256   return true;
259 std::string JsonHostConfig::GetSerializedData() const {
260   Json::FastWriter writer;
261   return writer.write(config_);
264 }  // namespace remoting
266 @implementation Me2MePreferencePane
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];
279 - (void)willSelect {
280   have_new_config_ = NO;
281   awaiting_service_stop_ = NO;
283   NSDistributedNotificationCenter* center =
284       [NSDistributedNotificationCenter defaultCenter];
285   [center addObserver:self
286              selector:@selector(onNewConfigFile:)
287                  name:[NSString stringWithUTF8String:remoting::kServiceName]
288                object:nil];
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];
299   [self checkInstalledVersion];
300   if (!restart_pending_or_canceled_)
301     [self readNewConfig];
303   [self updateUI];
306 - (void)didSelect {
307   [self checkInstalledVersion];
310 - (void)willUnselect {
311   NSDistributedNotificationCenter* center =
312       [NSDistributedNotificationCenter defaultCenter];
313   [center removeObserver:self];
315   [service_status_timer_ invalidate];
316   [service_status_timer_ release];
317   service_status_timer_ = nil;
319   [self notifyPlugin:UPDATE_FAILED_NOTIFICATION_NAME];
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   }
330   // Ensure the authorization token is up-to-date before using it.
331   [self updateAuthorizationStatus];
332   [self updateUI];
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   }
348   [self applyNewServiceConfig];
349   [self updateUI];
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;
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   }
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;
373 - (void)onNewConfigFile:(NSNotification*)notification {
374   [self checkInstalledVersion];
375   if (!restart_pending_or_canceled_)
376     [self readNewConfig];
378   [self updateUI];
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   }
389   if (was_running != is_service_running_)
390     [self updateUI];
393 - (void)authorizationViewDidAuthorize:(SFAuthorizationView*)view {
394   [self updateAuthorizationStatus];
395   [self updateUI];
398 - (void)authorizationViewDidDeauthorize:(SFAuthorizationView*)view {
399   [self updateAuthorizationStatus];
400   [self updateUI];
403 - (void)updateServiceStatus {
404   pid_t job_pid = base::mac::PIDForJob(remoting::kServiceName);
405   is_service_running_ = (job_pid > 0);
408 - (void)updateAuthorizationStatus {
409   is_pane_unlocked_ = [authorization_view_ updateStatus:authorization_view_];
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;
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   }
438   config_.swap(new_config_);
439   have_new_config_ = YES;
441   [confirm_pin_view_ resetPin];
444 - (void)updateUI {
445   if (have_new_config_) {
446     [box_ setContentView:[confirm_pin_view_ view]];
447   } else {
448     [box_ setContentView:[disable_view_ view]];
449   }
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];
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);
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];
494   if (restart_pending_or_canceled_)
495     [authorization_view_ setEnabled:NO];
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];
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];
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   }
532   have_new_config_ = NO;
534   // Ensure the service is started.
535   if (!is_service_running_) {
536     [self sendJobControlMessage:LAUNCH_KEY_STARTJOB];
537   }
539   // Broadcast a distributed notification to inform the plugin that the
540   // configuration has been applied.
541   [self notifyPlugin:UPDATE_SUCCEEDED_NOTIFICATION_NAME];
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   }
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, nullptr };
557   FILE* pipe = nullptr;
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);
576     return NO;
577   }
578   if (!pipe) {
579     NSLog(@"Unexpected nullptr pipe");
580     return NO;
581   }
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;
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   }
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   }
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   }
614   // No more cleanup needed.
615   if (error)
616     return NO;
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   }
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   }
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   }
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;
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];
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"];
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"];
679     if (disk_version == nil) {
680       NSLog(@"Failed to get installed version information");
681       [self showError];
682       return;
683     }
685     if ([this_version isEqualToString:disk_version])
686       return;
688     restart_pending_or_canceled_ = YES;
689     [self updateUI];
690   }
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   }
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];
718 - (void)mismatchAlertDidEnd:(NSAlert*)alert
719                  returnCode:(NSInteger)returnCode
720                 contextInfo:(void*)contextInfo {
721   if (returnCode == NSAlertFirstButtonReturn) {
722     // OK was pressed.
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.
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     }
744     remove(file.c_str());
745     [self notifyPlugin:UPDATE_FAILED_NOTIFICATION_NAME];
746   }
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];
762 @end