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