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 759