Openmoko Flasher

Openmoko Flasher Git Source Tree

Root/AppController.m

1//
2// AppController.m
3// OpenMoko Flasher
4//
5// Created by H. Nikolaus Schaller on 01.08.07.
6// Copyright 2007 Golden Delicious Computers GmbH&Co. KG. All rights reserved.
7// Licensed under GPLv2 - see www.fsf.org
8//
9
10#import "AppController.h"
11
12@implementation NSString (Reverse)
13
14- (NSComparisonResult) reverseCaseInsensitiveCompare:(NSString *)aString
15{
16    NSComparisonResult r=[self caseInsensitiveCompare:aString];
17    return -r;
18}
19
20@end
21
22enum
23{ // this must match the tag of the popup button values
24    TYPE_UBOOT=1,
25    TYPE_2,
26    TYPE_KERNEL,
27    TYPE_SPLASH,
28    TYPE_ROOTFS
29};
30
31@implementation AppController
32
33- (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication
34{
35    return YES;
36}
37
38- (NSString *) cachePath:(NSString *) package;
39{ // path may be either a file or some http:// address
40    NSMutableString *r=[package mutableCopy];
41    NSString *str;
42    [r replaceOccurrencesOfString:@"file://" withString:@"" options:0 range:NSMakeRange(0, [r length])];
43    [r replaceOccurrencesOfString:@"http://" withString:@":" options:0 range:NSMakeRange(0, [r length])];
44    [r replaceOccurrencesOfString:@"/" withString:@":" options:0 range:NSMakeRange(0, [r length])];
45    str=[NSHomeDirectory() stringByAppendingFormat:@"/Library/Caches/OpenMoko Flasher/%@", r];
46    [r autorelease];
47#if 0
48    NSLog(@"cachePath %@ -> %@", package, str);
49#endif
50    return str;
51}
52
53- (NSString *) urlPath:(NSString *) cacheFile;
54{ // path may be either a file or some http:// address
55    NSMutableString *r=[cacheFile mutableCopy];
56    [r replaceOccurrencesOfString:@":" withString:@"/" options:0 range:NSMakeRange(1, [r length]-1)];
57    if([r replaceOccurrencesOfString:@":" withString:@"http://" options:0 range:NSMakeRange(0, 1)] == 0)
58        [r insertString:@"file://" atIndex:0]; // has no initial :
59    [r autorelease];
60#if 0
61    NSLog(@"urlPath %@ -> %@", cacheFile, r);
62#endif
63    return r;
64}
65
66- (NSString *) serverFor:(NSString *) cacheFile;
67{ // path may be either a file or some http:// address
68    NSMutableArray *c=[[cacheFile componentsSeparatedByString:@"/"] mutableCopy];
69    NSString *r;
70    [c removeLastObject];
71    r=[c componentsJoinedByString:@"/"];
72    [c release];
73#if 0
74    NSLog(@"serverFor %@ -> %@", cacheFile, r);
75#endif
76    return r;
77}
78
79- (BOOL) downloading:(NSString *) pack;
80{ // check if we are currently downloading this package
81    NSEnumerator *e=[downloads objectEnumerator];
82    NSURLDownload *d;
83    while((d=[e nextObject]))
84        {
85        if([[[[d request] URL] absoluteString] isEqualToString:pack])
86            return YES;
87        }
88    return NO;
89}
90
91- (BOOL) cached:(NSString *) pack;
92{ // already cached?
93    return [[NSFileManager defaultManager] fileExistsAtPath:[self cachePath:pack]];
94}
95
96- (void) loadCached;
97{ // loads all from cache
98    NSString *dir=[self cachePath:@""];
99    NSArray *cached=[[NSFileManager defaultManager] directoryContentsAtPath:dir];
100    NSEnumerator *e=[cached objectEnumerator];
101    NSString *cacheFile;
102    [packages removeAllObjects];
103    while((cacheFile=[e nextObject]))
104        {
105        NSString *path;
106        if([cacheFile hasPrefix:@"."])
107            continue; // skip
108        if([cacheFile length] < 8)
109            continue; // too short (is some cache file)
110        path=[self urlPath:cacheFile];
111        [packages addObject:path];
112        if([path hasPrefix:@"file:"])
113            continue; // don't add
114        path=[self serverFor:path];
115        if(![repositories containsObject:path])
116            { // add to known repositories
117            [repositories addObject:path];
118            [repositoryView reloadData];
119            }
120        }
121    [self filter:nil];
122}
123
124- (void) updateButtons;
125{
126    int row=[table selectedRow];
127    BOOL flag=row >= 0;
128    NSString *pkg=(row >= 0 && row <[filtered count])?[filtered objectAtIndex:row]:nil;
129    [loadButton setEnabled:flag && ![self cached:pkg] && ![self downloading:pkg]];
130// [stopButton setEnabled:flag && [self downloading:pkg]];
131    [removeButton setEnabled:flag && ([self cached:pkg] || [self downloading:pkg])]; // can also stop a download
132    [removeButton setTitle:[self downloading:pkg]?@"Cancel":@"Uncache"];
133    [refreshButton setEnabled:[[repositoryView stringValue] hasPrefix:@"http://"]];
134    [flashButton setEnabled:flag && [self cached:pkg] && ![self downloading:pkg] && !flasher];
135    [progress setHidden:!(flasher || [downloads count] > 0)];
136    if(![progress isHidden])
137        [progress startAnimation:nil];
138}
139
140- (void) awakeFromNib;
141{
142    [table setDoubleAction:@selector(doubleClick:)];
143    [self tableViewSelectionDidChange:nil];
144    packages=[[NSMutableArray alloc] initWithCapacity:100];
145    moreInfo=[[NSMutableDictionary alloc] initWithCapacity:100];
146    downloads=[[NSMutableArray alloc] initWithCapacity:5];
147    repositories=[[[NSBundle mainBundle] objectForInfoDictionaryKey:@"Repositories"] mutableCopy];
148    [repositoryView reloadData];
149    [repositoryView setStringValue:[repositories objectAtIndex:1]]; // preselect first (should we remember by user defaults?)
150#if 0
151    NSLog(@"repositories=%@", repositories);
152#endif
153    [[NSFileManager defaultManager] createDirectoryAtPath:[self cachePath:@""] attributes:nil];
154    [self loadCached];
155    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(readNotification:) name:NSFileHandleReadCompletionNotification object:nil];
156}
157
158- (void) applicationDidFinishLaunching:(NSNotification *)aNotification
159{ // refresh initial repository
160    [self performSelector:@selector(refresh:) withObject:nil afterDelay:0.1];
161}
162
163- (id) comboBox:(NSComboBox *)aComboBox objectValueForItemAtIndex:(int)index
164{
165    return [repositories objectAtIndex:index];
166}
167
168- (int) numberOfItemsInComboBox:(NSComboBox *)aComboBox
169{
170#if 0
171    NSLog(@"items=%d", [repositories count]);
172#endif
173    return [repositories count];
174}
175
176- (IBAction) refresh:(id) sender; // refresh list
177{
178    NSString *repos=[repositoryView stringValue];
179    NSURL *url=[NSURL URLWithString:repos];
180    NSError *err;
181    NSString *str;
182// NSAttributedString *astr;
183// NSDictionary *attribs;
184    if(!url)
185            {
186                [self updateButtons];
187                [self filter:nil];
188                return;
189            }
190    [progress setHidden:NO];
191    [progress startAnimation:nil];
192// astr=[[NSAttributedString alloc] initWithURL:url options:nil documentAttributes:&attribs error:&err];
193    str=[NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&err];
194    [self loadCached];
195    if(!str)
196        {
197        NSRunAlertPanel(@"OpenMoko Flasher", @"Can't access %@ due to %@", @"Ok", nil, nil, url, err);
198        }
199    else
200        {
201        unsigned pos=0;
202        unsigned len=[str length];
203        while(YES)
204            {
205                // should be reworked to use NSScanner!
206                
207            NSRange f=[str rangeOfString:@"href=\"" options:NSCaseInsensitiveSearch range:NSMakeRange(pos, len-pos)];
208            NSRange g;
209            NSString *name;
210            NSString *date;
211            if(f.location == NSNotFound)
212                break; // no more locations
213            f.location+=f.length; // advance to start of href string
214            g=[str rangeOfString:@"\">" options:NSCaseInsensitiveSearch range:NSMakeRange(f.location, len-f.location)]; // find closing location
215            if(g.location == NSNotFound)
216                break; // some error
217            name=[str substringWithRange:NSMakeRange(f.location, g.location-f.location)];
218            if([[repositoryView stringValue] hasSuffix:@"/"])
219                name=[[repositoryView stringValue] stringByAppendingString:name];
220            else
221                name=[[repositoryView stringValue] stringByAppendingFormat:@"/%@", name]; // make URL package name
222// NSLog(@"pkg=%@--", name);
223            if([name hasSuffix:@".bin"] ||
224               [name hasSuffix:@".tgz"] ||
225               [name hasSuffix:@".gz"] ||
226               [name hasSuffix:@".jffs2"])
227                { // candidate
228                    if(![packages containsObject:name])
229                            { // new
230                                [packages addObject:name];
231                                [moreInfo setObject:[NSMutableDictionary dictionaryWithCapacity:3] forKey:name];
232                            }
233                    g=[str rangeOfString:@"</a> " options:NSCaseInsensitiveSearch range:NSMakeRange(g.location, len-g.location)]; // find closing location
234                if(g.location != NSNotFound)
235                        {
236                            int pos=g.location+g.length;
237                            while(pos < [str length] && [str characterAtIndex:pos] == ' ')
238                                pos++;
239                            date=[str substringWithRange:NSMakeRange(pos, 17)];
240                        }
241                else
242                    date=@"?";
243                // should convert to real NSDate for better sorting
244                NSLog(@"add date=%@ name=%@", date, name);
245                [[moreInfo objectForKey:name] setObject:date forKey:@"date"];
246                }
247            pos=g.location;
248            }
249        }
250    [self updateButtons];
251    [self filter:nil];
252}
253
254- (IBAction) add:(id) sender; // manually add file to cache
255{
256    NSOpenPanel *o=[NSOpenPanel openPanel];
257    NSString *pack;
258    if([o runModalForDirectory:nil file:nil types:[NSArray arrayWithObjects:@"jffs2", @"bin", @"gz", nil]] != NSFileHandlingPanelOKButton)
259        return; // ignore
260    pack=[@"file://" stringByAppendingString:[[o filename] lastPathComponent]];
261    if([[NSFileManager defaultManager] fileExistsAtPath:[self cachePath:pack]])
262        NSRunAlertPanel(@"OpenMoko Flasher", @"Already exists in cache: %@", @"Ok", nil, nil, [o filename]);
263    else if(![[NSFileManager defaultManager] copyPath:[o filename] toPath:[self cachePath:pack] handler:nil])
264        NSRunAlertPanel(@"OpenMoko Flasher", @"Can't add %@", @"Ok", nil, nil, [o filename]);
265    else
266        {
267        if(![packages containsObject:pack])
268            [packages addObject:pack]; // cache as file:name
269        }
270    [self filter:nil]; // reload
271}
272
273- (IBAction) remove:(id) sender; // remove from cache
274{
275    NSString *path;
276    NSString *package;
277    int row=[table selectedRow];
278    NSEnumerator *e=[downloads objectEnumerator];
279    NSURLDownload *d;
280    if(row < 0)
281        return;
282    package=[filtered objectAtIndex:row];
283    path=[self cachePath:package];
284    while((d=[e nextObject]))
285        {
286        NSURL *url=[[d request] URL];
287        if([[url absoluteString] isEqualToString:package])
288            {
289            [d cancel];
290#if 1 // cancel should have deleted the file?
291            [[NSFileManager defaultManager] removeFileAtPath:path handler:nil];
292#endif
293            [downloads removeObject:d];
294            NSLog(@"cancelled %@", d);
295            NSLog(@"downloads %@", downloads);
296            [self filter:nil]; // show result
297            return;
298            }
299        }
300    if(![self cached:package])
301        return; // not cached
302    if(NSRunAlertPanel(@"OpenMoko Flasher", @"Do you really want to delete %@ from the cache?", @"Ok", @"Cancel", nil, package) != NSAlertDefaultReturn)
303        return;
304    [[NSFileManager defaultManager] removeFileAtPath:path handler:nil];
305    [self filter:nil]; // show result
306}
307
308- (IBAction) load:(id) sender; // load selected to cache
309{
310    NSString *path;
311    NSString *package;
312    int row=[table selectedRow];
313    if(row < 0)
314        return;
315    package=[filtered objectAtIndex:row];
316    path=[self cachePath:package];
317    if(![self cached:package] || ![self downloading:package])
318        { // does not exist
319        NSURL *url=[NSURL URLWithString:package];
320        NSURLRequest *request=[NSURLRequest requestWithURL:url];
321        NSURLDownload *download=[[NSURLDownload alloc] initWithRequest:request delegate:self];
322        NSLog(@"url=%@", url);
323        NSLog(@"request=%@", request);
324        [download setDestination:path allowOverwrite:YES];
325        [download setDeletesFileUponFailure:YES];
326        [downloads addObject:download]; // add to list
327        NSLog(@"downloads=%@", downloads);
328        [download release];
329        [self filter:nil]; // needs a refresh to show downloading status
330        }
331}
332
333- (void) readNotification:(NSNotification *) n;
334{
335    NSFileHandle *f=[n object];
336    NSData *d=[[n userInfo] objectForKey:NSFileHandleNotificationDataItem];
337    NSAttributedString *astr;
338    NSString *str;
339    if([d length] == 0)
340        {
341        NSLog(@"flashing done");
342        [flasher waitUntilExit];
343        if([flasher terminationStatus] == 0)
344            { // flashed ok
345            [[NSUserDefaults standardUserDefaults] setObject:[currentFile lastPathComponent] forKey:[[region selectedItem] title]];
346            }
347        else
348            NSRunAlertPanel(@"Access OpenMoko", @"Flashing failed. Try to unplug/replug the USB connection. And, try twice.", @"Ok", nil, nil);
349        [table reloadData]; // update status
350        [flasher release];
351        flasher=nil;
352        [currentFile release];
353        [self updateButtons]; // stop progress indicator
354        return; // done
355        }
356    NSLog(@"received %d bytes", [d length]);
357    str=[[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding];
358    astr=[[NSAttributedString alloc] initWithString:str];
359    [str release];
360    [[logView textStorage] appendAttributedString:astr];
361    [astr release];
362    [logView scrollRangeToVisible:NSMakeRange([[logView string] length], 0)]; // scroll to bottom
363    [[logView window] display];
364    [f readInBackgroundAndNotify]; // continue
365}
366
367- (int) packageType:(NSString *) file
368{
369    if([file hasPrefix:@"u-boot-"] && [file hasSuffix:@".bin"])
370        return TYPE_UBOOT;
371    if([file hasSuffix:@"-u-boot.bin"])
372        return TYPE_UBOOT;
373    if([file hasSuffix:@"u-boot_env"])
374        return TYPE_2;
375    if([file hasPrefix:@"uImage-"] && [file hasSuffix:@".bin"])
376        return TYPE_KERNEL;
377    if([file hasSuffix:@".uImage.bin"])
378        return TYPE_KERNEL;
379    if([file hasSuffix:@".splash.gz"])
380        return TYPE_SPLASH;
381    if([file hasSuffix:@".jffs2"])
382        return TYPE_ROOTFS;
383    return NSNotFound;
384}
385
386- (IBAction) flash:(id) sender; // flash selected file from cache
387{
388    NSBundle *b=[NSBundle mainBundle];
389    NSString *path;
390    NSString *package;
391    NSPipe *pipe;
392    int type;
393    NSString *mappedType=nil;
394    int row=[table selectedRow];
395    if(row < 0)
396        return;
397    if([self downloading:package])
398        return;
399    package=[filtered objectAtIndex:row];
400    type=[self packageType:[package lastPathComponent]];
401    switch(type)
402        {
403            case TYPE_UBOOT: mappedType=@"u-boot"; break;
404            case TYPE_2: mappedType=@"2"; break;
405            case TYPE_KERNEL: mappedType=@"kernel"; break;
406            case TYPE_SPLASH: mappedType=@"splash"; break;
407            case TYPE_ROOTFS: mappedType=@"rootfs"; break;
408            default:
409                return; // unknown type
410        }
411    path=[self cachePath:package];
412    if(!path)
413        return; // can't load
414    currentFile=[path retain];
415#if 1
416    {
417    NSString *cmd=[NSString stringWithFormat:@"export DYLD_LIBRARY_PATH=\"%@:${DYLD_LIBRARY_PATH}\"; '%@' -a %@ -R -D '%@'",
418            [b resourcePath], [b pathForAuxiliaryExecutable:@"dfu-util"], mappedType, path];
419    NSLog(@"\n%@", cmd);
420    }
421#endif
422    if(NSRunAlertPanel(@"Access OpenMoko", @"Do you really want to flash a new file?\nThis may damage your device!\n\nTo turn on Flashing mode, press the AUX buton while switching on the OpenMoko. Leave the device in the BOOT menu. If it fails, unplug/replug USB. And try again (twice is better than once).", @"Ok", @"Cancel", nil) != NSOKButton)
423        return;
424    flasher=[[NSTask alloc] init];
425#if 0
426    [flasher setLaunchPath:@"/bin/echo"];
427#else
428    [flasher setLaunchPath:[b pathForAuxiliaryExecutable:@"dfu-util"]];
429#endif
430    [flasher setArguments:[NSArray arrayWithObjects:
431// @"dfu-util",
432        @"-a",
433        mappedType,
434        @"-R",
435        @"-D",
436        path,
437        nil]];
438    [flasher setEnvironment:[NSDictionary dictionaryWithObjectsAndKeys:
439        [b resourcePath], @"DYLD_LIBRARY_PATH",
440        nil]];
441#if 1
442    NSLog(@"%@ %@ %@", [flasher launchPath], [flasher arguments], [flasher environment]);
443    // write virtual command(s) to log window
444#endif
445    pipe=[NSPipe pipe];
446    [flasher setStandardOutput:pipe];
447    [flasher setStandardError:pipe];
448    [[pipe fileHandleForReading] readInBackgroundAndNotify];
449    [flasher launch];
450    [self updateButtons]; // start progress indicator
451}
452
453// should be effective for every change!!!
454
455- (IBAction) didchange:(id) sender;
456{
457    // check if it really did change...
458    if(sender == repositoryView)
459            {
460                [self refresh:nil];
461            }
462    else
463            {
464                [self filter:nil]; // update filter
465                [self updateButtons];
466            }
467}
468
469- (IBAction) filter:(id) sender; // change filter
470{
471    [filtered release]; // clear cache
472    filtered=nil;
473    [table reloadData];
474}
475
476- (IBAction) go:(id) sender;
477{
478    [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[repositoryView stringValue]]];
479    [sender setState:NSOffState];
480}
481
482- (BOOL) validateMenuItem:(id <NSMenuItem>)menuItem
483{
484    NSString *action=NSStringFromSelector([menuItem action]);
485    if([action isEqualToString:@"load:"])
486        return [loadButton isEnabled];
487    if([action isEqualToString:@"flash:"])
488        return [flashButton isEnabled];
489    return YES;
490}
491
492// table callbacks
493
494- (IBAction) doubleClick:(id) sender;
495{
496    int row=[sender clickedRow];
497    NSString *pack;
498    if(row < 0)
499        return;
500    pack=[filtered objectAtIndex:row];
501    if([self cached:pack])
502        [[NSWorkspace sharedWorkspace] selectFile:[self cachePath:pack] inFileViewerRootedAtPath:@""];
503}
504
505- (int) numberOfRowsInTableView:(NSTableView *)aTableView
506{
507    if(!filtered)
508        {
509        int tag=[[region selectedItem] tag];
510        NSEnumerator *e=[packages objectEnumerator];
511        NSString *package;
512        NSString *repos=[repositoryView stringValue];
513        if([repos hasSuffix:@"/"])
514            repos=[repos substringToIndex:[repos length]-1];
515        filtered=[[NSMutableArray alloc] initWithCapacity:100];
516        while((package=[e nextObject]))
517            {
518            NSString *loc=[self serverFor:package];
519            NSString *file=[package lastPathComponent];
520            if([repos hasPrefix:@"http://"] && ![loc isEqualToString:repos])
521                {
522#if 0
523                NSLog(@"no match: %@ - %@", loc, repos);
524#endif
525                continue; // filter by repository
526                }
527            if(tag != 0 && [self packageType:file] != tag)
528                continue; // didn't pass filter
529            [filtered addObject:package]; // did go through filter
530            }
531        [filtered sortUsingSelector:@selector(reverseCaseInsensitiveCompare:)];
532        [self updateButtons]; // may change button status
533        }
534    return [filtered count];
535}
536
537- (id) tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
538{
539    NSString *ident=[aTableColumn identifier];
540    if([ident isEqualToString:@"status"])
541        { // get status
542        NSString *name=[filtered objectAtIndex:rowIndex];
543        NSString *current=[[NSUserDefaults standardUserDefaults] objectForKey:[[region selectedItem] title]];
544        if([current isEqualToString:name])
545            return @"Flashed";
546        if([self downloading:name])
547            return @"Loading";
548        if([self cached:name])
549            return @"Cached";
550        return @"";
551        }
552    if([ident isEqualToString:@"type"])
553        {
554        switch([self packageType:[[filtered objectAtIndex:rowIndex] lastPathComponent]])
555            {
556            case TYPE_UBOOT: return @"BootLD";
557            case TYPE_KERNEL: return @"Kernel";
558            case TYPE_SPLASH: return @"Splash";
559            case TYPE_ROOTFS: return @"RootFS";
560            default: @"?";
561            }
562        }
563    if([ident isEqualToString:@"package"])
564        {
565        if([[repositoryView stringValue] hasPrefix:@"http://"])
566            return [[filtered objectAtIndex:rowIndex] lastPathComponent]; // show filename only
567        return [filtered objectAtIndex:rowIndex]; // show full path
568        }
569    if([ident isEqualToString:@"date"])
570        {
571        NSString *package=[filtered objectAtIndex:rowIndex];
572        NSDictionary *more=[moreInfo objectForKey:package];
573        return [more objectForKey:@"date"];
574        }
575    if([ident isEqualToString:@"size"])
576        {
577        NSString *package=[filtered objectAtIndex:rowIndex];
578        if([self cached:package])
579            { // get cached file size
580            NSString *path=[self cachePath:package];
581            NSDictionary *stat=[[NSFileManager defaultManager] fileAttributesAtPath:path traverseLink:YES];
582            if(stat)
583                { // get (real) file size
584                return [[stat objectForKey:NSFileSize] description];
585                }
586            }
587        return @"?";
588        }
589    return @"?";
590}
591
592- (void) tableViewSelectionDidChange:(NSNotification *)aNotification
593{
594    [self updateButtons];
595}
596
597- (void) downloadDidFinish:(NSURLDownload *)download
598{
599    NSLog(@"download finished %@", download);
600    [downloads removeObject:download];
601    [self filter:nil];
602}
603
604- (void)download:(NSURLDownload *)download didReceiveDataOfLength:(unsigned)length
605{ // show new file length
606    static NSDate *last;
607    if(!last || [[NSDate date] timeIntervalSinceDate:last] > 0.2)
608        { // refresh
609        [table reloadData];
610        [last release];
611        last=[[NSDate alloc] init];
612        }
613}
614
615- (void) download:(NSURLDownload *)download didFailWithError:(NSError *)error
616{
617    NSLog(@"download error %@ for %@", error, download);
618    [downloads removeObject:download];
619    [self filter:nil];
620}
621
622@end
623

Archive Download this file

Branches