| // Copyright (c) 2012 The Chromium OS Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import "AppController.h" |
| |
| #include <CoreFoundation/CoreFoundation.h> |
| #include <DiskArbitration/DiskArbitration.h> |
| #include <assert.h> |
| #include <errno.h> |
| #include <fcntl.h> |
| #include <mach/mach_time.h> |
| #include <objc/objc-runtime.h> |
| #include <pthread.h> |
| #include <stdarg.h> |
| #include <sys/socket.h> |
| #include <unistd.h> |
| #include <util.h> |
| |
| #include <vector> |
| |
| #include "DockTile.h" |
| #include "eintr_wrapper.h" |
| #include "scoped_ptr.h" |
| |
| namespace { |
| |
| NSString* const kConfigurationURLKey = @"ChromeOSRecoveryConfigurationURL"; |
| NSString* const kConfigurationURLString = |
| @"https://dl.google.com/dl/edgedl/chromeos/recovery/recovery.conf"; |
| // TODO: Soon this value will be in the config file. When that is the case, use |
| // that value with this as a backup. |
| NSString* const kRecoveryToolURLString = |
| @"http://www.google.com/chromeos/recovery"; |
| NSString* const kHelpURLString = |
| @"http://www.google.com/chromeos/recovery"; |
| |
| NSString* const kConfigFileVersion = @"recovery_tool_mac_version"; |
| NSString* const kConfigUpgradeMessage = @"recovery_tool_update"; |
| NSString* const kConfigChannel = @"channel"; |
| NSString* const kConfigName = @"name"; |
| NSString* const kConfigFile = @"file"; |
| NSString* const kConfigHWID = @"hwid"; |
| NSString* const kConfigSHA1 = @"sha1"; |
| NSString* const kConfigVersion = @"version"; |
| NSString* const kConfigDesc = @"desc"; |
| NSString* const kConfigZipSize = @"zipfilesize"; |
| NSString* const kConfigImageSize = @"filesize"; |
| NSString* const kConfigURL = @"url"; |
| |
| enum { |
| kInternalErrorUnexpectedEOF = 1 |
| }; |
| |
| const NSTimeInterval kNSTaskPollingInterval = 0.2; |
| |
| enum { |
| kSelectDeviceTab = 0, |
| kSelectUSBStickTab, |
| kWorkTab, |
| kDoneTab |
| }; |
| |
| void DiskAppeared(DADiskRef disk, void* context) { |
| AppController* appController = (AppController*)context; |
| |
| [appController diskAppeared:disk]; |
| } |
| |
| void DiskDisappeared(DADiskRef disk, void* context) { |
| AppController* appController = (AppController*)context; |
| |
| [appController diskDisappeared:disk]; |
| } |
| |
| void DiskPeek(DADiskRef disk, void* context) { |
| AppController* appController = (AppController*)context; |
| |
| [appController diskPeek:disk]; |
| } |
| |
| void DiskUnmounted(DADiskRef disk, DADissenterRef dissenter, void* context) { |
| AppController* appController = (AppController*)context; |
| |
| pthread_mutex_lock(&appController->diskArbMutex_); |
| appController->diskArbSuccess_ = |
| dissenter ? kDiskArbFailed : kDiskArbSucceeded; |
| pthread_mutex_unlock(&appController->diskArbMutex_); |
| pthread_cond_signal(&appController->diskArbCondition_); |
| } |
| |
| void DiskEjected(DADiskRef disk, DADissenterRef dissenter, void* context) { |
| AppController* appController = (AppController*)context; |
| |
| pthread_mutex_lock(&appController->diskArbMutex_); |
| appController->diskArbSuccess_ = |
| dissenter ? kDiskArbFailed : kDiskArbSucceeded; |
| pthread_mutex_unlock(&appController->diskArbMutex_); |
| pthread_cond_signal(&appController->diskArbCondition_); |
| } |
| |
| void DiskClaimed(DADiskRef disk, DADissenterRef dissenter, void* context) { |
| AppController* appController = (AppController*)context; |
| |
| pthread_mutex_lock(&appController->diskArbMutex_); |
| appController->diskArbSuccess_ = |
| dissenter ? kDiskArbFailed : kDiskArbSucceeded; |
| pthread_mutex_unlock(&appController->diskArbMutex_); |
| pthread_cond_signal(&appController->diskArbCondition_); |
| } |
| |
| DADissenterRef DiskClaimRevoked(DADiskRef disk, void* context) { |
| CFStringRef reason = |
| CFSTR("Hi. Sorry to bother you, but I'm busy overwriting the entire disk " |
| "here. There's nothing to claim but the smoldering ruins of bytes " |
| "that were in flash memory. Trust me, it's nothing that you want. " |
| "All the best. Toodles!"); |
| return DADissenterCreate(kCFAllocatorDefault, kDAReturnBusy, reason); |
| } |
| |
| void ProtectiveDiskClaimed(DADiskRef disk, DADissenterRef dissenter, |
| void* context) { |
| AppController* appController = (AppController*)context; |
| |
| if (!dissenter) |
| [appController->claimedSticks_ addObject:(id)disk]; |
| } |
| |
| DADissenterRef ProtectiveDiskClaimRevoked(DADiskRef disk, void* context) { |
| AppController* appController = (AppController*)context; |
| if (disk == appController->selectedStick_) |
| return DiskClaimRevoked(disk, context); |
| |
| [appController->claimedSticks_ removeObject:(id)disk]; |
| return NULL; |
| } |
| |
| id DictLookup(CFDictionaryRef dict, CFStringRef key) { |
| return (id)[(NSDictionary*)dict objectForKey:(NSString*)key]; |
| } |
| |
| struct NSAutoreleasePoolDestroy { |
| void operator()(NSAutoreleasePool* pool) const { |
| [pool drain]; |
| } |
| }; |
| typedef scoped_ptr_malloc<NSAutoreleasePool, NSAutoreleasePoolDestroy> |
| ScopedNSAutoreleasePool; |
| |
| struct PThreadMutexUnlock { |
| void operator()(pthread_mutex_t* mutex) const { |
| if (mutex) |
| pthread_mutex_unlock(mutex); |
| } |
| }; |
| typedef scoped_ptr_malloc<pthread_mutex_t, PThreadMutexUnlock> |
| ScopedPThreadMutexLock; |
| |
| struct PThreadMutexDestroy { |
| void operator()(pthread_mutex_t* mutex) const { |
| if (mutex) |
| pthread_mutex_destroy(mutex); |
| } |
| }; |
| typedef scoped_ptr_malloc<pthread_mutex_t, PThreadMutexDestroy> |
| ScopedPThreadMutexOwner; |
| |
| struct PThreadConditionDestroy { |
| void operator()(pthread_cond_t* cond) const { |
| if (cond) |
| pthread_cond_destroy(cond); |
| } |
| }; |
| typedef scoped_ptr_malloc<pthread_cond_t, PThreadConditionDestroy> |
| ScopedPThreadConditionOwner; |
| |
| struct DADiskUnclaimDoer { |
| void operator()(DADiskRef disk) const { |
| if (disk) |
| DADiskUnclaim(disk); |
| } |
| }; |
| typedef scoped_ptr_malloc<__DADisk, DADiskUnclaimDoer> |
| ScopedDADiskClaim; |
| |
| NSString* CommonPrefixOfStringArray(NSArray* array) { |
| NSUInteger items = [array count]; |
| if (!items) |
| return @""; |
| |
| NSString* firstString = [array objectAtIndex:0]; |
| NSString* prefixSoFar = @""; |
| for (NSUInteger index = 1; index <= [firstString length]; ++index) { |
| NSString* possiblePrefix = [firstString substringToIndex:index]; |
| for (NSUInteger item = 1; item < items; ++item) { |
| if (![[array objectAtIndex:item] hasPrefix:possiblePrefix]) { |
| return prefixSoFar; |
| } |
| } |
| prefixSoFar = possiblePrefix; |
| } |
| return prefixSoFar; |
| } |
| |
| NSString* SizeStringForValue(double size) { |
| NSArray* sizes = [NSArray arrayWithObjects: |
| NSLocalizedString(@"Size Bytes", nil), |
| NSLocalizedString(@"Size Kilobytes", nil), |
| NSLocalizedString(@"Size Megabytes", nil), |
| NSLocalizedString(@"Size Gigabytes", nil), |
| NSLocalizedString(@"Size Terabytes", nil), |
| NSLocalizedString(@"Size Petabytes", nil), |
| nil]; |
| |
| unsigned int dimension = 0; |
| const int kKilo = 1000; // we're doing New Apple Style sizes |
| while (size > kKilo && dimension < [sizes count] - 1) { |
| size /= kKilo; |
| dimension++; |
| } |
| |
| return [NSString stringWithFormat:@"%.2f%@", |
| size, [sizes objectAtIndex:dimension]]; |
| } |
| |
| // Opens a path for read/write using the authopen(1) command-line tool. Returns |
| // a valid file descriptor or -1 if an error occurs. If -1 is returned, errno |
| // holds the error value. |
| int OpenPathForReadWriteUsingAuthopen(const char* path) { |
| int sockets[2]; // [parent's end, child's end] |
| int result = socketpair(AF_UNIX, SOCK_STREAM, 0, sockets); |
| if (result == -1) |
| return -1; |
| |
| pid_t childPid = fork(); |
| if (childPid == -1) |
| return -1; |
| |
| if (childPid == 0) { // child |
| HANDLE_EINTR(dup2(sockets[1], STDOUT_FILENO)); |
| HANDLE_EINTR(close(sockets[0])); |
| HANDLE_EINTR(close(sockets[1])); |
| |
| const char authopenPath[] = "/usr/libexec/authopen"; |
| execl(authopenPath, |
| authopenPath, |
| "-stdoutpipe", |
| "-o", |
| [[NSString stringWithFormat:@"%d", O_RDWR] UTF8String], |
| path, |
| NULL); |
| _exit(errno); |
| } else { // parent |
| HANDLE_EINTR(close(sockets[1])); |
| int fd = -1; |
| |
| msghdr message = { 0 }; |
| const size_t kDataBufferSize = 1024; |
| char dataBuffer[kDataBufferSize]; |
| iovec ioVec[1]; |
| ioVec[0].iov_base = dataBuffer; |
| ioVec[0].iov_len = kDataBufferSize; |
| message.msg_iov = ioVec; |
| message.msg_iovlen = 1; |
| const socklen_t kCmsgSocketSize = (socklen_t)CMSG_SPACE(sizeof(int)); |
| char cmsgSocket[kCmsgSocketSize]; |
| message.msg_control = cmsgSocket; |
| message.msg_controllen = kCmsgSocketSize; |
| ssize_t size = HANDLE_EINTR(recvmsg(sockets[0], &message, 0)); |
| if (size > 0) { |
| cmsghdr* cmsgSocketHeader = CMSG_FIRSTHDR(&message); |
| // Paranoia. |
| if (cmsgSocketHeader && |
| cmsgSocketHeader->cmsg_level == SOL_SOCKET && |
| cmsgSocketHeader->cmsg_type == SCM_RIGHTS) |
| fd = *((int *)CMSG_DATA(cmsgSocketHeader)); |
| } |
| |
| int childStat; |
| result = HANDLE_EINTR(waitpid(childPid, &childStat, 0)); |
| HANDLE_EINTR(close(sockets[0])); |
| |
| if (result != -1 && WIFEXITED(childStat)) { |
| int exitStatus = WEXITSTATUS(childStat); |
| if (exitStatus) { |
| errno = exitStatus; |
| return -1; |
| } |
| } |
| |
| if (fd == -1) { |
| errno = ECANCELED; |
| return -1; |
| } |
| |
| return fd; |
| } |
| } |
| |
| } // namespace |
| |
| @implementation AppController |
| |
| - (void)awakeFromNib { |
| [stickTable_ setDoubleAction:@selector(stickWasDoubleClicked:)]; |
| [stickTable_ setTarget:self]; |
| |
| sticks_ = [[NSMutableArray alloc] init]; |
| claimedSticks_ = [[NSMutableArray alloc] init]; |
| |
| arbitrationSession_ = DASessionCreate(kCFAllocatorDefault); |
| if (!arbitrationSession_) { |
| // DASessionCreate is not documented to fail, but if it does we can't run. |
| [self whineAtUser:@"NoDiskArb"]; |
| [NSApp terminate:self]; |
| return; |
| } |
| |
| DARegisterDiskAppearedCallback(arbitrationSession_, |
| kDADiskDescriptionMatchMediaWhole, |
| DiskAppeared, |
| self); |
| |
| DARegisterDiskDisappearedCallback(arbitrationSession_, |
| kDADiskDescriptionMatchMediaWhole, |
| DiskDisappeared, |
| self); |
| |
| DARegisterDiskPeekCallback(arbitrationSession_, |
| NULL, |
| 0, |
| DiskPeek, |
| self); |
| |
| DASessionScheduleWithRunLoop(arbitrationSession_, |
| CFRunLoopGetMain(), |
| kCFRunLoopDefaultMode); |
| |
| // Cheesy RTL hackery to make things look decent; real tools would be nice. |
| if ([NSLocalizedString(@"UI Is RTL", nil) isEqualToString:@"YES"]) { |
| // Flip text fields if RTL. |
| [welcomeText_ setAlignment:NSRightTextAlignment]; |
| [selectStickText_ setAlignment:NSRightTextAlignment]; |
| [statusLine_ setAlignment:NSRightTextAlignment]; |
| [congratsText_ setAlignment:NSRightTextAlignment]; |
| |
| imageComboBox_ = imageComboBoxRTL_; |
| [imageComboBoxLTR_ removeFromSuperview]; |
| } else { |
| imageComboBox_ = imageComboBoxLTR_; |
| [imageComboBoxRTL_ removeFromSuperview]; |
| } |
| |
| |
| [window_ center]; |
| // The order in which objects are woken from the nib is undefined; if the |
| // window is shown now it may not yet be localized. Wait. |
| [window_ performSelector:@selector(makeKeyAndOrderFront:) |
| withObject:self |
| afterDelay:0]; |
| |
| [self loadConfig]; |
| } |
| |
| - (void)dealloc { |
| if (selectedStick_) |
| CFRelease(selectedStick_); |
| [sticks_ release]; |
| for (id disk in claimedSticks_) |
| DADiskUnclaim((DADiskRef)disk); |
| [claimedSticks_ release]; |
| if (arbitrationSession_) { |
| DASessionUnscheduleFromRunLoop(arbitrationSession_, |
| CFRunLoopGetMain(), |
| kCFRunLoopDefaultMode); |
| CFRelease(arbitrationSession_); |
| } |
| [download_ release]; |
| [downloadPath_ release]; |
| [imagePath_ release]; |
| [images_ release]; |
| [configData_ release]; |
| [configConnection_ release]; |
| |
| [super dealloc]; |
| } |
| |
| - (BOOL)isRTL { |
| return [NSLocalizedString(@"UI Is RTL", nil) isEqualToString:@"YES"]; |
| } |
| |
| - (IBAction)nextTab:(id)sender { |
| NSTabViewItem* currentTabView = [tabView_ selectedTabViewItem]; |
| NSInteger currentTab = [tabView_ indexOfTabViewItem:currentTabView]; |
| |
| // By default, clicking "Next" on a tab will take you to the next. A tab may |
| // customize this by implementing a method -[tabname]Next returning a BOOL |
| // that specifies if the next tab should be moved to (YES means proceed). |
| NSString* nextSelectorString = |
| [NSString stringWithFormat:@"%@Next", [currentTabView identifier]]; |
| SEL nextSelector = NSSelectorFromString(nextSelectorString); |
| |
| BOOL shouldAdvance = YES; |
| if ([self respondsToSelector:nextSelector]) { |
| // See http://www.red-sweater.com/blog/320/abusing-objective-c-with-class |
| // which refers to |
| // http://www.cocoabuilder.com/archive/cocoa/156384-objc-msgsend-problems-on-x86.html#156604 |
| typedef BOOL (*ImplReturningBOOL)(id, SEL); |
| ImplReturningBOOL sender = (ImplReturningBOOL)objc_msgSend; |
| shouldAdvance = sender(self, nextSelector); |
| } |
| |
| if (shouldAdvance) |
| [self switchToTabAtIndex:currentTab + 1]; |
| } |
| |
| - (IBAction)previousTab:(id)sender { |
| NSInteger currentTab = |
| [tabView_ indexOfTabViewItem:[tabView_ selectedTabViewItem]]; |
| |
| [self switchToTabAtIndex:currentTab - 1]; |
| } |
| |
| - (IBAction)done:(id)sender { |
| [NSApp terminate:sender]; |
| } |
| |
| - (void)switchToTabAtIndex:(NSInteger)index { |
| // In the nib, each NSTabViewItem has a view with a single NSView child of the |
| // desired size of the window. Switch while resizing the window. |
| NSView* newTabViewItemView = [[tabView_ tabViewItemAtIndex:index] view]; |
| assert([[newTabViewItemView subviews] count] == 1); |
| NSView* newContainerView = [[newTabViewItemView subviews] objectAtIndex:0]; |
| NSView* oldTabViewItemView = [[tabView_ selectedTabViewItem] view]; |
| assert([[oldTabViewItemView subviews] count] == 1); |
| NSView* oldContainerView = [[oldTabViewItemView subviews] objectAtIndex:0]; |
| |
| // Get a global delta. |
| NSRect oldRect = [oldContainerView convertRect:[oldContainerView bounds] |
| toView:nil]; |
| NSRect newRect = [newContainerView convertRect:[newContainerView bounds] |
| toView:nil]; |
| CGFloat delta = NSHeight(newRect) - NSHeight(oldRect); |
| NSRect windowRect = [window_ frame]; |
| windowRect.origin.y -= delta; |
| windowRect.size.height += delta; |
| |
| // Animate. |
| [NSAnimationContext beginGrouping]; |
| [[NSAnimationContext currentContext] setDuration:0.2]; // add a bit of zip |
| [[window_ animator] setFrame:windowRect display:YES]; |
| [[tabView_ animator] selectTabViewItemAtIndex:index]; |
| [NSAnimationContext endGrouping]; |
| } |
| |
| - (IBAction)showHelp:(id)sender { |
| [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:kHelpURLString]]; |
| } |
| |
| - (void)whineAtUser:(NSString*)errorName, ... { |
| NSString* message = |
| NSLocalizedString([errorName stringByAppendingString:@" Message"], nil); |
| NSString* informative = |
| NSLocalizedString([errorName stringByAppendingString:@" Informative"], |
| nil); |
| |
| const unichar ch = 0x2029; // U+2029 (PARAGRAPH SEPARATOR) |
| NSString* delim = [NSString stringWithCharacters:&ch length:1]; |
| NSString* both = [NSString stringWithFormat:@"%@%@%@", |
| message, delim, informative]; |
| |
| va_list args; |
| va_start(args, errorName); |
| both = [[[NSString alloc] initWithFormat:both |
| arguments:args] autorelease]; |
| va_end(args); |
| |
| NSArray* bothArray = [both componentsSeparatedByString:delim]; |
| message = [bothArray objectAtIndex:0]; |
| informative = [bothArray objectAtIndex:1]; |
| |
| NSAlert* alert = [[[NSAlert alloc] init] autorelease]; |
| [alert setMessageText:message]; |
| [alert setInformativeText:informative]; |
| |
| [alert addButtonWithTitle: |
| NSLocalizedString([errorName stringByAppendingString:@" OK"], nil)]; |
| [alert runModal]; |
| return; |
| } |
| |
| #pragma mark NSTableViewDelegate |
| |
| - (BOOL)tableView:(NSTableView*)tableView |
| shouldEditTableColumn:(NSTableColumn*)tableColumn |
| row:(NSInteger)rowIndex { |
| return NO; |
| } |
| |
| - (void)tableViewSelectionDidChange:(NSNotification*)notification { |
| [self stickSelectionChanged]; |
| } |
| |
| - (NSInteger)numberOfRowsInTableView:(NSTableView*)tableView { |
| return [self stickTableRowCount]; |
| } |
| |
| - (id)tableView:(NSTableView*)tableView |
| objectValueForTableColumn:(NSTableColumn*)tableColumn |
| row:(NSInteger)row { |
| return [self stickTableObjectValueForRow:row]; |
| } |
| |
| #pragma mark NSComboBoxDelegate |
| |
| - (void)comboBoxWillDismiss:(NSNotification*)notification { |
| imageComboBoxPopped_ = NO; |
| } |
| |
| - (void)comboBoxWillPopUp:(NSNotification*)notification { |
| imageComboBoxPopped_ = YES; |
| } |
| |
| - (void)controlTextDidChange:(NSNotification*)notification { |
| [self imageComboTextChanged]; |
| } |
| |
| - (NSArray*)control:(NSControl*)control |
| textView:(NSTextView*)textView |
| completions:(NSArray*)words |
| forPartialWordRange:(NSRange)charRange |
| indexOfSelectedItem:(NSInteger*)index { |
| return [NSArray array]; // no weird completions, please |
| } |
| |
| #pragma mark Select Device |
| |
| @synthesize loadingConfigFinished = loadingConfigFinished_; |
| |
| - (void)loadConfig { |
| NSBundle* bundle = [NSBundle mainBundle]; |
| NSString* configString = |
| [bundle objectForInfoDictionaryKey:kConfigurationURLKey]; |
| if (!configString) |
| configString = kConfigurationURLString; |
| |
| configData_ = [[NSMutableData alloc] init]; |
| NSURL* configURL = [NSURL URLWithString:configString]; |
| NSURLRequest* request = [NSURLRequest requestWithURL:configURL]; |
| configConnection_ = [[NSURLConnection alloc] initWithRequest:request |
| delegate:self |
| startImmediately:YES]; |
| if (!configConnection_) { |
| NSString* errorString = |
| NSLocalizedString(@"CantGetConfig Error NoConnection", nil); |
| [self whineAtUser:@"CantGetConfig", errorString]; |
| return; |
| } |
| } |
| |
| - (void)connection:(NSURLConnection*)connection |
| didFailWithError:(NSError*)error { |
| NSLog(@"Config file download failed with error: %@", error); |
| NSString* errorString = |
| NSLocalizedString(@"CantGetConfig Error DownloadError", nil); |
| [self whineAtUser:@"CantGetConfig", errorString]; |
| return; |
| } |
| |
| - (void)connection:(NSURLConnection*)connection |
| didReceiveData:(NSData*)data { |
| [configData_ appendData:data]; |
| } |
| |
| - (void)connectionDidFinishLoading:(NSURLConnection*)connection { |
| NSString* configString = |
| [[[NSString alloc] initWithData:configData_ |
| encoding:NSUTF8StringEncoding] autorelease]; |
| if (!configString) { |
| NSString* errorString = |
| NSLocalizedString(@"CantGetConfig Error StringCreate", nil); |
| [self whineAtUser:@"CantGetConfig", errorString]; |
| return; |
| } |
| |
| [self parseConfig:configString]; |
| |
| [self updateImageCombo]; |
| self.loadingConfigFinished = YES; |
| [imageComboBox_ becomeFirstResponder]; |
| } |
| |
| - (void)parseConfig:(NSString*)configString { |
| // Details of the config file format can be found in |
| // src/platform/vboot_reference/user_tools/README_recovery.txt . |
| |
| NSArray* stanzas = [configString componentsSeparatedByString:@"\n\n"]; |
| |
| // First stanza is autoupdate. |
| if ([stanzas count]) { |
| NSDictionary* autoupdate = [self parseStanza:[stanzas objectAtIndex:0] |
| withArrayKeys:nil]; |
| |
| NSString* configVersion = [autoupdate objectForKey:kConfigFileVersion]; |
| if (configVersion) { |
| NSBundle* bundle = [NSBundle mainBundle]; |
| NSString* bundleVersion = |
| [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"]; |
| |
| NSComparisonResult order = [self compareVersion:configVersion |
| toVersion:bundleVersion]; |
| |
| if (order == NSOrderedDescending) { |
| NSString* upgradeNote = [autoupdate objectForKey:kConfigUpgradeMessage]; |
| if (!upgradeNote) |
| upgradeNote = @""; |
| [self whineAtUser:@"VersionError", |
| bundleVersion, configVersion, upgradeNote]; |
| [[NSWorkspace sharedWorkspace] openURL: |
| [NSURL URLWithString:kRecoveryToolURLString]]; |
| [NSApp terminate:self]; |
| } |
| } |
| } |
| |
| NSSet* requiredKeys = |
| [NSSet setWithObjects:kConfigName, kConfigImageSize, kConfigFile, |
| kConfigSHA1, kConfigURL, nil]; |
| // Keys that may occur more than once. |
| NSSet* arrayKeys = [NSSet setWithObjects:kConfigHWID, kConfigURL, nil]; |
| |
| NSMutableDictionary* images = [[NSMutableDictionary alloc] init]; |
| // Skip the autoupdate stanza. |
| for (NSUInteger stanza = 1; stanza < [stanzas count]; ++stanza) { |
| NSDictionary* image = [self parseStanza:[stanzas objectAtIndex:stanza] |
| withArrayKeys:arrayKeys]; |
| |
| // Verify and add. |
| if ([image count]) { |
| BOOL isValid = YES; |
| NSArray* imageKeys = [image allKeys]; |
| for (NSString* key in requiredKeys) { |
| if (![imageKeys containsObject:key]) { |
| NSLog(@"Error: missing required key %@", key); |
| isValid = NO; |
| } |
| } |
| isValid &= [self isValidFilename:[image objectForKey:kConfigFile]]; |
| if (isValid) { |
| for (NSString* hwid in [image objectForKey:kConfigHWID]) |
| [images setObject:image forKey:hwid]; |
| } |
| } |
| } |
| images_ = images; |
| } |
| |
| - (NSDictionary*)parseStanza:(NSString*)stanza |
| withArrayKeys:(NSSet*)arrayKeys { |
| NSMutableDictionary* result = [NSMutableDictionary dictionary]; |
| NSArray* lines = [stanza componentsSeparatedByString:@"\n"]; |
| |
| for (NSString* line in lines) { |
| line = [line stringByTrimmingCharactersInSet: |
| [NSCharacterSet whitespaceAndNewlineCharacterSet]]; |
| |
| if ([line hasPrefix:@"#"]) |
| continue; |
| |
| NSArray* keyValue = [line componentsSeparatedByString:@"="]; |
| if ([keyValue count] != 2) |
| continue; |
| |
| NSString* key = [keyValue objectAtIndex:0]; |
| NSString* value = [keyValue objectAtIndex:1]; |
| if (arrayKeys && [arrayKeys containsObject:key]) { |
| NSMutableArray* items = [result objectForKey:key]; |
| if (!items) { |
| items = [NSMutableArray array]; |
| [result setObject:items forKey:key]; |
| } |
| [items addObject:value]; |
| } else { |
| id oldObject = [result objectForKey:key]; |
| if (oldObject) { |
| NSLog(@"Unexpectedly found two values for key %@; %@ and %@", |
| key, oldObject, value); |
| } |
| [result setObject:value forKey:key]; |
| } |
| } |
| |
| return result; |
| } |
| |
| - (NSComparisonResult)compareVersion:(NSString*)left |
| toVersion:(NSString*)right { |
| NSArray* leftComponents = [left componentsSeparatedByString:@"."]; |
| NSArray* rightComponents = [right componentsSeparatedByString:@"."]; |
| NSUInteger leftCount = [leftComponents count]; |
| NSUInteger rightCount = [rightComponents count]; |
| |
| for (NSUInteger i = 0; i < std::max(leftCount, rightCount); ++i) { |
| int leftComponent = 0, rightComponent = 0; |
| if (i < leftCount) |
| leftComponent = [[leftComponents objectAtIndex:i] intValue]; |
| if (i < rightCount) |
| rightComponent = [[rightComponents objectAtIndex:i] intValue]; |
| |
| if (leftComponent < rightComponent) |
| return NSOrderedAscending; |
| else if (leftComponent > rightComponent) |
| return NSOrderedDescending; |
| } |
| |
| return NSOrderedSame; |
| } |
| |
| - (BOOL)isValidFilename:(NSString*)filename { |
| // Paranoia! |
| if (![filename length]) |
| return NO; |
| if ([filename isEqualToString:@"."] || [filename isEqualToString:@".."]) |
| return NO; |
| if ([filename hasPrefix:@"-"]) // will be interpreted as switch |
| return NO; |
| NSCharacterSet* shadyCharacters = |
| [NSCharacterSet characterSetWithCharactersInString:@"/*?["]; |
| if ([filename rangeOfCharacterFromSet:shadyCharacters].location != NSNotFound) |
| return NO; |
| |
| return YES; |
| } |
| |
| - (BOOL)selectDeviceNextEnabled { |
| return [images_ objectForKey:[imageComboBox_ stringValue]] != nil; |
| } |
| |
| - (IBAction)selectLocalFile:(id)sender { |
| NSOpenPanel* openPanel = [NSOpenPanel openPanel]; |
| [openPanel setAllowsMultipleSelection:NO]; |
| [openPanel setCanChooseDirectories:NO]; |
| [openPanel setCanCreateDirectories:NO]; |
| [openPanel setCanChooseFiles:YES]; |
| if (!([[NSApp currentEvent] modifierFlags] & NSAlternateKeyMask)) { |
| // Option key = allow burning of any file. |
| [openPanel setDelegate:self]; |
| } |
| |
| [openPanel setPrompt:NSLocalizedString(@"SelectLocal Button", nil)]; |
| [openPanel setMessage:NSLocalizedString(@"SelectLocal File", nil)]; |
| |
| NSInteger result = [openPanel runModal]; |
| if (result == NSFileHandlingPanelCancelButton) |
| return; |
| |
| imagePath_ = [[[openPanel filenames] objectAtIndex:0] copy]; |
| isImageLocal_ = YES; |
| NSDictionary* attrs = |
| [[NSFileManager defaultManager] attributesOfItemAtPath:imagePath_ |
| error:nil]; |
| imageSize_ = [[attrs objectForKey:NSFileSize] longLongValue]; |
| |
| [self switchToTabAtIndex:kSelectUSBStickTab]; |
| } |
| |
| - (BOOL)panel:(id)sender shouldShowFilename:(NSString*)filename { |
| BOOL isDirectory; |
| BOOL fileExists = |
| [[NSFileManager defaultManager] fileExistsAtPath:filename |
| isDirectory:&isDirectory]; |
| if (!fileExists) // huh? |
| return NO; |
| if (isDirectory) |
| return ![[NSWorkspace sharedWorkspace] isFilePackageAtPath:filename]; |
| |
| if (![filename hasSuffix:@".bin"]) |
| return NO; |
| |
| const int kSectorSize = 512; |
| std::vector<unsigned char> buffer(kSectorSize); |
| int fd = HANDLE_EINTR(open([filename fileSystemRepresentation], O_RDONLY)); |
| if (fd < 0) |
| return NO; |
| |
| ssize_t bytesRead = 0; |
| while (bytesRead < kSectorSize) { |
| ssize_t bytesJustRead = HANDLE_EINTR(read(fd, |
| &buffer[0] + bytesRead, |
| kSectorSize - bytesRead)); |
| if (bytesJustRead <= 0) |
| break; |
| bytesRead += bytesJustRead; |
| } |
| close(fd); |
| |
| if (bytesRead < kSectorSize) |
| return NO; |
| |
| // For an MBR (or GPT) disk image, the first sector will end with 0x55AA. |
| // http://en.wikipedia.org/wiki/Master_boot_record |
| // http://en.wikipedia.org/wiki/GUID_Partition_Table#Legacy_MBR_.28LBA_0.29 |
| return buffer[kSectorSize - 2] == 0x55 && buffer[kSectorSize - 1] == 0xAA; |
| } |
| |
| - (NSArray*)matchingImageHwids { |
| NSArray* allHwids = [images_ allKeys]; |
| |
| NSString* text = [imageComboBox_ stringValue]; |
| if (![text length]) |
| return allHwids; |
| |
| // First, do any hwids start with what was typed? |
| NSPredicate* predicate = |
| [NSPredicate predicateWithFormat:@"SELF BEGINSWITH %@", text]; |
| NSArray* matches = [allHwids filteredArrayUsingPredicate:predicate]; |
| if ([matches count]) |
| return matches; |
| |
| // If not, try substring. |
| predicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS %@", text]; |
| return [allHwids filteredArrayUsingPredicate:predicate]; |
| } |
| |
| - (void)updateImageCombo { |
| [imageComboBox_ removeAllItems]; |
| |
| NSArray* matches = [[self matchingImageHwids] sortedArrayUsingSelector: |
| @selector(localizedCaseInsensitiveCompare:)]; |
| [imageComboBox_ addItemsWithObjectValues:matches]; |
| } |
| |
| - (void)imageComboTextChanged { |
| // Automatically capitalize user input. |
| NSString* text = [imageComboBox_ stringValue]; |
| [imageComboBox_ setStringValue:[text uppercaseString]]; |
| |
| // Update list of matching HWIDs. |
| [self updateImageCombo]; |
| |
| // Pop up completions. |
| if (!imageComboBoxPopped_) { |
| [[imageComboBox_ cell] performSelector:@selector(popUp:) |
| withObject:nil |
| afterDelay:0]; |
| } |
| } |
| |
| - (IBAction)imageWasSelected:(id)sender { |
| NSArray* matches = [self matchingImageHwids]; |
| if ([matches count] == 1) { |
| [imageComboBox_ setStringValue:[matches objectAtIndex:0]]; |
| } |
| |
| [self willChangeValueForKey:@"selectDeviceNextEnabled"]; |
| [self didChangeValueForKey:@"selectDeviceNextEnabled"]; |
| } |
| |
| - (BOOL)selectDeviceNext { |
| if (![self selectDeviceNextEnabled]) |
| return NO; |
| |
| isImageLocal_ = NO; |
| image_ = [images_ objectForKey:[imageComboBox_ stringValue]]; |
| imageSize_ = [[image_ objectForKey:kConfigImageSize] longLongValue]; |
| |
| return YES; |
| } |
| |
| #pragma mark Select USB Stick |
| |
| - (BOOL)selectUSBStickNextEnabled { |
| return [stickTable_ selectedRow] != -1; |
| } |
| |
| - (BOOL)insertUSBStickHidden { |
| return [sticks_ count] > 0; |
| } |
| |
| - (void)diskAppeared:(DADiskRef)disk { |
| if ([self isAcceptableMedia:disk]) { |
| BOOL notify = [sticks_ count] == 0; |
| if (notify) |
| [self willChangeValueForKey:@"insertUSBStickHidden"]; |
| [sticks_ addObject:(id)disk]; |
| [stickTable_ reloadData]; |
| if (notify) |
| [self didChangeValueForKey:@"insertUSBStickHidden"]; |
| } |
| } |
| |
| - (void)diskDisappeared:(DADiskRef)disk { |
| BOOL notify = [sticks_ count] == 1; |
| if (notify) |
| [self willChangeValueForKey:@"insertUSBStickHidden"]; |
| [sticks_ removeObject:(id)disk]; |
| [stickTable_ reloadData]; |
| if (notify) |
| [self didChangeValueForKey:@"insertUSBStickHidden"]; |
| } |
| |
| - (void)diskPeek:(DADiskRef)disk { |
| // Claim all USB sticks. This way if the user plugs in a blank stick to be |
| // used, they won't get distracted by DiskArb complaining. (This won't prevent |
| // it from mounting, though, if it has a mountable filesystem.) |
| if ([self isAcceptableMedia:disk]) { |
| DADiskClaim(disk, |
| kDADiskClaimOptionDefault, |
| ProtectiveDiskClaimRevoked, |
| self, |
| ProtectiveDiskClaimed, |
| self); |
| } |
| } |
| |
| - (BOOL)isAcceptableMedia:(DADiskRef)disk { |
| CFDictionaryRef info = DADiskCopyDescription(disk); |
| |
| // Be excruciatingly paranoid about what we consider to be a USB stick. |
| NSNumber* internal = DictLookup(info, kDADiskDescriptionDeviceInternalKey); |
| NSString* protocol = DictLookup(info, kDADiskDescriptionDeviceProtocolKey); |
| NSString* ioRegPath = DictLookup(info, kDADiskDescriptionDevicePathKey); |
| NSNumber* ejectable = DictLookup(info, kDADiskDescriptionMediaEjectableKey); |
| NSNumber* removable = DictLookup(info, kDADiskDescriptionMediaRemovableKey); |
| NSNumber* whole = DictLookup(info, kDADiskDescriptionMediaWholeKey); |
| NSString* kind = DictLookup(info, kDADiskDescriptionMediaKindKey); |
| |
| // A drive is a USB stick iff: |
| // - it is not internal |
| // - it is attached to the USB bus |
| // - it is ejectable (because it will be ejected after written to) |
| // - it is removable |
| // - it is the whole drive (although the use of |
| // kDADiskDescriptionMatchMediaWhole should have ensured this) |
| // - it is of type IOMedia (external DVD drives and the like are IOCDMedia or |
| // IODVDMedia) |
| |
| BOOL isUSBStick = ![internal boolValue] && |
| [protocol isEqualToString:@"USB"] && |
| [ejectable boolValue] && |
| [removable boolValue] && |
| [whole boolValue] && |
| [kind isEqualToString:@"IOMedia"]; |
| |
| // A drive is an SD card iff: |
| // - it is attached to the USB bus |
| // - it is ejectable (because it will be ejected after written to) |
| // - it is removable |
| // - it is the whole drive (although the use of |
| // kDADiskDescriptionMatchMediaWhole should have ensured this) |
| // - it is of type IOMedia (external DVD drives and the like are IOCDMedia or |
| // IODVDMedia) |
| // - the IORegistry device path contains "AppleUSBCardReader" |
| |
| BOOL isSDCard = [protocol isEqualToString:@"USB"] && |
| [ejectable boolValue] && |
| [removable boolValue] && |
| [whole boolValue] && |
| [kind isEqualToString:@"IOMedia"] && |
| [self hasIORegPathOfSDCard:ioRegPath]; |
| |
| CFRelease(info); |
| return isUSBStick || isSDCard; |
| } |
| |
| - (BOOL)hasIORegPathOfSDCard:(NSString*)path { |
| return [path rangeOfString:@"AppleUSBCardReader"].location != NSNotFound; |
| } |
| |
| - (NSInteger)stickTableRowCount { |
| return [sticks_ count]; |
| } |
| |
| - (id)stickTableObjectValueForRow:(NSInteger)row { |
| DADiskRef disk = (DADiskRef)[sticks_ objectAtIndex:row]; |
| |
| return [self descriptionForDisk:disk]; |
| } |
| |
| - (void)stickSelectionChanged { |
| [self willChangeValueForKey:@"selectUSBStickNextEnabled"]; |
| [self didChangeValueForKey:@"selectUSBStickNextEnabled"]; |
| } |
| |
| - (IBAction)stickWasDoubleClicked:(id)sender { |
| [self nextTab:sender]; |
| } |
| |
| - (BOOL)selectUSBStickNext { |
| if ([stickTable_ selectedRow] == -1) |
| return NO; |
| |
| DADiskRef disk = (DADiskRef)[sticks_ objectAtIndex:[stickTable_ selectedRow]]; |
| NSString* desc = [self descriptionForDisk:disk]; |
| CFDictionaryRef info = DADiskCopyDescription(disk); |
| off_t diskSize = |
| [DictLookup(info, kDADiskDescriptionMediaSizeKey) longLongValue]; |
| BOOL writable = |
| [DictLookup(info, kDADiskDescriptionMediaWritableKey) boolValue]; |
| CFRelease(info); |
| |
| if (diskSize < imageSize_) { |
| NSString* imageSizeString = SizeStringForValue(imageSize_); |
| [self whineAtUser:@"TooSmallAlert", desc, imageSizeString]; |
| return NO; |
| } |
| |
| if (!writable) { |
| [self whineAtUser:@"NotWritable", desc]; |
| return NO; |
| } |
| |
| NSAlert* alert = [[[NSAlert alloc] init] autorelease]; |
| [alert setAlertStyle:NSCriticalAlertStyle]; |
| NSString* message = NSLocalizedString(@"WriteAlert Message", nil); |
| message = [NSString stringWithFormat:message, desc]; |
| [alert setMessageText:message]; |
| [alert setInformativeText:NSLocalizedString(@"WriteAlert Informative", nil)]; |
| |
| NSButton* button = |
| [alert addButtonWithTitle:NSLocalizedString(@"WriteAlert OK", nil)]; |
| [button setKeyEquivalent:@""]; |
| button = |
| [alert addButtonWithTitle:NSLocalizedString(@"WriteAlert Cancel", nil)]; |
| [button setKeyEquivalent:@"\e"]; |
| |
| NSInteger result = [alert runModal]; |
| if (result == NSAlertSecondButtonReturn) { |
| return NO; |
| } |
| |
| CFRetain(disk); |
| selectedStick_ = disk; |
| |
| [self startWriteImage]; |
| |
| return YES; |
| } |
| |
| - (NSString*)descriptionForDisk:(DADiskRef)disk { |
| NSString* diskName; |
| |
| CFDictionaryRef info = DADiskCopyDescription(disk); |
| NSString* ioRegPath = DictLookup(info, kDADiskDescriptionDevicePathKey); |
| if ([self hasIORegPathOfSDCard:ioRegPath]) |
| diskName = NSLocalizedString(@"Label SD Card", nil); |
| else { |
| NSString* vendor = DictLookup(info, kDADiskDescriptionDeviceVendorKey); |
| NSString* model = DictLookup(info, kDADiskDescriptionDeviceModelKey); |
| diskName = [NSString stringWithFormat:@"%@ %@", vendor, model]; |
| } |
| |
| double size = [DictLookup(info, kDADiskDescriptionMediaSizeKey) doubleValue]; |
| |
| NSString* desc = [NSString stringWithFormat:@"%@ (%@)", |
| diskName, SizeStringForValue(size)]; |
| CFRelease(info); |
| |
| return desc; |
| } |
| |
| #pragma mark Work |
| |
| - (IBAction)knockItOff:(id)sender { |
| // In response to this flag the active task is to stop what it's doing, and |
| // call -cleanUp:. |
| stopping_ = YES; |
| } |
| |
| - (void)failWithInfo:(FailureInfo*)info { |
| NSData* failureInfoData = [NSData dataWithBytes:info |
| length:sizeof(FailureInfo)]; |
| [self performSelectorOnMainThread:@selector(cleanUp:) |
| withObject:failureInfoData |
| waitUntilDone:NO]; |
| } |
| |
| - (void)cleanUp:(NSData*)info { |
| stopping_ = NO; |
| |
| [download_ autorelease]; |
| download_ = nil; |
| |
| if (downloadPath_) { |
| [[NSFileManager defaultManager] removeItemAtPath:downloadPath_ error:nil]; |
| [downloadPath_ release]; |
| downloadPath_ = nil; |
| } |
| |
| if (!isImageLocal_ && imagePath_) { |
| [[NSFileManager defaultManager] removeItemAtPath:imagePath_ error:nil]; |
| [imagePath_ release]; |
| imagePath_ = nil; |
| } |
| |
| if (info) { |
| const FailureInfo* failureInfo = (const FailureInfo*)[info bytes]; |
| |
| NSAlert* alert = [[[NSAlert alloc] init] autorelease]; |
| NSString* messageKey = |
| [NSString stringWithUTF8String:failureInfo->failureStep]; |
| NSString* message = NSLocalizedString(messageKey, nil); |
| if (failureInfo->errorDomain != FailureInfo::kNoErrorCodeAvailable) { |
| const char* errorString; |
| switch (failureInfo->errorDomain) { |
| case FailureInfo::kErrnoError: { |
| errorString = strerror(failureInfo->errorCode); |
| break; |
| } |
| case FailureInfo::kReturnValueError: { |
| NSString* error = NSLocalizedString(@"Failure Termination Status", |
| nil); |
| error = [NSString stringWithFormat:error, failureInfo->errorCode]; |
| errorString = [error UTF8String]; |
| break; |
| } |
| case FailureInfo::kInternalError: { |
| NSString* errorKey = [NSString stringWithFormat: |
| @"Failure Internal Error %d", failureInfo->errorCode]; |
| errorString = [NSLocalizedString(errorKey, nil) UTF8String]; |
| break; |
| } |
| default: |
| assert(0); |
| } |
| message = [NSString stringWithFormat:message, errorString]; |
| } |
| [alert setMessageText:message]; |
| |
| [alert addButtonWithTitle:NSLocalizedString(@"Failure Try Again", nil)]; |
| [alert addButtonWithTitle:NSLocalizedString(@"Failure Quit", nil)]; |
| |
| NSInteger result = [alert runModal]; |
| if (result == NSAlertSecondButtonReturn) { |
| [NSApp terminate:self]; |
| return; |
| } |
| } |
| |
| [self switchToTabAtIndex:kSelectUSBStickTab]; |
| } |
| |
| - (void)startWriteImage { |
| SetDockTileProgress(kDockTileVisible, kDockTileIndeterminate, 0); |
| if (isImageLocal_) |
| [self unpackComplete]; |
| else |
| [self doDownload]; |
| } |
| |
| - (void)doDownload { |
| status_.statusText = "Status Downloading"; |
| status_.progressIndeterminate = YES; |
| status_.progressBytes = 0; |
| status_.attempt = urlIndex_; |
| [self sendStatusUpdate]; |
| |
| NSString* archiveURLString = |
| [[image_ objectForKey:kConfigURL] objectAtIndex:urlIndex_]; |
| NSURL* archiveURL = [NSURL URLWithString:archiveURLString]; |
| NSURLRequest* request = [NSURLRequest requestWithURL:archiveURL]; |
| download_ = [[NSURLDownload alloc] initWithRequest:request |
| delegate:self]; |
| NSString* tempDir = NSTemporaryDirectory(); |
| NSArray* components = [archiveURLString componentsSeparatedByString:@"/"]; |
| NSString* fileName = [components objectAtIndex:[components count] - 1]; |
| downloadPath_ = [[tempDir stringByAppendingPathComponent:fileName] retain]; |
| [download_ setDestination:downloadPath_ |
| allowOverwrite:YES]; |
| } |
| |
| - (void)download:(NSURLDownload*)download |
| didReceiveResponse:(NSURLResponse*)response { |
| long long expectedSize = [response expectedContentLength]; |
| if (expectedSize != NSURLResponseUnknownLength) { |
| [progressIndicator_ setMinValue:0]; |
| [progressIndicator_ setMaxValue:expectedSize]; |
| status_.progressIndeterminate = NO; |
| } |
| |
| if (stopping_) { |
| [download_ cancel]; |
| [self performSelectorOnMainThread:@selector(cleanUp:) |
| withObject:nil |
| waitUntilDone:NO]; |
| } |
| } |
| |
| - (void)download:(NSURLDownload*)download |
| didReceiveDataOfLength:(NSUInteger)length { |
| status_.progressBytes += length; |
| [self sendStatusUpdate]; |
| |
| if (stopping_) { |
| [download_ cancel]; |
| [self performSelectorOnMainThread:@selector(cleanUp:) |
| withObject:nil |
| waitUntilDone:NO]; |
| } |
| } |
| |
| - (BOOL)download:(NSURLDownload*)download |
| shouldDecodeSourceDataOfMIMEType:(NSString*)encodingType { |
| return NO; |
| } |
| |
| - (void)downloadDidFinish:(NSURLDownload*)download { |
| [self performSelectorInBackground:@selector(verify) |
| withObject:nil]; |
| } |
| |
| - (void)download:(NSURLDownload*)download |
| didFailWithError:(NSError*)error { |
| NSLog(@"Image download failed with error: %@", error); |
| [download_ autorelease]; |
| download_ = nil; |
| |
| ++urlIndex_; |
| if (urlIndex_ < [[image_ objectForKey:kConfigURL] count]) { |
| [self doDownload]; |
| } else { |
| FailureInfo failureInfo; |
| failureInfo.failureStep = "Failure Downloading"; |
| failureInfo.errorDomain = FailureInfo::kNoErrorCodeAvailable; |
| [self failWithInfo:&failureInfo]; |
| return; |
| } |
| } |
| |
| - (void)verify { |
| ScopedNSAutoreleasePool poolOwner([[NSAutoreleasePool alloc] init]); |
| status_.statusText = "Status Verifying Image"; |
| status_.progressIndeterminate = YES; |
| status_.progressBytes = 0; |
| status_.attempt = 0; |
| [self sendStatusUpdate]; |
| |
| NSTask* task = [[[NSTask alloc] init] autorelease]; |
| [task setLaunchPath: @"/usr/bin/openssl"]; |
| |
| NSArray* arguments = [NSArray arrayWithObjects:@"sha1", downloadPath_, nil]; |
| [task setArguments:arguments]; |
| |
| NSPipe* pipe = [NSPipe pipe]; |
| [task setStandardOutput:pipe]; |
| |
| [task launch]; |
| while ([task isRunning] && !stopping_) { |
| ScopedNSAutoreleasePool poolOwner([[NSAutoreleasePool alloc] init]); |
| NSDate* date = [NSDate dateWithTimeIntervalSinceNow:kNSTaskPollingInterval]; |
| [[NSRunLoop currentRunLoop] runUntilDate:date]; |
| } |
| if (stopping_) { |
| [task terminate]; |
| [self performSelectorOnMainThread:@selector(cleanUp:) |
| withObject:nil |
| waitUntilDone:NO]; |
| return; |
| } |
| int status = [task terminationStatus]; |
| |
| if (status) { |
| FailureInfo failureInfo; |
| failureInfo.failureStep = "Failure Verifying Return Value"; |
| failureInfo.errorDomain = FailureInfo::kReturnValueError; |
| failureInfo.errorCode = status; |
| [self failWithInfo:&failureInfo]; |
| return; |
| } |
| |
| NSData* output = [[pipe fileHandleForReading] readDataToEndOfFile]; |
| NSString* outputString = |
| [[[NSString alloc] initWithData:output |
| encoding:NSUTF8StringEncoding] autorelease]; |
| outputString = [outputString stringByTrimmingCharactersInSet: |
| [NSCharacterSet whitespaceAndNewlineCharacterSet]]; |
| NSArray* components = [outputString componentsSeparatedByString:@" "]; |
| NSString* actualSHA1 = [components objectAtIndex:[components count] - 1]; |
| if (![actualSHA1 isEqualToString:[image_ objectForKey:kConfigSHA1]]) { |
| FailureInfo failureInfo; |
| failureInfo.failureStep = "Failure Verifying SHA Mismatch"; |
| failureInfo.errorDomain = FailureInfo::kNoErrorCodeAvailable; |
| [self failWithInfo:&failureInfo]; |
| return; |
| } |
| |
| [self performSelectorOnMainThread:@selector(verifyComplete) |
| withObject:nil |
| waitUntilDone:NO]; |
| } |
| |
| - (void)verifyComplete { |
| [progressIndicator_ setMinValue:0]; |
| [progressIndicator_ setMaxValue:imageSize_]; |
| [self performSelectorInBackground:@selector(unpack) |
| withObject:nil]; |
| } |
| |
| - (void)unpack { |
| ScopedNSAutoreleasePool poolOwner([[NSAutoreleasePool alloc] init]); |
| status_.statusText = "Status Unpacking"; |
| status_.progressIndeterminate = YES; |
| status_.progressBytes = 0; |
| [self sendStatusUpdate]; |
| |
| NSFileManager* fileManager = [[[NSFileManager alloc] init] autorelease]; |
| NSString* filename = [image_ objectForKey:kConfigFile]; |
| imagePath_ = |
| [[NSTemporaryDirectory() stringByAppendingPathComponent:filename] retain]; |
| |
| NSTask* task = [[[NSTask alloc] init] autorelease]; |
| [task setLaunchPath: @"/usr/bin/unzip"]; |
| NSArray* arguments = [NSArray arrayWithObjects: |
| @"-o", // overwrite existing |
| @"-qq", // quiet, no console spew |
| @"-d", NSTemporaryDirectory(), // target directory |
| downloadPath_, |
| filename, |
| nil]; |
| [task setArguments:arguments]; |
| |
| [task launch]; |
| while ([task isRunning] && !stopping_) { |
| ScopedNSAutoreleasePool poolOwner([[NSAutoreleasePool alloc] init]); |
| NSDate* date = [NSDate dateWithTimeIntervalSinceNow:kNSTaskPollingInterval]; |
| [[NSRunLoop currentRunLoop] runUntilDate:date]; |
| |
| NSDictionary* attrs = |
| [fileManager attributesOfItemAtPath:imagePath_ error:nil]; |
| NSNumber* fileSize = [attrs objectForKey:NSFileSize]; |
| if (fileSize) { |
| status_.progressBytes = [fileSize longLongValue]; |
| status_.progressIndeterminate = NO; |
| [self sendStatusUpdate]; |
| } |
| } |
| if (stopping_) { |
| [task terminate]; |
| [self performSelectorOnMainThread:@selector(cleanUp:) |
| withObject:nil |
| waitUntilDone:NO]; |
| return; |
| } |
| int status = [task terminationStatus]; |
| |
| [fileManager removeItemAtPath:downloadPath_ error:nil]; |
| |
| if (status) { |
| FailureInfo failureInfo; |
| failureInfo.failureStep = "Failure Unpacking"; |
| failureInfo.errorDomain = FailureInfo::kReturnValueError; |
| failureInfo.errorCode = status; |
| [self failWithInfo:&failureInfo]; |
| return; |
| } |
| |
| NSDictionary* attrs = |
| [fileManager attributesOfItemAtPath:imagePath_ error:nil]; |
| long long actualSize = [[attrs objectForKey:NSFileSize] longLongValue]; |
| if (actualSize != imageSize_) { |
| // Paranoia? The actual size of the image is not the size claimed in the |
| // config file, yet the zip file passed a SHA-1. Assume that the config file |
| // got it wrong, but it won't hurt to complain. |
| NSLog(@"File size doesn't match; actual size is %lld, while the " |
| "configuration file claimed a size of %lld", actualSize, imageSize_); |
| imageSize_ = actualSize; |
| } |
| |
| [self performSelectorOnMainThread:@selector(unpackComplete) |
| withObject:nil |
| waitUntilDone:NO]; |
| } |
| |
| - (void)unpackComplete { |
| [progressIndicator_ setMinValue:0]; |
| [progressIndicator_ setMaxValue:imageSize_]; |
| [self performSelectorInBackground:@selector(writeImage) |
| withObject:nil]; |
| } |
| |
| namespace { |
| |
| struct FileDelete { |
| void operator()(NSString* path) const { |
| if (path) { |
| NSFileManager* fileManager = [[NSFileManager alloc] init]; |
| [fileManager removeItemAtPath:path error:nil]; |
| [fileManager release]; |
| } |
| } |
| }; |
| typedef scoped_ptr_malloc<NSString, FileDelete> FileOwner; |
| |
| } // namespace |
| |
| - (void)writeImage { |
| // Why a mutex/condition? |
| // |
| // DiskArb functions typically work this way: You call them (e.g. |
| // DADiskClaim), and go on your way. Sometime in the future, on the main |
| // thread, they call the indicated callback function specifying whether the |
| // operation succeeded (if there were no dissenters) or failed. To avoid |
| // having to break up the code into multiple functions, a mutex is used to |
| // protect a condition, this thread sleeps on the condition, and when DiskArb |
| // calls the callback the callback wakes up this thread which continues. |
| // |
| // Why a recursive mutex? |
| // |
| // Because the previous paragraph contains a lie. Most of the time DiskArb |
| // does callbacks on the main thread. However, in certain error cases (e.g. |
| // the user ripped the USB stick from the socket), calling the DiskArb |
| // function results in an immediate callback on the thread that made the |
| // DiskArb call. A recursive mutex is then needed to avoid deadlock. |
| ScopedNSAutoreleasePool poolOwner([[NSAutoreleasePool alloc] init]); |
| pthread_mutexattr_t mutexAttr; |
| pthread_mutexattr_init(&mutexAttr); |
| pthread_mutexattr_settype(&mutexAttr, PTHREAD_MUTEX_RECURSIVE); |
| pthread_mutex_init(&diskArbMutex_, &mutexAttr); |
| pthread_mutexattr_destroy(&mutexAttr); |
| ScopedPThreadMutexOwner mutexOwner(&diskArbMutex_); |
| pthread_cond_init(&diskArbCondition_, NULL); |
| ScopedPThreadConditionOwner condOwner(&diskArbCondition_); |
| pthread_mutex_lock(&diskArbMutex_); |
| ScopedPThreadMutexLock mutexLock(&diskArbMutex_); |
| FileOwner imageOwner(isImageLocal_ ? nil : imagePath_); |
| FailureInfo failureInfo = { 0 }; |
| |
| CFDictionaryRef info = DADiskCopyDescription(selectedStick_); |
| off_t blockSize = |
| [DictLookup(info, kDADiskDescriptionMediaBlockSizeKey) longLongValue]; |
| CFRelease(info); |
| |
| // Claim the disk. |
| if (![claimedSticks_ containsObject:(id)selectedStick_]) { |
| status_.statusText = "Status Claiming"; |
| status_.progressIndeterminate = YES; |
| [self sendStatusUpdate]; |
| DADiskClaim(selectedStick_, |
| kDADiskClaimOptionDefault, |
| DiskClaimRevoked, |
| self, |
| DiskClaimed, |
| self); |
| diskArbSuccess_ = kDiskArbUnknown; |
| while (diskArbSuccess_ == kDiskArbUnknown) |
| pthread_cond_wait(&diskArbCondition_, &diskArbMutex_); |
| if (diskArbSuccess_ == kDiskArbFailed) { |
| failureInfo.failureStep = "Failure Claiming"; |
| failureInfo.errorDomain = FailureInfo::kNoErrorCodeAvailable; |
| [self failWithInfo:&failureInfo]; |
| return; |
| } |
| } else { |
| [claimedSticks_ removeObject:(id)selectedStick_]; |
| } |
| ScopedDADiskClaim diskClaim(selectedStick_); |
| |
| // Unmount the disk. |
| status_.statusText = "Status Unmounting"; |
| status_.progressIndeterminate = YES; |
| [self sendStatusUpdate]; |
| diskArbSuccess_ = kDiskArbUnknown; |
| DADiskUnmount(selectedStick_, |
| kDADiskUnmountOptionForce | kDADiskUnmountOptionWhole, |
| DiskUnmounted, |
| self); |
| while (diskArbSuccess_ == kDiskArbUnknown) |
| pthread_cond_wait(&diskArbCondition_, &diskArbMutex_); |
| if (diskArbSuccess_ == kDiskArbFailed) { |
| failureInfo.failureStep = "Failure Unmounting"; |
| failureInfo.errorDomain = FailureInfo::kNoErrorCodeAvailable; |
| [self failWithInfo:&failureInfo]; |
| return; |
| } |
| |
| // Open the disk and image. |
| int imagefd = HANDLE_EINTR(open([imagePath_ fileSystemRepresentation], |
| O_RDONLY)); |
| if (imagefd < 0) { |
| failureInfo.failureStep = "Failure Opening Source Image"; |
| failureInfo.errorDomain = FailureInfo::kErrnoError; |
| failureInfo.errorCode = errno; |
| [self failWithInfo:&failureInfo]; |
| return; |
| } |
| fcntl(imagefd, F_NOCACHE, 1); |
| |
| const char* bsdName = DADiskGetBSDName(selectedStick_); |
| |
| int flags = OPENDEV_PART; |
| if (imageSize_ % blockSize) { |
| flags |= OPENDEV_BLCK; |
| NSLog(@"Warning: Size of image to be written (%lld) is not a multiple of " |
| @"the device's block size (%lld); using block device which will be " |
| @"significantly slower.", |
| (long long)imageSize_, (long long)blockSize); |
| } |
| int devfd = HANDLE_EINTR(opendev((char*)bsdName, |
| O_RDWR, |
| flags, |
| NULL)); |
| if (devfd < 0 && errno == EACCES) { |
| // Try harder. |
| NSMutableString* devicePath = [NSMutableString stringWithString:@"/dev/"]; |
| if (!(flags & OPENDEV_BLCK)) |
| [devicePath appendString:@"r"]; |
| [devicePath appendString:[NSString stringWithUTF8String:bsdName]]; |
| devfd = OpenPathForReadWriteUsingAuthopen( |
| [devicePath fileSystemRepresentation]); |
| } |
| if (devfd < 0) { |
| failureInfo.failureStep = "Failure Opening Destination Device"; |
| failureInfo.errorDomain = FailureInfo::kErrnoError; |
| failureInfo.errorCode = errno; |
| [self failWithInfo:&failureInfo]; |
| close(imagefd); |
| return; |
| } |
| fcntl(devfd, F_NOCACHE, 1); |
| |
| // Prep timing. |
| mach_timebase_info_data_t timebase; |
| mach_timebase_info(&timebase); |
| const uint64_t kProgressUpdateIntervalNanos = 100 * 1000 * 1000; // = 100 ms |
| uint64_t lastUpdateTime = 0; |
| |
| // Slam bits. |
| status_.progressBytes = 0; |
| status_.statusText = "Status Writing"; |
| status_.progressIndeterminate = NO; |
| [self sendStatusUpdate]; |
| |
| const int kBufferSize = 128 * 1024; |
| std::vector<char> sourceBuffer(kBufferSize); |
| while (status_.progressBytes < imageSize_ && |
| !failureInfo.failureStep && |
| !stopping_) { |
| ssize_t bytesRead = HANDLE_EINTR(read(imagefd, |
| &sourceBuffer[0], |
| kBufferSize)); |
| if (bytesRead <= 0) { |
| failureInfo.failureStep = "Failure Reading Source Image"; |
| if (bytesRead < 0) { |
| failureInfo.errorDomain = FailureInfo::kErrnoError; |
| failureInfo.errorCode = errno; |
| } else { |
| failureInfo.errorDomain = FailureInfo::kInternalError; |
| failureInfo.errorCode = kInternalErrorUnexpectedEOF; |
| } |
| break; |
| } |
| |
| ssize_t bytesWritten = 0; |
| while (bytesRead > bytesWritten) { |
| ssize_t bytesJustWritten = |
| HANDLE_EINTR(write(devfd, |
| &sourceBuffer[0] + bytesWritten, |
| bytesRead - bytesWritten)); |
| if (bytesJustWritten <= 0) { |
| failureInfo.failureStep = "Failure Writing Destination Device"; |
| if (bytesJustWritten < 0) { |
| failureInfo.errorDomain = FailureInfo::kErrnoError; |
| failureInfo.errorCode = errno; |
| } else { |
| failureInfo.errorDomain = FailureInfo::kInternalError; |
| failureInfo.errorCode = kInternalErrorUnexpectedEOF; |
| } |
| break; |
| } |
| |
| status_.progressBytes += bytesJustWritten; |
| bytesWritten += bytesJustWritten; |
| |
| uint64_t now = mach_absolute_time(); |
| uint64_t difference = |
| (now - lastUpdateTime) * timebase.numer / timebase.denom; |
| if (difference > kProgressUpdateIntervalNanos) { |
| [self sendStatusUpdate]; |
| lastUpdateTime = now; |
| } |
| } |
| } |
| |
| // Verify bits. |
| status_.progressBytes = 0; |
| status_.statusText = "Status Verifying Media"; |
| status_.progressIndeterminate = NO; |
| [self sendStatusUpdate]; |
| |
| if (lseek(imagefd, 0, SEEK_SET) != 0) { |
| failureInfo.failureStep = "Failure Resetting Source Image"; |
| failureInfo.errorDomain = FailureInfo::kErrnoError; |
| failureInfo.errorCode = errno; |
| } |
| if (lseek(devfd, 0, SEEK_SET) != 0) { |
| failureInfo.failureStep = "Failure Resetting Destination Device"; |
| failureInfo.errorDomain = FailureInfo::kErrnoError; |
| failureInfo.errorCode = errno; |
| } |
| |
| std::vector<char> destBuffer(kBufferSize); |
| while (status_.progressBytes < imageSize_ && |
| !failureInfo.failureStep && |
| !stopping_) { |
| ssize_t sourceBytesRead = HANDLE_EINTR(read(imagefd, |
| &sourceBuffer[0], |
| kBufferSize)); |
| if (sourceBytesRead <= 0) { |
| failureInfo.failureStep = "Failure Reading Source Image"; |
| if (sourceBytesRead < 0) { |
| failureInfo.errorDomain = FailureInfo::kErrnoError; |
| failureInfo.errorCode = errno; |
| } else { |
| failureInfo.errorDomain = FailureInfo::kInternalError; |
| failureInfo.errorCode = kInternalErrorUnexpectedEOF; |
| } |
| break; |
| } |
| |
| ssize_t destBytesRead = 0; |
| while (sourceBytesRead > destBytesRead) { |
| ssize_t destBytesJustRead = |
| HANDLE_EINTR(read(devfd, |
| &destBuffer[0] + destBytesRead, |
| sourceBytesRead - destBytesRead)); |
| if (destBytesJustRead <= 0) { |
| failureInfo.failureStep = "Failure Reading Destination Device"; |
| if (destBytesJustRead < 0) { |
| failureInfo.errorDomain = FailureInfo::kErrnoError; |
| failureInfo.errorCode = errno; |
| } else { |
| failureInfo.errorDomain = FailureInfo::kInternalError; |
| failureInfo.errorCode = kInternalErrorUnexpectedEOF; |
| } |
| break; |
| } |
| |
| status_.progressBytes += destBytesJustRead; |
| destBytesRead += destBytesJustRead; |
| |
| uint64_t now = mach_absolute_time(); |
| uint64_t difference = |
| (now - lastUpdateTime) * timebase.numer / timebase.denom; |
| if (difference > kProgressUpdateIntervalNanos) { |
| [self sendStatusUpdate]; |
| lastUpdateTime = now; |
| } |
| } |
| } |
| |
| // Close the disk and image. |
| close(imagefd); |
| close(devfd); |
| |
| // Eject the disk. |
| status_.statusText = "Status Ejecting"; |
| status_.progressIndeterminate = YES; |
| [self sendStatusUpdate]; |
| diskArbSuccess_ = kDiskArbUnknown; |
| DADiskEject(selectedStick_, |
| kDADiskEjectOptionDefault, |
| DiskEjected, |
| self); |
| while (diskArbSuccess_ == kDiskArbUnknown) |
| pthread_cond_wait(&diskArbCondition_, &diskArbMutex_); |
| if (diskArbSuccess_ == kDiskArbFailed) { |
| if (failureInfo.failureStep) { |
| // Failure to eject is probably not the real problem; ignore it. |
| } else { |
| failureInfo.failureStep = "Failure Ejecting"; |
| failureInfo.errorDomain = FailureInfo::kNoErrorCodeAvailable; |
| [self failWithInfo:&failureInfo]; |
| return; |
| } |
| } |
| |
| // Unclaim the disk. |
| diskClaim.reset(); |
| |
| if (stopping_) { |
| [self performSelectorOnMainThread:@selector(cleanUp:) |
| withObject:nil |
| waitUntilDone:NO]; |
| return; |
| } |
| |
| if (status_.progressBytes < imageSize_) { |
| [self failWithInfo:&failureInfo]; |
| return; |
| } |
| |
| status_.statusText = "Status Done"; |
| status_.progressIndeterminate = NO; |
| [self sendStatusUpdate]; |
| |
| // Let the user continue in the UI. |
| [self performSelectorOnMainThread:@selector(writeImageComplete) |
| withObject:nil |
| waitUntilDone:NO]; |
| } |
| |
| - (void)writeImageComplete { |
| [self nextTab:self]; |
| } |
| |
| - (void)sendStatusUpdate { |
| NSData* statusData = [[NSData alloc] initWithBytes:&status_ |
| length:sizeof(status_)]; |
| [self performSelectorOnMainThread:@selector(updateStatus:) |
| withObject:statusData |
| waitUntilDone:NO]; |
| [statusData release]; |
| } |
| |
| - (void)updateStatus:(NSData*)value { |
| const Status* statusData = (const Status*)[value bytes]; |
| |
| NSString* statusLine = NSLocalizedString( |
| [NSString stringWithUTF8String:statusData->statusText], nil); |
| if (statusData->attempt) { |
| NSString* addition = |
| [NSString stringWithFormat:NSLocalizedString(@"StatusAdd Attempt", nil), |
| statusData->attempt + 1]; |
| statusLine = [statusLine stringByAppendingString:addition]; |
| } |
| [statusLine_ setStringValue:statusLine]; |
| |
| BOOL currentlyIndeterminate = [progressIndicator_ isIndeterminate]; |
| if ((bool)currentlyIndeterminate != (bool)statusData->progressIndeterminate) { |
| [progressIndicator_ setIndeterminate:statusData->progressIndeterminate]; |
| if (statusData->progressIndeterminate) |
| [progressIndicator_ startAnimation:self]; |
| } |
| [progressIndicator_ setDoubleValue:statusData->progressBytes]; |
| |
| double progressValue = 0.0; |
| if ([progressIndicator_ maxValue]) |
| progressValue = statusData->progressBytes / [progressIndicator_ maxValue]; |
| SetDockTileProgress(kDockTileVisible, |
| statusData->progressIndeterminate ? kDockTileIndeterminate |
| : kDockTileDeterminate, |
| progressValue); |
| } |
| |
| - (BOOL)workNext { |
| SetDockTileProgress(kDockTileInvisible, kDockTileIndeterminate, 0); |
| |
| return YES; |
| } |
| |
| #pragma mark Done |
| |
| @end |