From 0ee1d39bac5f32fe68720987c249a4e93c8df4e5 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Fri, 19 Nov 2010 13:41:24 +1300 Subject: [PATCH] Refactor download mechanism to allow multiple concurrent downloads. Allow downloads to be cancelled. Allow js to query if files exist in the cache. Fix some compiler warnings. --- OpenRA.Launcher.Mac/Controller.h | 7 +- OpenRA.Launcher.Mac/Controller.m | 25 ++++++- OpenRA.Launcher.Mac/Download.h | 23 ++++++ OpenRA.Launcher.Mac/Download.m | 74 +++++++++++++++++++ OpenRA.Launcher.Mac/GameInstall.h | 7 +- OpenRA.Launcher.Mac/GameInstall.m | 58 +++------------ OpenRA.Launcher.Mac/JSBridge.h | 3 +- OpenRA.Launcher.Mac/JSBridge.m | 36 +++++++-- .../OpenRA.xcodeproj/project.pbxproj | 6 ++ mods/cnc/mod.html | 28 +++++-- 10 files changed, 201 insertions(+), 66 deletions(-) create mode 100644 OpenRA.Launcher.Mac/Download.h create mode 100644 OpenRA.Launcher.Mac/Download.m diff --git a/OpenRA.Launcher.Mac/Controller.h b/OpenRA.Launcher.Mac/Controller.h index c0084d50c3..56a0aa3f2e 100644 --- a/OpenRA.Launcher.Mac/Controller.h +++ b/OpenRA.Launcher.Mac/Controller.h @@ -17,14 +17,19 @@ SidebarEntry *sidebarItems; GameInstall *game; NSDictionary *allMods; + NSMutableDictionary *downloads; IBOutlet NSOutlineView *outlineView; IBOutlet WebView *webView; } @property(readonly) NSDictionary *allMods; +@property(readonly) WebView *webView; - (void)launchMod:(NSString *)mod; - (void)populateModInfo; - (SidebarEntry *)sidebarModsTree; - (SidebarEntry *)sidebarOtherTree; -- (BOOL)downloadUrl:(NSString *)url intoCache:(NSString *)filename withId:(NSString *)key; + +- (BOOL)downloadUrl:(NSString *)url toFile:(NSString *)filename withId:(NSString *)key; +- (void)cancelDownload:(NSString *)key; + @end diff --git a/OpenRA.Launcher.Mac/Controller.m b/OpenRA.Launcher.Mac/Controller.m index bb4c8f579c..49b0a271ad 100644 --- a/OpenRA.Launcher.Mac/Controller.m +++ b/OpenRA.Launcher.Mac/Controller.m @@ -12,14 +12,16 @@ #import "GameInstall.h" #import "ImageAndTextCell.h" #import "JSBridge.h" +#import "Download.h" @implementation Controller @synthesize allMods; - +@synthesize webView; - (void) awakeFromNib { game = [[GameInstall alloc] initWithURL:[NSURL URLWithString:@"/Users/paul/src/OpenRA"]]; [[JSBridge sharedInstance] setController:self]; + downloads = [[NSMutableDictionary alloc] init]; NSTableColumn *col = [outlineView tableColumnWithIdentifier:@"mods"]; ImageAndTextCell *imageAndTextCell = [[[ImageAndTextCell alloc] init] autorelease]; @@ -50,6 +52,7 @@ - (void)dealloc { [sidebarItems release]; sidebarItems = nil; + [downloads release]; downloads = nil; [super dealloc]; } @@ -90,10 +93,24 @@ [game launchMod:mod]; } -- (BOOL)downloadUrl:(NSString *)url intoCache:(NSString *)filename withId:(NSString *)key +- (BOOL)downloadUrl:(NSString *)url toFile:(NSString *)path withId:(NSString *)key { - id path = [[@"~/Library/Application Support/OpenRA/Downloads/" stringByAppendingPathComponent:filename] stringByExpandingTildeInPath]; - return [game downloadUrl:url toPath:path withId:key]; + if ([downloads objectForKey:key] != nil) + { + NSLog(@"Download already in progress for %@",key); + return NO; + } + + + Download *download = [Download downloadWithURL:url filename:path key:key game:game]; + [downloads setObject:download forKey:key]; + return YES; +} + +- (void)cancelDownload:(NSString *)key +{ + [[downloads objectForKey:key] cancel]; + [downloads removeObjectForKey:key]; } #pragma mark Sidebar Datasource and Delegate diff --git a/OpenRA.Launcher.Mac/Download.h b/OpenRA.Launcher.Mac/Download.h new file mode 100644 index 0000000000..be9a5a8f5c --- /dev/null +++ b/OpenRA.Launcher.Mac/Download.h @@ -0,0 +1,23 @@ +// +// Download.h +// OpenRA +// +// Created by Paul Chote on 19/11/10. +// Copyright 2010 __MyCompanyName__. All rights reserved. +// + +#import +@class GameInstall; +@interface Download : NSObject +{ + NSString *key; + NSString *url; + NSString *filename; + GameInstall *game; + NSTask *task; +} ++ (id)downloadWithURL:(NSString *)aURL filename:(NSString *)aFilename key:(NSString *)aKey game:(GameInstall *)aGame; +- (id)initWithURL:(NSString *)aURL filename:(NSString *)aFilename key:(NSString *)aKey game:(GameInstall *)game; +- (void)cancel; + +@end \ No newline at end of file diff --git a/OpenRA.Launcher.Mac/Download.m b/OpenRA.Launcher.Mac/Download.m new file mode 100644 index 0000000000..0e9d23d53a --- /dev/null +++ b/OpenRA.Launcher.Mac/Download.m @@ -0,0 +1,74 @@ +// +// Download.m +// OpenRA +// +// Created by Paul Chote on 19/11/10. +// Copyright 2010 __MyCompanyName__. All rights reserved. +// + +#import "Download.h" +#import "GameInstall.h" +#import "JSBridge.h" + +@implementation Download + ++ (id)downloadWithURL:(NSString *)aURL filename:(NSString *)aFilename key:(NSString *)aKey game:(GameInstall *)aGame +{ + id newObject = [[self alloc] initWithURL:aURL filename:aFilename key:aKey game:aGame]; + [newObject autorelease]; + return newObject; +} + +- (id)initWithURL:(NSString *)aURL filename:(NSString *)aFilename key:(NSString *)aKey game:(GameInstall *)aGame; +{ + self = [super init]; + if (self != nil) + { + url = [aURL retain]; + filename = [aFilename retain]; + key = [aKey retain]; + game = [aGame retain]; + + NSLog(@"Starting download..."); + task = [game runAsyncUtilityWithArg:[NSString stringWithFormat:@"--download-url=%@,%@",url,filename] + delegate:self + responseSelector:@selector(utilityResponded:) + terminatedSelector:@selector(utilityTerminated:)]; + [task retain]; + } + return self; +} + +- (void)cancel +{ + // Stop the download task. utilityTerminated: will handle the cleanup + NSLog(@"Cancelling"); + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc removeObserver:self name:NSFileHandleReadCompletionNotification object:[[task standardOutput] fileHandleForReading]]; + [nc removeObserver:self name:NSTaskDidTerminateNotification object:task]; + [task terminate]; +} + +- (void)utilityResponded:(NSNotification *)n +{ + NSData *data = [[n userInfo] valueForKey:NSFileHandleNotificationDataItem]; + NSString *response = [[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding] autorelease]; + NSLog(@"r: %@",response); + + [[JSBridge sharedInstance] notifyDownloadProgress:self]; + + // Keep reading + if ([n object] != nil) + [[n object] readInBackgroundAndNotify]; +} + +- (void)utilityTerminated:(NSNotification *)n +{ + NSLog(@"utility terminated"); + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; + [nc removeObserver:self name:NSFileHandleReadCompletionNotification object:[[task standardOutput] fileHandleForReading]]; + [nc removeObserver:self name:NSTaskDidTerminateNotification object:task]; + [task release]; task = nil; +} + +@end diff --git a/OpenRA.Launcher.Mac/GameInstall.h b/OpenRA.Launcher.Mac/GameInstall.h index 508dc411d9..1a6f219c88 100644 --- a/OpenRA.Launcher.Mac/GameInstall.h +++ b/OpenRA.Launcher.Mac/GameInstall.h @@ -9,8 +9,10 @@ #import @class Mod; +@class Controller; @interface GameInstall : NSObject { NSURL *gameURL; + Controller *controller; NSMutableDictionary *downloadTasks; } @property(readonly) NSURL *gameURL; @@ -20,5 +22,8 @@ - (NSString *)runUtilityQuery:(NSString *)arg; - (NSArray *)installedMods; - (NSDictionary *)infoForMods:(NSArray *)mods; -- (BOOL)downloadUrl:(NSString *)url toPath:(NSString *)filename withId:(NSString *)key; +- (NSTask *)runAsyncUtilityWithArg:(NSString *)arg + delegate:(id)object + responseSelector:(SEL)response + terminatedSelector:(SEL)terminated; @end diff --git a/OpenRA.Launcher.Mac/GameInstall.m b/OpenRA.Launcher.Mac/GameInstall.m index 7ca65e5fbf..f58be97fb6 100644 --- a/OpenRA.Launcher.Mac/GameInstall.m +++ b/OpenRA.Launcher.Mac/GameInstall.m @@ -7,6 +7,7 @@ */ #import "GameInstall.h" +#import "Controller.h" #import "Mod.h" @implementation GameInstall @@ -47,7 +48,7 @@ NSString *current = nil; for (id l in lines) { - id line = [l stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + NSString *line = [l stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (line == nil || [line length] == 0) continue; @@ -143,21 +144,15 @@ return [[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding] autorelease]; } - -- (BOOL)downloadUrl:(NSString *)url toPath:(NSString *)filename withId:(NSString *)key +- (NSTask *)runAsyncUtilityWithArg:(NSString *)arg + delegate:(id)object + responseSelector:(SEL)response + terminatedSelector:(SEL)terminated { - NSLog(@"Starting download..."); - if ([downloadTasks objectForKey:key] != nil) - { - NSLog(@"Error: a download is already in progress for %@",key); - return NO; - } - NSTask *task = [[[NSTask alloc] init] autorelease]; NSPipe *pipe = [NSPipe pipe]; NSMutableArray *taskArgs = [NSMutableArray arrayWithObject:@"OpenRA.Utility.exe"]; - NSString *arg = [NSString stringWithFormat:@"--download-url=%@,%@",url,filename]; [taskArgs addObject:arg]; [task setCurrentDirectoryPath:[gameURL absoluteString]]; @@ -167,48 +162,17 @@ NSFileHandle *readHandle = [pipe fileHandleForReading]; NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; - [nc addObserver:self - selector:@selector(utilityResponded:) + [nc addObserver:object + selector:response name:NSFileHandleReadCompletionNotification object:readHandle]; - [nc addObserver:self - selector:@selector(utilityTerminated:) + [nc addObserver:object + selector:terminated name:NSTaskDidTerminateNotification object:task]; [task launch]; [readHandle readInBackgroundAndNotify]; - - [downloadTasks setObject:task forKey:key]; - return YES; + return task; } -- (void)utilityResponded:(NSNotification *)n -{ - NSData *data = [[n userInfo] valueForKey:NSFileHandleNotificationDataItem]; - NSString *response = [[[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding] autorelease]; - NSLog(@"r: %@",response); - - // Keep reading - if ([n object] != nil) - [[n object] readInBackgroundAndNotify]; -} - -- (void)utilityTerminated:(NSNotification *)n -{ - id task = [n object]; - id pipe = [task standardOutput]; - - NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; - [nc removeObserver:self name:NSFileHandleReadCompletionNotification object:[pipe fileHandleForReading]]; - [nc removeObserver:self name:NSTaskDidTerminateNotification object:task]; -} - -- (void)cancelDownload:(NSString *)key -{ - id task = [downloadTasks objectForKey:key]; - if (task == nil) - return; - - [task interrupt]; -} @end diff --git a/OpenRA.Launcher.Mac/JSBridge.h b/OpenRA.Launcher.Mac/JSBridge.h index bd24b753c9..a4a78dcd01 100644 --- a/OpenRA.Launcher.Mac/JSBridge.h +++ b/OpenRA.Launcher.Mac/JSBridge.h @@ -10,6 +10,7 @@ #import @class Controller; +@class Download; @interface JSBridge : NSObject { Controller *controller; NSDictionary *methods; @@ -18,5 +19,5 @@ + (JSBridge *)sharedInstance; - (void)setController:(Controller *)aController; - +- (void)notifyDownloadProgress:(Download *)download; @end diff --git a/OpenRA.Launcher.Mac/JSBridge.m b/OpenRA.Launcher.Mac/JSBridge.m index f5f1f36bc5..faab102baa 100644 --- a/OpenRA.Launcher.Mac/JSBridge.m +++ b/OpenRA.Launcher.Mac/JSBridge.m @@ -8,6 +8,8 @@ #import "JSBridge.h" #import "Controller.h" +#import "Download.h" +#import "Mod.h" static JSBridge *SharedInstance; @@ -40,9 +42,10 @@ static JSBridge *SharedInstance; methods = [[NSDictionary dictionaryWithObjectsAndKeys: @"launchMod", NSStringFromSelector(@selector(launchMod:)), @"log", NSStringFromSelector(@selector(log:)), - @"installCncPackagesFromWeb", NSStringFromSelector(@selector(installCncPackagesFromWeb)), @"fileExistsInMod", NSStringFromSelector(@selector(fileExists:inMod:)), - @"downloadFileToCache",NSStringFromSelector(@selector(downloadFile:intoCacheWithName:id:)), + @"fileExistsInCache", NSStringFromSelector(@selector(fileExistsInCache:)), + @"downloadFileToCache", NSStringFromSelector(@selector(downloadFileIntoCache:withName:key:)), + @"cancelDownload", NSStringFromSelector(@selector(cancelDownload:)), nil] retain]; } return self; @@ -59,7 +62,14 @@ static JSBridge *SharedInstance; [super dealloc]; } -#pragma mark JS methods +- (void)notifyDownloadProgress:(Download *)download +{ + NSLog(@"notified"); + //[[[controller webView] windowScriptObject] evaluateWebScript: + // @"updateDownloadStatus(’sample_graphic.jpg’, ‘320’, ‘240’)"]; +} + +#pragma mark JS API methods - (BOOL)launchMod:(NSString *)aMod { @@ -89,11 +99,25 @@ static JSBridge *SharedInstance; return YES; } -- (void)downloadFile:(NSString *)url intoCacheWithName:(NSString *)name id:(NSString *)did +- (BOOL)fileExistsInCache:(NSString *)name { - NSLog(@"downloadFile:%@ intoCacheWithName:%@ id:%@",url,name,did); // Disallow traversing directories; take only the last component - [controller downloadUrl:url intoCache:[name lastPathComponent] withId:did]; + id path = [[@"~/Library/Application Support/OpenRA/Downloads/" stringByAppendingPathComponent:[name lastPathComponent]] stringByExpandingTildeInPath]; + return [[NSFileManager defaultManager] fileExistsAtPath:path]; +} + +- (void)downloadFileIntoCache:(NSString *)url withName:(NSString *)name key:(NSString *)key +{ + NSLog(@"downloadFile:%@ intoCacheWithName:%@ key:%@",url,name,key); + + // Disallow traversing directories; take only the last component + id path = [[@"~/Library/Application Support/OpenRA/Downloads/" stringByAppendingPathComponent:[name lastPathComponent]] stringByExpandingTildeInPath]; + [controller downloadUrl:url toFile:path withId:key]; +} + +- (void)cancelDownload:(NSString *)key +{ + [controller cancelDownload:key]; } - (void)log:(NSString *)message diff --git a/OpenRA.Launcher.Mac/OpenRA.xcodeproj/project.pbxproj b/OpenRA.Launcher.Mac/OpenRA.xcodeproj/project.pbxproj index ea2486a9ed..23705fd051 100644 --- a/OpenRA.Launcher.Mac/OpenRA.xcodeproj/project.pbxproj +++ b/OpenRA.Launcher.Mac/OpenRA.xcodeproj/project.pbxproj @@ -13,6 +13,7 @@ 8D11072D0486CEB800E47090 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 29B97316FDCFA39411CA2CEA /* main.m */; settings = {ATTRIBUTES = (); }; }; 8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; }; DA38212212925344003B0BB5 /* JSBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = DA38212112925344003B0BB5 /* JSBridge.m */; }; + DA7D85671295E92900E58547 /* Download.m in Sources */ = {isa = PBXBuildFile; fileRef = DA7D85661295E92900E58547 /* Download.m */; }; DA81FA821290F5C800C48F2F /* Controller.m in Sources */ = {isa = PBXBuildFile; fileRef = DA81FA811290F5C800C48F2F /* Controller.m */; }; DA81FAAA1290FA0000C48F2F /* Mod.m in Sources */ = {isa = PBXBuildFile; fileRef = DA81FAA91290FA0000C48F2F /* Mod.m */; }; DA81FB9312910A8B00C48F2F /* ImageAndTextCell.m in Sources */ = {isa = PBXBuildFile; fileRef = DA81FB9212910A8B00C48F2F /* ImageAndTextCell.m */; }; @@ -37,6 +38,8 @@ 8D1107320486CEB800E47090 /* OpenRA.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = OpenRA.app; sourceTree = BUILT_PRODUCTS_DIR; }; DA38212012925344003B0BB5 /* JSBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JSBridge.h; sourceTree = ""; }; DA38212112925344003B0BB5 /* JSBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSBridge.m; sourceTree = ""; }; + DA7D85651295E92900E58547 /* Download.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Download.h; sourceTree = ""; }; + DA7D85661295E92900E58547 /* Download.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Download.m; sourceTree = ""; }; DA81FA801290F5C800C48F2F /* Controller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Controller.h; sourceTree = ""; }; DA81FA811290F5C800C48F2F /* Controller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Controller.m; sourceTree = ""; }; DA81FAA81290FA0000C48F2F /* Mod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Mod.h; sourceTree = ""; }; @@ -78,6 +81,8 @@ DA81FAA91290FA0000C48F2F /* Mod.m */, DA81FC3D12911E2B00C48F2F /* GameInstall.h */, DA81FC3E12911E2B00C48F2F /* GameInstall.m */, + DA7D85651295E92900E58547 /* Download.h */, + DA7D85661295E92900E58547 /* Download.m */, DA92968E1292328200EDB02E /* SidebarEntry.h */, DA92968F1292328200EDB02E /* SidebarEntry.m */, DA38212012925344003B0BB5 /* JSBridge.h */, @@ -226,6 +231,7 @@ DA81FC3F12911E2B00C48F2F /* GameInstall.m in Sources */, DA9296901292328200EDB02E /* SidebarEntry.m in Sources */, DA38212212925344003B0BB5 /* JSBridge.m in Sources */, + DA7D85671295E92900E58547 /* Download.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/mods/cnc/mod.html b/mods/cnc/mod.html index 35586081ac..ce341ef67b 100644 --- a/mods/cnc/mod.html +++ b/mods/cnc/mod.html @@ -106,12 +106,25 @@ window.external.log("installFromCD()"); } - function installFromWeb() + function download1() { - window.external.downloadFileToCache("http://www.open-ra.org/get-dependency.php?file=cnc-packages","test.zip","cnc-packages"); - //window.external.installCncPackagesFromWeb(); + window.external.downloadFileToCache("http://www.open-ra.org/get-dependency.php?file=cnc-packages","test.zip","cnc-packages"); } - + function download2() + { + window.external.downloadFileToCache("http://www.open-ra.org/get-dependency.php?file=ra-packages","test2.zip","ra-packages"); + } + + function cancel1() + { + window.external.cancelDownload("cnc-packages"); + } + + function cancel2() + { + window.external.cancelDownload("ra-packages"); + } + function onLoad() { document.getElementById("buttons-install").style.display = (packagesInstalled() == 0) ? "" : "none"; @@ -142,8 +155,11 @@ Installing from web will install the minimal files required to play.
Installing from CD will also install the music and movie files for an improved game experience. - - + + + + +