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