blob: 7dd2720de18844827a35d5a4ddcdefe5861cdc93 [file] [log] [blame]
// 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