aboutsummaryrefslogtreecommitdiffstats
path: root/Source/SPBundleCommandRunner.m
diff options
context:
space:
mode:
Diffstat (limited to 'Source/SPBundleCommandRunner.m')
-rw-r--r--Source/SPBundleCommandRunner.m340
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