| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "remoting/host/mac/me2me_preference_pane.h" |
| |
| #import <Cocoa/Cocoa.h> |
| #include <CommonCrypto/CommonHMAC.h> |
| #include <errno.h> |
| #include <launch.h> |
| #import <PreferencePanes/PreferencePanes.h> |
| #import <SecurityInterface/SFAuthorizationView.h> |
| #include <stdlib.h> |
| #include <unistd.h> |
| |
| #include <fstream> |
| |
| #include "base/mac/scoped_launch_data.h" |
| #include "base/memory/scoped_ptr.h" |
| #include "base/posix/eintr_wrapper.h" |
| #include "remoting/host/constants_mac.h" |
| #include "remoting/host/host_config.h" |
| #import "remoting/host/mac/me2me_preference_pane_confirm_pin.h" |
| #import "remoting/host/mac/me2me_preference_pane_disable.h" |
| #include "third_party/jsoncpp/source/include/json/reader.h" |
| #include "third_party/jsoncpp/source/include/json/writer.h" |
| #include "third_party/modp_b64/modp_b64.h" |
| |
| namespace { |
| |
| bool GetTemporaryConfigFilePath(std::string* path) { |
| NSString* filename = NSTemporaryDirectory(); |
| if (filename == nil) |
| return false; |
| |
| *path = [[NSString stringWithFormat:@"%@/%s", |
| filename, remoting::kHostConfigFileName] UTF8String]; |
| return true; |
| } |
| |
| bool IsConfigValid(const remoting::JsonHostConfig* config) { |
| std::string value; |
| return (config->GetString(remoting::kHostIdConfigPath, &value) && |
| config->GetString(remoting::kHostSecretHashConfigPath, &value) && |
| config->GetString(remoting::kXmppLoginConfigPath, &value)); |
| } |
| |
| bool IsPinValid(const std::string& pin, const std::string& host_id, |
| const std::string& host_secret_hash) { |
| // TODO(lambroslambrou): Once the "base" target supports building for 64-bit |
| // on Mac OS X, remove this code and replace it with |VerifyHostPinHash()| |
| // from host/pin_hash.h. |
| size_t separator = host_secret_hash.find(':'); |
| if (separator == std::string::npos) |
| return false; |
| |
| std::string method = host_secret_hash.substr(0, separator); |
| if (method != "hmac") { |
| NSLog(@"Authentication method '%s' not supported", method.c_str()); |
| return false; |
| } |
| |
| std::string hash_base64 = host_secret_hash.substr(separator + 1); |
| |
| // Convert |hash_base64| to |hash|, based on code from base/base64.cc. |
| int hash_base64_size = static_cast<int>(hash_base64.size()); |
| std::string hash; |
| hash.resize(modp_b64_decode_len(hash_base64_size)); |
| |
| // modp_b64_decode_len() returns at least 1, so hash[0] is safe here. |
| int hash_size = modp_b64_decode(&(hash[0]), hash_base64.data(), |
| hash_base64_size); |
| if (hash_size < 0) { |
| NSLog(@"Failed to parse host_secret_hash"); |
| return false; |
| } |
| hash.resize(hash_size); |
| |
| std::string computed_hash; |
| computed_hash.resize(CC_SHA256_DIGEST_LENGTH); |
| |
| CCHmac(kCCHmacAlgSHA256, |
| host_id.data(), host_id.size(), |
| pin.data(), pin.size(), |
| &(computed_hash[0])); |
| |
| // Normally, a constant-time comparison function would be used, but it is |
| // unnecessary here as the "secret" is already readable by the user |
| // supplying input to this routine. |
| return computed_hash == hash; |
| } |
| |
| } // namespace |
| |
| // These methods are copied from base/mac, but with the logging changed to use |
| // NSLog(). |
| // |
| // TODO(lambroslambrou): Once the "base" target supports building for 64-bit |
| // on Mac OS X, remove these implementations and use the ones in base/mac. |
| namespace base { |
| namespace mac { |
| |
| // MessageForJob sends a single message to launchd with a simple dictionary |
| // mapping |operation| to |job_label|, and returns the result of calling |
| // launch_msg to send that message. On failure, returns NULL. The caller |
| // assumes ownership of the returned launch_data_t object. |
| launch_data_t MessageForJob(const std::string& job_label, |
| const char* operation) { |
| // launch_data_alloc returns something that needs to be freed. |
| ScopedLaunchData message(launch_data_alloc(LAUNCH_DATA_DICTIONARY)); |
| if (!message) { |
| NSLog(@"launch_data_alloc"); |
| return NULL; |
| } |
| |
| // launch_data_new_string returns something that needs to be freed, but |
| // the dictionary will assume ownership when launch_data_dict_insert is |
| // called, so put it in a scoper and .release() it when given to the |
| // dictionary. |
| ScopedLaunchData job_label_launchd(launch_data_new_string(job_label.c_str())); |
| if (!job_label_launchd) { |
| NSLog(@"launch_data_new_string"); |
| return NULL; |
| } |
| |
| if (!launch_data_dict_insert(message, |
| job_label_launchd.release(), |
| operation)) { |
| return NULL; |
| } |
| |
| return launch_msg(message); |
| } |
| |
| pid_t PIDForJob(const std::string& job_label) { |
| ScopedLaunchData response(MessageForJob(job_label, LAUNCH_KEY_GETJOB)); |
| if (!response) { |
| return -1; |
| } |
| |
| launch_data_type_t response_type = launch_data_get_type(response); |
| if (response_type != LAUNCH_DATA_DICTIONARY) { |
| if (response_type == LAUNCH_DATA_ERRNO) { |
| NSLog(@"PIDForJob: error %d", launch_data_get_errno(response)); |
| } else { |
| NSLog(@"PIDForJob: expected dictionary, got %d", response_type); |
| } |
| return -1; |
| } |
| |
| launch_data_t pid_data = launch_data_dict_lookup(response, |
| LAUNCH_JOBKEY_PID); |
| if (!pid_data) |
| return 0; |
| |
| if (launch_data_get_type(pid_data) != LAUNCH_DATA_INTEGER) { |
| NSLog(@"PIDForJob: expected integer"); |
| return -1; |
| } |
| |
| return launch_data_get_integer(pid_data); |
| } |
| |
| OSStatus ExecuteWithPrivilegesAndGetPID(AuthorizationRef authorization, |
| const char* tool_path, |
| AuthorizationFlags options, |
| const char** arguments, |
| FILE** pipe, |
| pid_t* pid) { |
| // pipe may be NULL, but this function needs one. In that case, use a local |
| // pipe. |
| FILE* local_pipe; |
| FILE** pipe_pointer; |
| if (pipe) { |
| pipe_pointer = pipe; |
| } else { |
| pipe_pointer = &local_pipe; |
| } |
| |
| // AuthorizationExecuteWithPrivileges wants |char* const*| for |arguments|, |
| // but it doesn't actually modify the arguments, and that type is kind of |
| // silly and callers probably aren't dealing with that. Put the cast here |
| // to make things a little easier on callers. |
| OSStatus status = AuthorizationExecuteWithPrivileges(authorization, |
| tool_path, |
| options, |
| (char* const*)arguments, |
| pipe_pointer); |
| if (status != errAuthorizationSuccess) { |
| return status; |
| } |
| |
| long line_pid = -1; |
| size_t line_length = 0; |
| char* line_c = fgetln(*pipe_pointer, &line_length); |
| if (line_c) { |
| if (line_length > 0 && line_c[line_length - 1] == '\n') { |
| // line_c + line_length is the start of the next line if there is one. |
| // Back up one character. |
| --line_length; |
| } |
| std::string line(line_c, line_length); |
| |
| // The version in base/mac used base::StringToInt() here. |
| line_pid = strtol(line.c_str(), NULL, 10); |
| if (line_pid == 0) { |
| NSLog(@"ExecuteWithPrivilegesAndGetPid: funny line: %s", line.c_str()); |
| line_pid = -1; |
| } |
| } else { |
| NSLog(@"ExecuteWithPrivilegesAndGetPid: no line"); |
| } |
| |
| if (!pipe) { |
| fclose(*pipe_pointer); |
| } |
| |
| if (pid) { |
| *pid = line_pid; |
| } |
| |
| return status; |
| } |
| |
| } // namespace mac |
| } // namespace base |
| |
| namespace remoting { |
| |
| JsonHostConfig::JsonHostConfig(const std::string& filename) |
| : filename_(filename) { |
| } |
| |
| JsonHostConfig::~JsonHostConfig() { |
| } |
| |
| bool JsonHostConfig::Read() { |
| std::ifstream file(filename_.c_str()); |
| Json::Reader reader; |
| return reader.parse(file, config_, false /* ignore comments */); |
| } |
| |
| bool JsonHostConfig::GetString(const std::string& path, |
| std::string* out_value) const { |
| if (!config_.isObject()) |
| return false; |
| |
| if (!config_.isMember(path)) |
| return false; |
| |
| Json::Value value = config_[path]; |
| if (!value.isString()) |
| return false; |
| |
| *out_value = value.asString(); |
| return true; |
| } |
| |
| std::string JsonHostConfig::GetSerializedData() const { |
| Json::FastWriter writer; |
| return writer.write(config_); |
| } |
| |
| } // namespace remoting |
| |
| @implementation Me2MePreferencePane |
| |
| - (void)mainViewDidLoad { |
| [authorization_view_ setDelegate:self]; |
| [authorization_view_ setString:kAuthorizationRightExecute]; |
| [authorization_view_ setAutoupdate:YES |
| interval:60]; |
| confirm_pin_view_ = [[Me2MePreferencePaneConfirmPin alloc] init]; |
| [confirm_pin_view_ setDelegate:self]; |
| disable_view_ = [[Me2MePreferencePaneDisable alloc] init]; |
| [disable_view_ setDelegate:self]; |
| } |
| |
| - (void)willSelect { |
| have_new_config_ = NO; |
| awaiting_service_stop_ = NO; |
| |
| NSDistributedNotificationCenter* center = |
| [NSDistributedNotificationCenter defaultCenter]; |
| [center addObserver:self |
| selector:@selector(onNewConfigFile:) |
| name:[NSString stringWithUTF8String:remoting::kServiceName] |
| object:nil]; |
| |
| service_status_timer_ = |
| [[NSTimer scheduledTimerWithTimeInterval:2.0 |
| target:self |
| selector:@selector(refreshServiceStatus:) |
| userInfo:nil |
| repeats:YES] retain]; |
| [self updateServiceStatus]; |
| [self updateAuthorizationStatus]; |
| |
| [self checkInstalledVersion]; |
| if (!restart_pending_or_canceled_) |
| [self readNewConfig]; |
| |
| [self updateUI]; |
| } |
| |
| - (void)didSelect { |
| [self checkInstalledVersion]; |
| } |
| |
| - (void)willUnselect { |
| NSDistributedNotificationCenter* center = |
| [NSDistributedNotificationCenter defaultCenter]; |
| [center removeObserver:self]; |
| |
| [service_status_timer_ invalidate]; |
| [service_status_timer_ release]; |
| service_status_timer_ = nil; |
| |
| [self notifyPlugin:UPDATE_FAILED_NOTIFICATION_NAME]; |
| } |
| |
| - (void)applyConfiguration:(id)sender |
| pin:(NSString*)pin { |
| if (!have_new_config_) { |
| // It shouldn't be possible to hit the button if there is no config to |
| // apply, but check anyway just in case it happens somehow. |
| return; |
| } |
| |
| // Ensure the authorization token is up-to-date before using it. |
| [self updateAuthorizationStatus]; |
| [self updateUI]; |
| |
| std::string pin_utf8 = [pin UTF8String]; |
| std::string host_id, host_secret_hash; |
| bool result = (config_->GetString(remoting::kHostIdConfigPath, &host_id) && |
| config_->GetString(remoting::kHostSecretHashConfigPath, |
| &host_secret_hash)); |
| if (!result) { |
| [self showError]; |
| return; |
| } |
| if (!IsPinValid(pin_utf8, host_id, host_secret_hash)) { |
| [self showIncorrectPinMessage]; |
| return; |
| } |
| |
| [self applyNewServiceConfig]; |
| [self updateUI]; |
| } |
| |
| - (void)onDisable:(id)sender { |
| // Ensure the authorization token is up-to-date before using it. |
| [self updateAuthorizationStatus]; |
| [self updateUI]; |
| if (!is_pane_unlocked_) |
| return; |
| |
| if (![self runHelperAsRootWithCommand:"--disable" |
| inputData:""]) { |
| NSLog(@"Failed to run the helper tool"); |
| [self showError]; |
| [self notifyPlugin:UPDATE_FAILED_NOTIFICATION_NAME]; |
| return; |
| } |
| |
| // Stop the launchd job. This cannot easily be done by the helper tool, |
| // since the launchd job runs in the current user's context. |
| [self sendJobControlMessage:LAUNCH_KEY_STOPJOB]; |
| awaiting_service_stop_ = YES; |
| } |
| |
| - (void)onNewConfigFile:(NSNotification*)notification { |
| [self checkInstalledVersion]; |
| if (!restart_pending_or_canceled_) |
| [self readNewConfig]; |
| |
| [self updateUI]; |
| } |
| |
| - (void)refreshServiceStatus:(NSTimer*)timer { |
| BOOL was_running = is_service_running_; |
| [self updateServiceStatus]; |
| if (awaiting_service_stop_ && !is_service_running_) { |
| awaiting_service_stop_ = NO; |
| [self notifyPlugin:UPDATE_SUCCEEDED_NOTIFICATION_NAME]; |
| } |
| |
| if (was_running != is_service_running_) |
| [self updateUI]; |
| } |
| |
| - (void)authorizationViewDidAuthorize:(SFAuthorizationView*)view { |
| [self updateAuthorizationStatus]; |
| [self updateUI]; |
| } |
| |
| - (void)authorizationViewDidDeauthorize:(SFAuthorizationView*)view { |
| [self updateAuthorizationStatus]; |
| [self updateUI]; |
| } |
| |
| - (void)updateServiceStatus { |
| pid_t job_pid = base::mac::PIDForJob(remoting::kServiceName); |
| is_service_running_ = (job_pid > 0); |
| } |
| |
| - (void)updateAuthorizationStatus { |
| is_pane_unlocked_ = [authorization_view_ updateStatus:authorization_view_]; |
| } |
| |
| - (void)readNewConfig { |
| std::string file; |
| if (!GetTemporaryConfigFilePath(&file)) { |
| NSLog(@"Failed to get path of configuration data."); |
| [self showError]; |
| return; |
| } |
| if (access(file.c_str(), F_OK) != 0) |
| return; |
| |
| scoped_ptr<remoting::JsonHostConfig> new_config_( |
| new remoting::JsonHostConfig(file)); |
| if (!new_config_->Read()) { |
| // Report the error, because the file exists but couldn't be read. The |
| // case of non-existence is normal and expected. |
| NSLog(@"Error reading configuration data from %s", file.c_str()); |
| [self showError]; |
| return; |
| } |
| remove(file.c_str()); |
| if (!IsConfigValid(new_config_.get())) { |
| NSLog(@"Invalid configuration data read."); |
| [self showError]; |
| return; |
| } |
| |
| config_.swap(new_config_); |
| have_new_config_ = YES; |
| |
| [confirm_pin_view_ resetPin]; |
| } |
| |
| - (void)updateUI { |
| if (have_new_config_) { |
| [box_ setContentView:[confirm_pin_view_ view]]; |
| } else { |
| [box_ setContentView:[disable_view_ view]]; |
| } |
| |
| // TODO(lambroslambrou): Show "enabled" and "disabled" in bold font. |
| NSString* message; |
| if (is_service_running_) { |
| if (have_new_config_) { |
| message = @"Please confirm your new PIN."; |
| } else { |
| message = @"Remote connections to this computer are enabled."; |
| } |
| } else { |
| if (have_new_config_) { |
| message = @"Remote connections to this computer are disabled. To enable " |
| "remote connections you must confirm your PIN."; |
| } else { |
| message = @"Remote connections to this computer are disabled."; |
| } |
| } |
| [status_message_ setStringValue:message]; |
| |
| std::string email; |
| if (config_.get()) { |
| bool result = |
| config_->GetString(remoting::kHostOwnerEmailConfigPath, &email); |
| if (!result) { |
| result = config_->GetString(remoting::kHostOwnerConfigPath, &email); |
| if (!result) { |
| result = config_->GetString(remoting::kXmppLoginConfigPath, &email); |
| |
| // The config has already been checked by |IsConfigValid|. |
| if (!result) { |
| [self showError]; |
| return; |
| } |
| } |
| } |
| } |
| [disable_view_ setEnabled:(is_pane_unlocked_ && is_service_running_ && |
| !restart_pending_or_canceled_)]; |
| [confirm_pin_view_ setEnabled:(is_pane_unlocked_ && |
| !restart_pending_or_canceled_)]; |
| [confirm_pin_view_ setEmail:[NSString stringWithUTF8String:email.c_str()]]; |
| NSString* applyButtonText = is_service_running_ ? @"Confirm" : @"Enable"; |
| [confirm_pin_view_ setButtonText:applyButtonText]; |
| |
| if (restart_pending_or_canceled_) |
| [authorization_view_ setEnabled:NO]; |
| } |
| |
| - (void)showError { |
| NSAlert* alert = [[NSAlert alloc] init]; |
| [alert setMessageText:@"An unexpected error occurred."]; |
| [alert setInformativeText:@"Check the system log for more information."]; |
| [alert setAlertStyle:NSWarningAlertStyle]; |
| [alert beginSheetModalForWindow:[[self mainView] window] |
| modalDelegate:nil |
| didEndSelector:nil |
| contextInfo:nil]; |
| [alert release]; |
| } |
| |
| - (void)showIncorrectPinMessage { |
| NSAlert* alert = [[NSAlert alloc] init]; |
| [alert setMessageText:@"Incorrect PIN entered."]; |
| [alert setAlertStyle:NSWarningAlertStyle]; |
| [alert beginSheetModalForWindow:[[self mainView] window] |
| modalDelegate:nil |
| didEndSelector:nil |
| contextInfo:nil]; |
| [alert release]; |
| } |
| |
| - (void)applyNewServiceConfig { |
| [self updateServiceStatus]; |
| std::string serialized_config = config_->GetSerializedData(); |
| const char* command = is_service_running_ ? "--save-config" : "--enable"; |
| if (![self runHelperAsRootWithCommand:command |
| inputData:serialized_config]) { |
| NSLog(@"Failed to run the helper tool"); |
| [self showError]; |
| return; |
| } |
| |
| have_new_config_ = NO; |
| |
| // Ensure the service is started. |
| if (!is_service_running_) { |
| [self sendJobControlMessage:LAUNCH_KEY_STARTJOB]; |
| } |
| |
| // Broadcast a distributed notification to inform the plugin that the |
| // configuration has been applied. |
| [self notifyPlugin:UPDATE_SUCCEEDED_NOTIFICATION_NAME]; |
| } |
| |
| - (BOOL)runHelperAsRootWithCommand:(const char*)command |
| inputData:(const std::string&)input_data { |
| AuthorizationRef authorization = |
| [[authorization_view_ authorization] authorizationRef]; |
| if (!authorization) { |
| NSLog(@"Failed to obtain authorizationRef"); |
| return NO; |
| } |
| |
| // TODO(lambroslambrou): Replace the deprecated ExecuteWithPrivileges |
| // call with a launchd-based helper tool, which is more secure. |
| // http://crbug.com/120903 |
| const char* arguments[] = { command, NULL }; |
| FILE* pipe = NULL; |
| pid_t pid; |
| OSStatus status = base::mac::ExecuteWithPrivilegesAndGetPID( |
| authorization, |
| remoting::kHostHelperScriptPath, |
| kAuthorizationFlagDefaults, |
| arguments, |
| &pipe, |
| &pid); |
| if (status != errAuthorizationSuccess) { |
| NSLog(@"AuthorizationExecuteWithPrivileges: %s (%d)", |
| GetMacOSStatusErrorString(status), static_cast<int>(status)); |
| return NO; |
| } |
| if (pid == -1) { |
| NSLog(@"Failed to get child PID"); |
| if (pipe) |
| fclose(pipe); |
| |
| return NO; |
| } |
| if (!pipe) { |
| NSLog(@"Unexpected NULL pipe"); |
| return NO; |
| } |
| |
| // Some cleanup is needed (closing the pipe and waiting for the child |
| // process), so flag any errors before returning. |
| BOOL error = NO; |
| |
| if (!input_data.empty()) { |
| size_t bytes_written = fwrite(input_data.data(), sizeof(char), |
| input_data.size(), pipe); |
| // According to the fwrite manpage, a partial count is returned only if a |
| // write error has occurred. |
| if (bytes_written != input_data.size()) { |
| NSLog(@"Failed to write data to child process"); |
| error = YES; |
| } |
| } |
| |
| // In all cases, fclose() should be called with the returned FILE*. In the |
| // case of sending data to the child, this needs to be done before calling |
| // waitpid(), since the child reads until EOF on its stdin, so calling |
| // waitpid() first would result in deadlock. |
| if (fclose(pipe) != 0) { |
| NSLog(@"fclose failed with error %d", errno); |
| error = YES; |
| } |
| |
| int exit_status; |
| pid_t wait_result = HANDLE_EINTR(waitpid(pid, &exit_status, 0)); |
| if (wait_result != pid) { |
| NSLog(@"waitpid failed with error %d", errno); |
| error = YES; |
| } |
| |
| // No more cleanup needed. |
| if (error) |
| return NO; |
| |
| if (WIFEXITED(exit_status) && WEXITSTATUS(exit_status) == 0) { |
| return YES; |
| } else { |
| NSLog(@"%s failed with exit status %d", remoting::kHostHelperScriptPath, |
| exit_status); |
| return NO; |
| } |
| } |
| |
| - (BOOL)sendJobControlMessage:(const char*)launch_key { |
| base::mac::ScopedLaunchData response( |
| base::mac::MessageForJob(remoting::kServiceName, launch_key)); |
| if (!response) { |
| NSLog(@"Failed to send message to launchd"); |
| [self showError]; |
| return NO; |
| } |
| |
| // Expect a response of type LAUNCH_DATA_ERRNO. |
| launch_data_type_t type = launch_data_get_type(response.get()); |
| if (type != LAUNCH_DATA_ERRNO) { |
| NSLog(@"launchd returned unexpected type: %d", type); |
| [self showError]; |
| return NO; |
| } |
| |
| int error = launch_data_get_errno(response.get()); |
| if (error) { |
| NSLog(@"launchd returned error: %d", error); |
| [self showError]; |
| return NO; |
| } |
| return YES; |
| } |
| |
| - (void)notifyPlugin:(const char*)message { |
| NSDistributedNotificationCenter* center = |
| [NSDistributedNotificationCenter defaultCenter]; |
| NSString* name = [NSString stringWithUTF8String:message]; |
| [center postNotificationName:name |
| object:nil |
| userInfo:nil]; |
| } |
| |
| - (void)checkInstalledVersion { |
| // There's no point repeating the check if the pane has already been disabled |
| // from a previous call to this method. The pane only gets disabled when a |
| // version-mismatch has been detected here, so skip the check, but continue to |
| // handle the version-mismatch case. |
| if (!restart_pending_or_canceled_) { |
| NSBundle* this_bundle = [NSBundle bundleForClass:[self class]]; |
| NSDictionary* this_plist = [this_bundle infoDictionary]; |
| NSString* this_version = [this_plist objectForKey:@"CFBundleVersion"]; |
| |
| NSString* bundle_path = [this_bundle bundlePath]; |
| NSString* plist_path = |
| [bundle_path stringByAppendingString:@"/Contents/Info.plist"]; |
| NSDictionary* disk_plist = |
| [NSDictionary dictionaryWithContentsOfFile:plist_path]; |
| NSString* disk_version = [disk_plist objectForKey:@"CFBundleVersion"]; |
| |
| if (disk_version == nil) { |
| NSLog(@"Failed to get installed version information"); |
| [self showError]; |
| return; |
| } |
| |
| if ([this_version isEqualToString:disk_version]) |
| return; |
| |
| restart_pending_or_canceled_ = YES; |
| [self updateUI]; |
| } |
| |
| NSWindow* window = [[self mainView] window]; |
| if (window == nil) { |
| // Defer the alert until |didSelect| is called, which happens just after |
| // the window is created. |
| return; |
| } |
| |
| // This alert appears as a sheet over the top of the Chromoting pref-pane, |
| // underneath the title, so it's OK to refer to "this preference pane" rather |
| // than repeat the title "Chromoting" here. |
| NSAlert* alert = [[NSAlert alloc] init]; |
| [alert setMessageText:@"System update detected"]; |
| [alert setInformativeText:@"To use this preference pane, System Preferences " |
| "needs to be restarted"]; |
| [alert addButtonWithTitle:@"OK"]; |
| NSButton* cancel_button = [alert addButtonWithTitle:@"Cancel"]; |
| [cancel_button setKeyEquivalent:@"\e"]; |
| [alert setAlertStyle:NSWarningAlertStyle]; |
| [alert beginSheetModalForWindow:window |
| modalDelegate:self |
| didEndSelector:@selector( |
| mismatchAlertDidEnd:returnCode:contextInfo:) |
| contextInfo:nil]; |
| [alert release]; |
| } |
| |
| - (void)mismatchAlertDidEnd:(NSAlert*)alert |
| returnCode:(NSInteger)returnCode |
| contextInfo:(void*)contextInfo { |
| if (returnCode == NSAlertFirstButtonReturn) { |
| // OK was pressed. |
| |
| // Dismiss the alert window here, so that the application will respond to |
| // the NSApp terminate: message. |
| [[alert window] orderOut:nil]; |
| [self restartSystemPreferences]; |
| } else { |
| // Cancel was pressed. |
| |
| // If there is a new config file, delete it and notify the web-app of |
| // failure to apply the config. Otherwise, the web-app will remain in a |
| // spinning state until System Preferences eventually gets restarted and |
| // the user visits this pane again. |
| std::string file; |
| if (!GetTemporaryConfigFilePath(&file)) { |
| // There's no point in alerting the user here. The same error would |
| // happen when the pane is eventually restarted, so the user would be |
| // alerted at that time. |
| NSLog(@"Failed to get path of configuration data."); |
| return; |
| } |
| |
| remove(file.c_str()); |
| [self notifyPlugin:UPDATE_FAILED_NOTIFICATION_NAME]; |
| } |
| } |
| |
| - (void)restartSystemPreferences { |
| NSTask* task = [[NSTask alloc] init]; |
| NSString* command = |
| [NSString stringWithUTF8String:remoting::kHostHelperScriptPath]; |
| NSArray* arguments = [NSArray arrayWithObjects:@"--relaunch-prefpane", nil]; |
| [task setLaunchPath:command]; |
| [task setArguments:arguments]; |
| [task setStandardInput:[NSPipe pipe]]; |
| [task launch]; |
| [task release]; |
| [NSApp terminate:nil]; |
| } |
| |
| @end |