PageRenderTime 85ms CodeModel.GetById 2ms app.highlight 77ms RepoModel.GetById 1ms app.codeStats 0ms

/core/externals/update-engine/Core/KSInstallAction.m

http://macfuse.googlecode.com/
Objective C | 432 lines | 281 code | 69 blank | 82 comment | 40 complexity | 9c48931fa097f61667aee8e4f669c8d2 MD5 | raw file
  1// Copyright 2008 Google Inc.
  2//
  3// Licensed under the Apache License, Version 2.0 (the "License");
  4// you may not use this file except in compliance with the License.
  5// You may obtain a copy of the License at
  6//
  7//     http://www.apache.org/licenses/LICENSE-2.0
  8//
  9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#import "KSInstallAction.h"
 16
 17#import <sys/mount.h>  // for MNAMELEN
 18
 19#import "GTMLogger.h"
 20#import "KSActionPipe.h"
 21#import "KSActionProcessor.h"
 22#import "KSCommandRunner.h"
 23#import "KSDiskImage.h"
 24#import "KSTicket.h"
 25
 26
 27static NSString *gInstallScriptPrefix;
 28
 29
 30@interface KSInstallAction (PrivateMethods)
 31- (NSString *)engineToolsPath;
 32- (NSString *)mountPoint;
 33- (void)addUpdateInfoToEnvironment:(NSMutableDictionary *)env;
 34- (void)addSupportedFeaturesToEnvironment:(NSMutableDictionary *)env;
 35- (NSMutableDictionary *)environment;
 36- (BOOL)isPathToExecutableFile:(NSString *)path;
 37@end
 38
 39
 40@implementation KSInstallAction
 41
 42+ (id)actionWithDMGPath:(NSString *)path
 43                 runner:(id<KSCommandRunner>)runner
 44          userInitiated:(BOOL)ui {
 45  return [self actionWithDMGPath:path
 46                          runner:runner
 47                   userInitiated:ui
 48                      updateInfo:nil];
 49}
 50
 51+ (id)actionWithDMGPath:(NSString *)path
 52                 runner:(id<KSCommandRunner>)runner
 53          userInitiated:(BOOL)ui
 54             updateInfo:(KSUpdateInfo *)updateInfo {
 55  return [[[self alloc] initWithDMGPath:path
 56                                 runner:runner
 57                          userInitiated:ui
 58                             updateInfo:updateInfo] autorelease];
 59}
 60
 61- (id)init {
 62  return [self initWithDMGPath:nil runner:nil userInitiated:NO updateInfo:nil];
 63}
 64
 65- (id)initWithDMGPath:(NSString *)path
 66               runner:(id<KSCommandRunner>)runner
 67        userInitiated:(BOOL)ui
 68           updateInfo:(KSUpdateInfo *)updateInfo {
 69  if ((self = [super init])) {
 70    [self setInPipe:[KSActionPipe pipeWithContents:path]];
 71    runner_ = [runner retain];
 72    ui_ = ui;
 73    updateInfo_ = [updateInfo retain];  // allowed to be nil
 74
 75    if (runner_ == nil) {
 76      GTMLoggerDebug(@"created with illegal argument: "
 77                     @"runner=%@, ui=%d", runner_, ui_);
 78      [self release];
 79      return nil;
 80    }
 81  }
 82  return self;
 83}
 84
 85- (void)dealloc {
 86  [runner_ release];
 87  [updateInfo_ release];
 88  [super dealloc];
 89}
 90
 91- (NSString *)dmgPath {
 92  return [[self inPipe] contents];
 93}
 94
 95- (id<KSCommandRunner>)runner {
 96  return runner_;
 97}
 98
 99- (BOOL)userInitiated {
100  return ui_;
101}
102
103- (void)performAction {
104  // When this method is called, we'll mount a disk and run some install
105  // scripts, so it's important that we don't terminate before we're all done.
106  // This means that if this action is terminated (via -terminateAction), we
107  // *still* want to run to completion. If this happens, we need to guarantee
108  // that this object ("self") stays around until this method completes. Which
109  // is why we retain ourself on the first line, and release on the last line.
110  [self retain];
111
112  // Assert class invariants that we care about here
113  _GTMDevAssert(runner_ != nil, @"runner_ must not be nil");
114
115  // A magic constant to set the rc "result code" to, so we can tell later on if
116  // a failure is due to a script result, or if we bail out before the scripts
117  // are run.
118  static const int kNoScriptsRunRC = 'k:-O';  // 0x6b3a2d4f = 1798974799
119  int rc = kNoScriptsRunRC;
120  BOOL success = NO;
121
122  KSDiskImage *diskImage = [KSDiskImage diskImageWithPath:[self dmgPath]];
123  NSString *mountPoint = [diskImage mount:[self mountPoint]];
124  if (mountPoint == nil) {
125    GTMLoggerError(@"Failed to mount %@ at %@",
126                   [self dmgPath], [self mountPoint]);
127    rc = kNoScriptsRunRC;
128    success = NO;
129    goto bail_no_unmount;
130  }
131
132  NSString *script1 = [mountPoint stringByAppendingPathComponent:
133                       [[self class] preinstallScriptName]];
134  NSString *script2 = [mountPoint stringByAppendingPathComponent:
135                       [[self class] installScriptName]];
136  NSString *script3 = [mountPoint stringByAppendingPathComponent:
137                       [[self class] postinstallScriptName]];
138
139  if (![self isPathToExecutableFile:script2]) {
140    // This script is the ".engine_install" script, and it MUST exist
141    GTMLoggerError(@"%@ does not exist", script2);
142    success = NO;
143    goto bail;
144  }
145
146  NSString *output1 = nil;
147  NSString *output2 = nil;
148  NSString *output3 = nil;
149  NSString *error1 = nil;
150  NSString *error2 = nil;
151  NSString *error3 = nil;
152
153  NSArray *args = [NSArray arrayWithObject:mountPoint];
154  NSMutableDictionary *env = [self environment];
155
156  //
157  // Script 1
158  //
159  if ([self isPathToExecutableFile:script1]) {
160    @try {
161      rc = 1;  // non-zero is failure
162      rc = [runner_ runCommand:script1
163                      withArgs:args
164                   environment:env
165                        output:&output1
166                      stdError:&error1];
167    }
168    @catch (id ex) {
169      GTMLoggerError(@"Caught exception from runner_ (script1): %@", ex);
170    }
171    // It's possible for the script to return a successful return
172    // status even if there was an error, so always log stderr from
173    // the scripts.
174    if ([error1 length] > 0) {
175      GTMLoggerError(@"stderr from preinstall script %@: %@", script1, error1);
176    }
177    if (rc != KS_INSTALL_SUCCESS) {
178      success = NO;
179      goto bail;
180    }
181  }
182  [env setObject:(output1 ? output1 : @"") forKey:@"KS_PREINSTALL_OUT"];
183
184  //
185  // Script 2
186  //
187  if ([self isPathToExecutableFile:script2]) {
188    // Notice that this "runCommand" is different from the other two because
189    // this one is sent to "self", whereas the other two are sent to the
190    // runner. This is because the pre/post-install scripts need to be
191    // executed by the console user, but the install script must be run as
192    // *this* user (where, "this" user might be root).
193    @try {
194      rc = 1;  // non-zero is failure
195      rc = [[KSTaskCommandRunner commandRunner] runCommand:script2
196                                                  withArgs:args
197                                               environment:env
198                                                    output:&output2
199                                                  stdError:&error2];
200    }
201    @catch (id ex) {
202      GTMLoggerError(@"Caught exception from runner_ (script2): %@", ex);
203    }
204    if ([error2 length] > 0) {
205      GTMLoggerError(@"stderr from install script %@: %@", script2, error2);
206    }
207    if (rc != KS_INSTALL_SUCCESS) {
208      success = NO;
209      goto bail;
210    }
211  }
212  [env setObject:(output2 ? output2 : @"") forKey:@"KS_INSTALL_OUT"];
213
214  //
215  // Script 3
216  //
217  if ([self isPathToExecutableFile:script3]) {
218    @try {
219      rc = 1;  // non-zero is failure
220      rc = [runner_ runCommand:script3
221                      withArgs:args
222                   environment:env
223                        output:&output3
224                      stdError:&error3];
225    }
226    @catch (id ex) {
227      GTMLoggerError(@"Caught exception from runner_ (script3): %@", ex);
228    }
229    if ([error3 length] > 0) {
230      GTMLoggerError(@"stderr from postinstall script %@: %@", script3, error3);
231    }
232    if (rc != KS_INSTALL_SUCCESS) {
233      success = NO;
234      goto bail;
235    }
236  }
237
238  success = YES;
239
240bail:
241  if (![diskImage unmount])
242    GTMLoggerError(@"Failed to unmount %@", mountPoint);  // COV_NF_LINE
243
244bail_no_unmount:
245  // Treat "try again later" and "requires reboot" return codes as successes.
246  if (rc == KS_INSTALL_TRY_AGAIN_LATER || rc == KS_INSTALL_WANTS_REBOOT)
247    success = YES;
248
249  if (!success && rc != kNoScriptsRunRC) {
250    GTMLoggerError(@"Return code %d from an install script. "
251                   "output1: %@, output2: %@, output3: %@",
252                   rc, output1, output2, output3);
253  }
254
255  [[self outPipe] setContents:[NSNumber numberWithInt:rc]];
256  [[self processor] finishedProcessing:self successfully:success];
257
258  // Balance our retain on the first line of this method
259  [self release];
260}
261
262- (NSString *)description {
263  return [NSString stringWithFormat:@"<%@:%p inPipe=%@ outPipe=%@>",
264                   [self class], self, [self inPipe], [self outPipe]];
265}
266
267@end  // KSInstallAction
268
269
270@implementation KSInstallAction (Configuration)
271
272+ (NSString *)installScriptPrefix {
273  return gInstallScriptPrefix ? gInstallScriptPrefix : @".engine";
274}
275
276+ (void)setInstallScriptPrefix:(NSString *)prefix {
277  [gInstallScriptPrefix autorelease];
278  gInstallScriptPrefix = [prefix copy];
279}
280
281+ (NSString *)preinstallScriptName {
282  return [[self installScriptPrefix] stringByAppendingString:@"_preinstall"];
283}
284
285+ (NSString *)installScriptName {
286  return [[self installScriptPrefix] stringByAppendingString:@"_install"];
287}
288
289+ (NSString *)postinstallScriptName {
290  return [[self installScriptPrefix] stringByAppendingString:@"_postinstall"];
291}
292
293@end  // Configuration
294
295
296@implementation KSInstallAction (PrivateMethods)
297
298// Returns the path to the directory that contains "ksadmin". Yes,
299// this is an ugly hack because it forces an ugly dependency on this
300// framework.  Specifically, the UpdateEngine framework must be
301// located in a directory that is a peer to a MacOS directory, which
302// must contain the "ksadmin" command. Yeah.  ... but hey, it might
303// make someone else's life a bit easier.
304- (NSString *)engineToolsPath {
305  NSBundle *framework = [NSBundle bundleForClass:[KSInstallAction class]];
306  return [NSString stringWithFormat:@"%@/../../MacOS", [framework bundlePath]];
307}
308
309// Returns a mount point path to be used for mounting the current dmg
310// ([self dmgPath]). The mountPoint is simply /Volumes/<product_id>-<code_hash>
311// The only trick is that the full mount point must be less than 90 characters
312// (this is a strange Apple limitation). So, we guarantee this by ensuring that
313// the product ID is never more than 50 characters. And since "/Volumes/" is 9
314// characters, and our hashes are 28 characters, we will always end up w/ a
315// mountpoint less than 90. But just to be sure, we have a GTMLoggerError that
316// will tell us.
317- (NSString *)mountPoint {
318  if (updateInfo_ == nil) return nil;  // nil means to use the default value
319
320  static const int kMaxProductIDLen = 50;
321  NSString *prodid = [updateInfo_ productID];
322  if ([prodid length] > kMaxProductIDLen)
323    prodid = [prodid substringToIndex:kMaxProductIDLen];
324
325  // Since the hash will be used as a path component, we must replace "/" chars
326  // with a char that's legal in path component names. We'll use underscores.
327  NSMutableString *legalHash = [[[updateInfo_ codeHash]
328                                 mutableCopy] autorelease];
329  NSRange wholeString = NSMakeRange(0, ([legalHash length]-1));
330  [legalHash replaceOccurrencesOfString:@"/"
331                             withString:@"_"
332                                options:0
333                                  range:wholeString];
334
335  NSString *mountPoint = [@"/Volumes/" stringByAppendingPathComponent:
336                          [NSString stringWithFormat:@"%@-%@",
337                           prodid, legalHash]];
338
339  // MNAMELEN is the max mount point name length (hint: it's 90)
340  if ([mountPoint length] >= MNAMELEN)
341    GTMLoggerError(@"Oops! mountPoint path is too long (>=90): %@", mountPoint);
342
343  return mountPoint;
344}
345
346// Add all of the objects in |updateInfo_| to the mutable dictionary |env|, but
347// prepend all of updateInfo_'s keys with the string @"KS_". This avoids the
348// possibility that someone's server config conflicts w/ an actual shell
349// variable.
350- (void)addUpdateInfoToEnvironment:(NSMutableDictionary *)env {
351  NSString *key = nil;
352  NSEnumerator *keyEnumerator = [updateInfo_ keyEnumerator];
353
354  while ((key = [keyEnumerator nextObject])) {
355    id value = [updateInfo_ objectForKey:key];
356
357    // Pick apart a ticket and add its pieces to the environment individually.
358    if ([value isKindOfClass:[KSTicket class]]) {
359      KSTicket *ticket = value;
360      [env setObject:[ticket productID]
361              forKey:@"KS_TICKET_PRODUCT_ID"];
362      [env setObject:[ticket determineVersion]
363              forKey:@"KS_TICKET_VERSION"];
364      [env setObject:[[ticket serverURL] description]
365              forKey:@"KS_TICKET_SERVER_URL"];
366      KSExistenceChecker *xc = [ticket existenceChecker];
367      if ([xc respondsToSelector:@selector(path)]) {
368        [env setObject:[xc path]
369                forKey:@"KS_TICKET_XC_PATH"];
370      }
371
372    } else {
373      [env setObject:[value description]
374              forKey:[@"KS_" stringByAppendingString:key]];
375    }
376  }
377}
378
379// Set environment variables of new-since-1.0 features that have been
380// added to UpdateEngine, so that install scripts can decide what features
381// to take advantage of.
382- (void)addSupportedFeaturesToEnvironment:(NSMutableDictionary *)env {
383  [env setObject:@"YES" forKey:@"KS_SUPPORTS_TAG"];
384}
385
386// Construct a dictionary of environment variables to be used when launching
387// the install script NSTasks.
388// A mutable dictionary is returned because the output of one task will
389// be added to the environment for the next task.
390- (NSMutableDictionary *)environment {
391  NSMutableDictionary *env = [NSMutableDictionary dictionary];
392
393  // Start off by adding all of the keys in |updateInfo_| to the environment,
394  // but prepend them all with some unique string.
395  [self addUpdateInfoToEnvironment:env];
396
397  // Set a good default path that starts with the directory containing
398  // UpdateEngine Tools, such as ksadmin. This allows the scripts to be able to
399  // use UpdateEngine commands without having to know where they're located.
400  NSString *toolsPath = [self engineToolsPath];
401  NSString *path = [NSString stringWithFormat:@"%@:/bin:/usr/bin", toolsPath];
402  [env setObject:path forKey:@"PATH"];
403
404  // Let scripts know if the user explicitly checked for updates.
405  [env setObject:(ui_ ? @"YES" : @"NO") forKey:@"KS_USER_INITIATED"];
406
407  // KS_INTERACTIVE means that the user has been involved in the process,
408  // either by the Prompt=true server configuration, or if the user has
409  // initiated the update process.
410  NSNumber *prompt = [updateInfo_ objectForKey:kServerPromptUser];
411  NSString *interactiveValue = @"NO";
412  if (ui_ || [prompt boolValue]) interactiveValue = @"YES";
413  [env setObject:interactiveValue forKey:@"KS_INTERACTIVE"];
414
415  // Add new-since-1.0 feature flags.
416  [self addSupportedFeaturesToEnvironment:env];
417
418  return env;
419}
420
421- (BOOL)isPathToExecutableFile:(NSString *)path {
422  NSFileManager *fm = [NSFileManager defaultManager];
423  BOOL isDir;
424
425  if ([fm fileExistsAtPath:path isDirectory:&isDir] && !isDir) {
426    return [fm isExecutableFileAtPath:path];
427  } else {
428    return NO;
429  }
430}
431
432@end  // PrivateMethods