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