Permalink
Browse files

Refactored GBTask to allow continuous reporting of received output an…

…d error.

This allows users see progress while indexing large documentation sets for example.
  • Loading branch information...
1 parent 6c5cd0d commit dff70ffaad8cbd3231eabb2cdb240e9868d3dbdb @tomaz committed Dec 2, 2010
Showing with 123 additions and 19 deletions.
  1. +0 −8 Common/GBLog.h
  2. +47 −5 Common/GBTask.h
  3. +70 −3 Common/GBTask.m
  4. +6 −3 Generating/GBDocSetOutputGenerator.m
View
@@ -60,14 +60,6 @@ extern NSUInteger kGBLogLevel;
#define LOG_LEVEL_VERBOSE (LOG_FLAG_VERBOSE | LOG_LEVEL_INFO) // 0...0111111
#define LOG_LEVEL_DEBUG (LOG_FLAG_DEBUG | LOG_LEVEL_VERBOSE) // 0...1111111
-#define LOG_FATAL (kGBLogLevel & LOG_FLAG_FATAL)
-#define LOG_ERROR (kGBLogLevel & LOG_FLAG_ERROR)
-#define LOG_WARN (kGBLogLevel & LOG_FLAG_WARN)
-#define LOG_NORMAL (kGBLogLevel & LOG_FLAG_NORMAL)
-#define LOG_INFO (kGBLogLevel & LOG_FLAG_INFO)
-#define LOG_VERBOSE (kGBLogLevel & LOG_FLAG_VERBOSE)
-#define LOG_DEBUG (kGBLogLevel & LOG_FLAG_DEBUG)
-
#define SYNC_LOG_OBJC_MAYBE(lvl, flg, frmt, ...) LOG_MAYBE(YES, lvl, flg, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)
#define GBLogFatal(frmt, ...) SYNC_LOG_OBJC_MAYBE(kGBLogLevel, LOG_FLAG_FATAL, frmt, ##__VA_ARGS__)
View
@@ -8,18 +8,26 @@
#import <Foundation/Foundation.h>
+typedef void(^GBTaskReportBlock)(NSString *output, NSString *error);
+
/** Implements a simpler interface to `NSTask`.
- To use, instantiate the class and send `runCommand:` message like this:
+ The class is designed to be completely reusable - it doesn't depend on any project specific object or external library, so you can simply copy the .h and .m files to another project and use it. To run a command instantiate the class and send `runCommand:` message. You can pass in optional arguments if needed:
GBTask *task = [GBTask task];
- [task runCommand:@"/bin/ls", nil];
+ [task runCommand:@"/bin/ls", nil];
+ [task runCommand:@"/bin/ls", @"-l", @"-a", nil];
- You can also pass arguments to the command and read the output:
+ If you want to be continuously notified when output or error is reported by the command (for example when you're running lenghtier commands and want to update user interface so the user is aware something is happening), use block method `runCommand:arguments:block`:
- NSString *result = [task runCommand:@"/bin/ls", @"-l", @"-a", nil];
+ GBTask *task = [GBTask task];
+ [task runCommand:@"/bin/ls" arguments:nil block:^(NSString *output, NSString *error) {
+ // do something with output and error here...
+ }];
+
+ You can affect how the output and error is reported through by changing the value of `reportIndividualLines`.
- You can reuse the same instance for any number of commands.
+ You can reuse the same instance for any number of commands. After the command is finished, you can examine it's results through `lastStandardOutput` and `lastStandardError` properties. You can also check the actual command line string used for running the command through `lastCommandLine`; this value includes the command and all parameters in a single string. If any parameter contains whitespace, it is embedded into quotes. All these properties work the same regardless of the way you run the command.
*/
@interface GBTask : NSObject
@@ -39,19 +47,51 @@
The command is run synchronously; the application is halted until the command completes. All standard output and error from the command is copied to `lastStandardOutput` and `lastStandardError` properties. If you're interested in these values, check the values. The result of the method is determined from `lastStandardError` value - if it contains non-empty string, error is reported, otherwise success. This should work for most commands, but if you use it on a command that emits errors to standard output, you should not rely solely on method result to determine success - you should instead parse the output string for indications of errors!
+ Internally, sending this message is equivalent to sending `runCommand:arguments:block:` with wrapping all the arguments into an `NSArray` and passing `nil` for block!
+
@param command Full path to the command to run.
@param ... A comma separated list of arguments to substitute into the format.
@return Returns `YES` if command succedded, `NO` otherwise.
@exception NSException Thrown if the given command is invalid or cannot be started.
+ @see runCommand:arguments:block:
@see lastCommandLine
@see lastStandardOutput
@see lastStandardError
*/
- (BOOL)runCommand:(NSString *)command, ... NS_REQUIRES_NIL_TERMINATION;
+/** Runs the given command and optional arguments using the given block to continuously report back any output or error received from the command while it's running.
+
+ In contrast to `runCommand:`, this method uses the given block to report any string received on standard output or error, immediately when the command emits it. The block reports only the type of input received - if output is received only, error is `nil` and vice versa. In addition, all strings are concatenated and copied into `lastStandardOutput` and `lastStandardError` respectively. However these properties are only useful after the method returns. To change the way reporting is handled, use `reportIndividualLines` property. Note that if `nil` is passed for block, the method simply reverts to normal handling and doesn't use block.
+
+ The command is run synchronously; the application is halted until the command completes. All standard output and error from the command is copied to `lastStandardOutput` and `lastStandardError` properties. The result of the method is determined from `lastStandardError` value - if it contains non-empty string, error is reported, otherwise success. This should work for most commands, but if you use it on a command that emits errors to standard output, you should not rely solely on method results to determine success - you should instea parse the output string for indications of errors!
+
+ @param command Full path to the command to run.
+ @param arguments Array of arguments or `nil` if no arguments are used.
+ @param block Block to use for continuous reporting or `nil` to not use block.
+ @return Returns `YES` if command succedded, `NO` otherwise.
+ @exception NSException Thrown if the given command is invalid or cannot be started.
+ @see runCommand:
+ @see lastCommandLine
+ @see lastStandardOutput
+ @see lastStandardError
+ */
+- (BOOL)runCommand:(NSString *)command arguments:(NSArray *)arguments block:(GBTaskReportBlock)block;
+
+/** Specifies whether output reported while the command is running is split to individual lines or not.
+
+ If set to `YES`, any output from standard output and error is first split to individual lines, then each line is reported separately. This can be useful in cases where multiple lines are reported in one block call, but we want to handle them line by line. Turning the option on does reduce runtime performance, so be sure to measure it. Defaults to `NO`.
+ */
+@property (assign) BOOL reportIndividualLines;
+
+///---------------------------------------------------------------------------------------
+/// @name Last results
+///---------------------------------------------------------------------------------------
+
/** Returns last command line including all arguments as passed to `runCommand:` the last it was sent.
@see runCommand:
+ @see runCommand:arguments:block:
@see lastStandardOutput
@see lastStandardError
*/
@@ -60,13 +100,15 @@
/** Returns string emited to standard output pipe the last time `runCommand:` was sent.
@see runCommand:
+ @see runCommand:arguments:block:
@see lastStandardError
*/
@property (readonly, retain) NSString *lastStandardOutput;
/** Returns string emited to standard error pipe the last time `runCommand:` was sent.
@see runCommand:
+ @see runCommand:arguments:block:
@see lastStandardOutput
*/
@property (readonly, retain) NSString *lastStandardError;
View
@@ -12,6 +12,8 @@ @interface GBTask ()
- (NSString *)stringFromPipe:(NSPipe *)pipe;
- (NSArray *)commandLineArgumentsFromList:(va_list)args;
+- (NSArray *)linesFromString:(NSString *)string;
+@property (copy) GBTaskReportBlock reportBlock;
@property (readwrite, retain) NSString *lastCommandLine;
@property (readwrite, retain) NSString *lastStandardOutput;
@property (readwrite, retain) NSString *lastStandardError;
@@ -36,30 +38,79 @@ - (BOOL)runCommand:(NSString *)command, ... {
va_start(args, command);
NSArray *arguments = [self commandLineArgumentsFromList:args];
va_end(args);
+ return [self runCommand:command arguments:arguments block:nil];
+}
+
+- (BOOL)runCommand:(NSString *)command arguments:(NSArray *)arguments block:(GBTaskReportBlock)block {
+ // If nil is passed for arguments, convert it to empty array.
+ if (!arguments) arguments = [NSArray array];
// Log the command we're about to run.
NSMutableString *commandLine = [NSMutableString string];
for (id argument in arguments) [commandLine appendFormat:@" %@", argument];
self.lastCommandLine = [NSString stringWithFormat:@"%@%@", command, commandLine];
GBLogDebug(@"Running command '%@'", self.lastCommandLine);
- // Ok, now prepare the NSTask and really run the command... Note that [NSTask launch] raises exception if it can't launch, we just pass it on.
+ // Prepare deviation pipes so that we can extract the data from the task. If requested, prepare everything for continuous updating.
NSPipe *stdOutPipe = [NSPipe pipe];
NSPipe *stdErrPipe = [NSPipe pipe];
+ if (block) {
+ NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
+ [center addObserver:self selector:@selector(outputHandleDataReceived:) name:NSFileHandleReadCompletionNotification object:[stdOutPipe fileHandleForReading]];
+ [center addObserver:self selector:@selector(errorHandleDataReceived:) name:NSFileHandleReadCompletionNotification object:[stdErrPipe fileHandleForReading]];
+ self.lastStandardOutput = @"";
+ self.lastStandardError = @"";
+ self.reportBlock = block;
+ }
+
+ // Ok, now prepare the NSTask and really run the command... Note that [NSTask launch] raises exception if it can't launch, we just pass it on.
NSTask *task = [[[NSTask alloc] init] autorelease];
[task setLaunchPath:command];
[task setArguments:arguments];
[task setStandardOutput:stdOutPipe];
[task setStandardError:stdErrPipe];
[task launch];
- self.lastStandardOutput = [self stringFromPipe:stdOutPipe];
- self.lastStandardError = [self stringFromPipe:stdErrPipe];
+ if (block) {
+ [[stdOutPipe fileHandleForReading] readInBackgroundAndNotify];
+ [[stdErrPipe fileHandleForReading] readInBackgroundAndNotify];
+ } else {
+ self.lastStandardOutput = [self stringFromPipe:stdOutPipe];
+ self.lastStandardError = [self stringFromPipe:stdErrPipe];
+ }
[task waitUntilExit];
// If we got something on standard error, report error, otherwise success.
return ([self.lastStandardError length] == 0);
}
+#pragma mark Continuous reporting handling
+
+- (void)outputHandleDataReceived:(NSNotification *)note {
+ NSData *data = [[note userInfo] objectForKey:NSFileHandleNotificationDataItem];
+ NSString *string = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
+ self.lastStandardOutput = [self.lastStandardOutput stringByAppendingFormat:@"%@\n", string];
+ if (self.reportIndividualLines) {
+ NSArray *lines = [self linesFromString:string];
+ for (NSString *line in lines) self.reportBlock(line, nil);
+ } else {
+ self.reportBlock(string, nil);
+ }
+ [[note object] readInBackgroundAndNotify];
+}
+
+- (void)errorHandleDataReceived:(NSNotification *)note {
+ NSData *data = [[note userInfo] objectForKey:NSFileHandleNotificationDataItem];
+ NSString *string = [[NSString alloc] initWithData:data encoding:NSASCIIStringEncoding];
+ self.lastStandardError = [self.lastStandardError stringByAppendingFormat:@"%@\n", string];
+ if (self.reportIndividualLines) {
+ NSArray *lines = [self linesFromString:string];
+ for (NSString *line in lines) self.reportBlock(nil, line);
+ } else {
+ self.reportBlock(nil, string);
+ }
+ [[note object] readInBackgroundAndNotify];
+}
+
#pragma mark Helper methods
- (NSString *)stringFromPipe:(NSPipe *)pipe {
@@ -78,8 +129,24 @@ - (NSArray *)commandLineArgumentsFromList:(va_list)args {
return result;
}
+- (NSArray *)linesFromString:(NSString *)string {
+ // This is copied from Apple documentation.
+ NSUInteger length = [string length];
+ NSUInteger paraStart = 0, paraEnd = 0, contentsEnd = 0;
+ NSMutableArray *result = [NSMutableArray array];
+ NSRange currentRange;
+ while (paraEnd < length) {
+ [string getParagraphStart:&paraStart end:&paraEnd contentsEnd:&contentsEnd forRange:NSMakeRange(paraEnd, 0)];
+ currentRange = NSMakeRange(paraStart, contentsEnd - paraStart);
+ [result addObject:[string substringWithRange:currentRange]];
+ }
+ return result;
+}
+
#pragma mark Properties
+@synthesize reportBlock;
+@synthesize reportIndividualLines;
@synthesize lastStandardOutput;
@synthesize lastStandardError;
@@ -177,9 +177,12 @@ - (BOOL)processTokensXml:(NSError **)error {
- (BOOL)indexDocSet:(NSError **)error {
GBLogInfo(@"Indexing DocSet...");
GBTask *task = [GBTask task];
- BOOL result = [task runCommand:@"/Developer/usr/bin/docsetutil", @"index", [self.outputUserPath stringByStandardizingPath], nil];
- if ([task.lastStandardOutput length] > 0) GBLogDebug(@"'%@' results:\n%@", task.lastCommandLine, task.lastStandardOutput);
- if ([task.lastStandardError length] > 0) GBLogError(@"'%@' ERRORS:\n%@", task.lastCommandLine, task.lastStandardError);
+ task.reportIndividualLines = YES;
+ NSArray *args = [NSArray arrayWithObjects:@"index", [self.outputUserPath stringByStandardizingPath], nil];
+ BOOL result = [task runCommand:@"/Developer/usr/bin/docsetutil" arguments:args block:^(NSString *output, NSString *error) {
+ if (output) GBLogDebug(@"> %@", [output stringByTrimmingWhitespaceAndNewLine]);
+ if (error) GBLogError(@"!> %@", [error stringByTrimmingWhitespaceAndNewLine]);
+ }];
if (!result) {
if (error) *error = [NSError errorWithCode:3 description:@"docsetutil failed to index the documentation set!" reason:task.lastStandardError];
return NO;

0 comments on commit dff70ff

Please sign in to comment.