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.

This commit is contained in:
Paul Chote
2010-11-19 13:41:24 +13:00
parent c3521a2490
commit 0ee1d39bac
10 changed files with 201 additions and 66 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,23 @@
//
// Download.h
// OpenRA
//
// Created by Paul Chote on 19/11/10.
// Copyright 2010 __MyCompanyName__. All rights reserved.
//
#import <Cocoa/Cocoa.h>
@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

View File

@@ -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

View File

@@ -9,8 +9,10 @@
#import <Cocoa/Cocoa.h>
@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

View File

@@ -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

View File

@@ -10,6 +10,7 @@
#import <Cocoa/Cocoa.h>
@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

View File

@@ -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

View File

@@ -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 = "<group>"; };
DA38212112925344003B0BB5 /* JSBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JSBridge.m; sourceTree = "<group>"; };
DA7D85651295E92900E58547 /* Download.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Download.h; sourceTree = "<group>"; };
DA7D85661295E92900E58547 /* Download.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Download.m; sourceTree = "<group>"; };
DA81FA801290F5C800C48F2F /* Controller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Controller.h; sourceTree = "<group>"; };
DA81FA811290F5C800C48F2F /* Controller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Controller.m; sourceTree = "<group>"; };
DA81FAA81290FA0000C48F2F /* Mod.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Mod.h; sourceTree = "<group>"; };
@@ -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;
};

View File

@@ -106,10 +106,23 @@
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();
}
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()
@@ -142,8 +155,11 @@
Installing from web will install the minimal files required to play.<br />
Installing from CD will also install the music and movie files for an improved game experience.
</div>
<input type="button" class="button" onclick="installFromWeb();" value="Install from Web" />
<input type="button" class="button" onclick="installFromCD();" value="Install from CD" />
<input type="button" class="button" onclick="download1();" value="Download1" />
<input type="button" class="button" onclick="cancel1();" value="Cancel1" />
<input type="button" class="button" onclick="download2();" value="Download2" />
<input type="button" class="button" onclick="cancel2();" value="Cancel2" />
</div>
<div id="buttons-upgrade" class="buttons" style="display:none">
<div class="desc">