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::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