diff options
Diffstat (limited to 'Source/SPBundleCommandRunner.m')
-rw-r--r-- | Source/SPBundleCommandRunner.m | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/Source/SPBundleCommandRunner.m b/Source/SPBundleCommandRunner.m new file mode 100644 index 00000000..5d2336f2 --- /dev/null +++ b/Source/SPBundleCommandRunner.m @@ -0,0 +1,340 @@ +// +// $Id$ +// +// SPBundleCommandRunner.m +// Sequel Pro +// +// Created by Stuart Connolly (stuconnolly.com) on May 6, 2012 +// Copyright (c) 2012 Stuart Connolly. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the "Software"), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// +// More info at <http://code.google.com/p/sequel-pro/> + +#import "SPBundleCommandRunner.h" +#import "SPDatabaseDocument.h" + +// Defined to suppress warnings +@interface NSObject (SPBundleMethods) + +- (NSString *)lastBundleBlobFilesDirectory; +- (void)setLastBundleBlobFilesDirectory:(NSString *)path; + +@end + +// Defined to suppress warnings +@interface NSObject (SPWindowControllerTabMethods) + +- (id)selectedTableDocument; + +@end + +@implementation SPBundleCommandRunner + +#ifndef SP_REFACTOR /* run commands */ + +/** + * Run the supplied string as a BASH command(s) and return the result. + * This task can be interrupted by pressing ⌘. + * + * @param command The command to run + * @param shellEnvironment A dictionary of environment variable values whose keys are the variable names. + * @param path The current directory for the bash command. If path is nil, the current directory is inherited from the process that created the receiver (normally /). + * @param theError If not nil and the bash command failed it contains the returned error message as NSLocalizedDescriptionKey + * + */ ++ (NSString *)runBashCommand:(NSString *)command withEnvironment:(NSDictionary*)shellEnvironment atCurrentDirectoryPath:(NSString*)path error:(NSError**)theError +{ + return [SPBundleCommandRunner runBashCommand:command withEnvironment:shellEnvironment atCurrentDirectoryPath:path callerInstance:nil contextInfo:nil error:theError]; +} + +/** + * Run the supplied command as a BASH command(s) and return the result. + * This task can be interrupted by pressing ⌘. + * + * @param command The command to run + * @param shellEnvironment A dictionary of environment variable values whose keys are the variable names. + * @param path The current directory for the bash command. If path is nil, the current directory is inherited from the process that created the receiver (normally /). + * @param caller The SPDatabaseDocument which invoked that command to register the command for cancelling; if nil the command won't be registered. + * @param name The menu title of the command. + * @param theError If not nil and the bash command failed it contains the returned error message as NSLocalizedDescriptionKey + * + */ ++ (NSString *)runBashCommand:(NSString *)command withEnvironment:(NSDictionary*)shellEnvironment atCurrentDirectoryPath:(NSString*)path callerInstance:(id)caller contextInfo:(NSDictionary*)contextInfo error:(NSError**)theError +{ + NSFileManager *fm = [NSFileManager defaultManager]; + + BOOL userTerminated = NO; + BOOL redirectForScript = NO; + BOOL isDir = NO; + + NSMutableArray *scriptHeaderArguments = [NSMutableArray array]; + NSString *scriptPath = @""; + NSString *uuid = (contextInfo && [contextInfo objectForKey:SPBundleFileInternalexecutionUUID]) ? [contextInfo objectForKey:SPBundleFileInternalexecutionUUID] : [NSString stringWithNewUUID]; + NSString *stdoutFilePath = [NSString stringWithFormat:@"%@_%@", SPBundleTaskOutputFilePath, uuid]; + NSString *scriptFilePath = [NSString stringWithFormat:@"%@_%@", SPBundleTaskScriptCommandFilePath, uuid]; + + [fm removeItemAtPath:scriptFilePath error:nil]; + [fm removeItemAtPath:stdoutFilePath error:nil]; + if([[NSApp delegate] lastBundleBlobFilesDirectory] != nil) + [fm removeItemAtPath:[[NSApp delegate] lastBundleBlobFilesDirectory] error:nil]; + + if([shellEnvironment objectForKey:SPBundleShellVariableBlobFileDirectory]) + [[NSApp delegate] setLastBundleBlobFilesDirectory:[shellEnvironment objectForKey:SPBundleShellVariableBlobFileDirectory]]; + + // Parse first line for magic header #! ; if found save the script content and run the command after #! with that file. + // This allows to write perl, ruby, osascript scripts natively. + if([command length] > 3 && [command hasPrefix:@"#!"] && [shellEnvironment objectForKey:SPBundleShellVariableBundlePath]) { + + NSRange firstLineRange = NSMakeRange(2, [command rangeOfString:@"\n"].location - 2); + + [scriptHeaderArguments setArray:[[command substringWithRange:firstLineRange] componentsSeparatedByString:@" "]]; + + while([scriptHeaderArguments containsObject:@""]) + [scriptHeaderArguments removeObject:@""]; + + if([scriptHeaderArguments count]) + scriptPath = [scriptHeaderArguments objectAtIndex:0]; + + if([scriptPath hasPrefix:@"/"] && [fm fileExistsAtPath:scriptPath isDirectory:&isDir] && !isDir) { + NSString *script = [command substringWithRange:NSMakeRange(NSMaxRange(firstLineRange), [command length] - NSMaxRange(firstLineRange))]; + NSError *writeError = nil; + [script writeToFile:scriptFilePath atomically:YES encoding:NSUTF8StringEncoding error:&writeError]; + if(writeError == nil) { + redirectForScript = YES; + [scriptHeaderArguments addObject:scriptFilePath]; + } else { + NSBeep(); + NSLog(@"Couldn't write script file."); + } + } + } else { + [scriptHeaderArguments addObject:@"/bin/sh"]; + NSError *writeError = nil; + [command writeToFile:scriptFilePath atomically:YES encoding:NSUTF8StringEncoding error:&writeError]; + if(writeError == nil) { + redirectForScript = YES; + [scriptHeaderArguments addObject:scriptFilePath]; + } else { + NSBeep(); + NSLog(@"Couldn't write script file."); + } + } + + NSTask *bashTask = [[NSTask alloc] init]; + [bashTask setLaunchPath:@"/bin/bash"]; + + NSMutableDictionary *theEnv = [NSMutableDictionary dictionary]; + [theEnv setDictionary:shellEnvironment]; + + [theEnv setObject:[[NSBundle mainBundle] pathForResource:@"appicon" ofType:@"icns"] forKey:SPBundleShellVariableIconFile]; + [theEnv setObject:[NSString stringWithFormat:@"%@/Contents/Resources", [[NSBundle mainBundle] bundlePath]] forKey:SPBundleShellVariableAppResourcesDirectory]; + [theEnv setObject:[NSNumber numberWithInteger:SPBundleRedirectActionNone] forKey:SPBundleShellVariableExitNone]; + [theEnv setObject:[NSNumber numberWithInteger:SPBundleRedirectActionReplaceSection] forKey:SPBundleShellVariableExitReplaceSelection]; + [theEnv setObject:[NSNumber numberWithInteger:SPBundleRedirectActionReplaceContent] forKey:SPBundleShellVariableExitReplaceContent]; + [theEnv setObject:[NSNumber numberWithInteger:SPBundleRedirectActionInsertAsText] forKey:SPBundleShellVariableExitInsertAsText]; + [theEnv setObject:[NSNumber numberWithInteger:SPBundleRedirectActionInsertAsSnippet] forKey:SPBundleShellVariableExitInsertAsSnippet]; + [theEnv setObject:[NSNumber numberWithInteger:SPBundleRedirectActionShowAsHTML] forKey:SPBundleShellVariableExitShowAsHTML]; + [theEnv setObject:[NSNumber numberWithInteger:SPBundleRedirectActionShowAsTextTooltip] forKey:SPBundleShellVariableExitShowAsTextTooltip]; + [theEnv setObject:[NSNumber numberWithInteger:SPBundleRedirectActionShowAsHTMLTooltip] forKey:SPBundleShellVariableExitShowAsHTMLTooltip]; + + // Create and set an unique process ID for each SPDatabaseDocument which has to passed + // for each sequelpro:// scheme command as user to be able to identify the url scheme command. + // Furthermore this id is used to communicate with the called command as file name. + id doc = nil; + if([[[NSApp mainWindow] delegate] respondsToSelector:@selector(selectedTableDocument)]) + doc = [[[NSApp mainWindow] delegate] selectedTableDocument]; + // Check if connected + if([doc getConnection] == nil) + doc = nil; + else { + for (NSWindow *aWindow in [NSApp orderedWindows]) { + if([[[[aWindow windowController] class] description] isEqualToString:@"SPWindowController"]) { + if([[[aWindow windowController] documents] count] && [[[[[[aWindow windowController] documents] objectAtIndex:0] class] description] isEqualToString:@"SPDatabaseDocument"]) { + // Check if connected + if([[[[aWindow windowController] documents] objectAtIndex:0] getConnection]) + doc = [[[aWindow windowController] documents] objectAtIndex:0]; + else + doc = nil; + } + } + if(doc) break; + } + } + + if(doc != nil) { + + [doc setProcessID:uuid]; + + [theEnv setObject:uuid forKey:SPBundleShellVariableProcessID]; + [theEnv setObject:[NSString stringWithFormat:@"%@%@", SPURLSchemeQueryInputPathHeader, uuid] forKey:SPBundleShellVariableQueryFile]; + [theEnv setObject:[NSString stringWithFormat:@"%@%@", SPURLSchemeQueryResultPathHeader, uuid] forKey:SPBundleShellVariableQueryResultFile]; + [theEnv setObject:[NSString stringWithFormat:@"%@%@", SPURLSchemeQueryResultStatusPathHeader, uuid] forKey:SPBundleShellVariableQueryResultStatusFile]; + [theEnv setObject:[NSString stringWithFormat:@"%@%@", SPURLSchemeQueryResultMetaPathHeader, uuid] forKey:SPBundleShellVariableQueryResultMetaFile]; + + if([doc shellVariables]) + [theEnv addEntriesFromDictionary:[doc shellVariables]]; + + if([theEnv objectForKey:SPBundleShellVariableCurrentEditedColumnName] && [[theEnv objectForKey:SPBundleShellVariableDataTableSource] isEqualToString:@"content"]) + [theEnv setObject:[theEnv objectForKey:SPBundleShellVariableSelectedTable] forKey:SPBundleShellVariableCurrentEditedTable]; + + } + + if(theEnv != nil && [theEnv count]) + [bashTask setEnvironment:theEnv]; + + if(path != nil) + [bashTask setCurrentDirectoryPath:path]; + else if([shellEnvironment objectForKey:SPBundleShellVariableBundlePath] && [fm fileExistsAtPath:[shellEnvironment objectForKey:SPBundleShellVariableBundlePath] isDirectory:&isDir] && isDir) + [bashTask setCurrentDirectoryPath:[shellEnvironment objectForKey:SPBundleShellVariableBundlePath]]; + + // STDOUT will be redirected to SPBundleTaskOutputFilePath in order to avoid nasty pipe programming due to block size reading + if([shellEnvironment objectForKey:SPBundleShellVariableInputFilePath]) + [bashTask setArguments:[NSArray arrayWithObjects:@"-c", [NSString stringWithFormat:@"%@ > %@ < %@", [scriptHeaderArguments componentsJoinedByString:@" "], stdoutFilePath, [shellEnvironment objectForKey:SPBundleShellVariableInputFilePath]], nil]]; + else + [bashTask setArguments:[NSArray arrayWithObjects:@"-c", [NSString stringWithFormat:@"%@ > %@", [scriptHeaderArguments componentsJoinedByString:@" "], stdoutFilePath], nil]]; + + NSPipe *stderr_pipe = [NSPipe pipe]; + [bashTask setStandardError:stderr_pipe]; + NSFileHandle *stderr_file = [stderr_pipe fileHandleForReading]; + [bashTask launch]; + NSInteger pid = -1; + if(caller != nil && [caller respondsToSelector:@selector(registerActivity:)]) { + // register command + pid = [bashTask processIdentifier]; + NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInteger:pid], @"pid", + (contextInfo)?:[NSDictionary dictionary], @"contextInfo", + @"bashcommand", @"type", + [[NSDate date] descriptionWithCalendarFormat:@"%H:%M:%S" timeZone:nil locale:[[NSUserDefaults standardUserDefaults] dictionaryRepresentation]], @"starttime", + nil]; + [caller registerActivity:dict]; + } + + // Listen to ⌘. to terminate + while(1) { + if(![bashTask isRunning] || [bashTask processIdentifier] == 0) break; + NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask + untilDate:[NSDate distantPast] + inMode:NSDefaultRunLoopMode + dequeue:YES]; + usleep(1000); + if(!event) continue; + if ([event type] == NSKeyDown) { + unichar key = [[event characters] length] == 1 ? [[event characters] characterAtIndex:0] : 0; + if (([event modifierFlags] & NSCommandKeyMask) && key == '.') { + [bashTask terminate]; + userTerminated = YES; + break; + } + [NSApp sendEvent:event]; + } else { + [NSApp sendEvent:event]; + } + } + + [bashTask waitUntilExit]; + + // unregister BASH command if it was registered + if(pid > 0) { + [caller removeRegisteredActivity:pid]; + } + + // Remove files + [fm removeItemAtPath:scriptFilePath error:nil]; + if([theEnv objectForKey:SPBundleShellVariableQueryFile]) + [fm removeItemAtPath:[theEnv objectForKey:SPBundleShellVariableQueryFile] error:nil]; + if([theEnv objectForKey:SPBundleShellVariableQueryResultFile]) + [fm removeItemAtPath:[theEnv objectForKey:SPBundleShellVariableQueryResultFile] error:nil]; + if([theEnv objectForKey:SPBundleShellVariableQueryResultStatusFile]) + [fm removeItemAtPath:[theEnv objectForKey:SPBundleShellVariableQueryResultStatusFile] error:nil]; + if([theEnv objectForKey:SPBundleShellVariableQueryResultMetaFile]) + [fm removeItemAtPath:[theEnv objectForKey:SPBundleShellVariableQueryResultMetaFile] error:nil]; + if([theEnv objectForKey:SPBundleShellVariableInputTableMetaData]) + [fm removeItemAtPath:[theEnv objectForKey:SPBundleShellVariableInputTableMetaData] error:nil]; + + // If return from bash re-activate Sequel Pro + [NSApp activateIgnoringOtherApps:YES]; + + NSInteger status = [bashTask terminationStatus]; + NSData *errdata = [stderr_file readDataToEndOfFile]; + + // Check STDERR + if([errdata length] && (status < SPBundleRedirectActionNone || status > SPBundleRedirectActionLastCode)) { + [fm removeItemAtPath:stdoutFilePath error:nil]; + + if(status == 9 || userTerminated) return @""; + if(theError != NULL) { + NSMutableString *errMessage = [[[NSMutableString alloc] initWithData:errdata encoding:NSUTF8StringEncoding] autorelease]; + [errMessage replaceOccurrencesOfString:[NSString stringWithFormat:@"%@: ", scriptFilePath] withString:@"" options:NSLiteralSearch range:NSMakeRange(0, [errMessage length])]; + *theError = [[[NSError alloc] initWithDomain:NSPOSIXErrorDomain + code:status + userInfo:[NSDictionary dictionaryWithObjectsAndKeys: + errMessage, + NSLocalizedDescriptionKey, + nil]] autorelease]; + } else { + NSBeep(); + } + return @""; + } + + // Read STDOUT saved to file + if([fm fileExistsAtPath:stdoutFilePath isDirectory:nil]) { + NSString *stdoutContent = [NSString stringWithContentsOfFile:stdoutFilePath encoding:NSUTF8StringEncoding error:nil]; + if(bashTask) [bashTask release], bashTask = nil; + [fm removeItemAtPath:stdoutFilePath error:nil]; + if(stdoutContent != nil) { + if (status == 0) { + return stdoutContent; + } else { + if(theError != NULL) { + if(status == 9 || userTerminated) return @""; + NSMutableString *errMessage = [[[NSMutableString alloc] initWithData:errdata encoding:NSUTF8StringEncoding] autorelease]; + [errMessage replaceOccurrencesOfString:SPBundleTaskScriptCommandFilePath withString:@"" options:NSLiteralSearch range:NSMakeRange(0, [errMessage length])]; + *theError = [[[NSError alloc] initWithDomain:NSPOSIXErrorDomain + code:status + userInfo:[NSDictionary dictionaryWithObjectsAndKeys: + errMessage, + NSLocalizedDescriptionKey, + nil]] autorelease]; + } else { + NSBeep(); + } + if(status > SPBundleRedirectActionNone && status <= SPBundleRedirectActionLastCode) + return stdoutContent; + else + return @""; + } + } else { + NSLog(@"Couldn't read return string from “%@” by using UTF-8 encoding.", command); + NSBeep(); + } + } + + if (bashTask) [bashTask release]; + [fm removeItemAtPath:stdoutFilePath error:nil]; + return @""; +} + +#endif + +@end |