Use io::spawn instead of oak::command_t for TextMate.system

Like with snippets, we no longer support executing a shebang-script given “inline” — though this was never a documented feature and I am not aware of anything having made use of it.
This commit is contained in:
Allan Odgaard
2013-05-16 18:49:22 +07:00
parent b6fce5fe30
commit 03aea09148

View File

@@ -1,39 +1,14 @@
#import "HOJSBridge.h"
#import <OakFoundation/NSString Additions.h>
#import <oak/debug.h>
#import <OakSystem/process.h>
#import <OakSystem/reader.h>
#import <cf/run_loop.h>
#import <OakFoundation/NSArray Additions.h>
#import <document/collection.h>
#import <text/utf8.h>
#import <ns/ns.h>
#import <command/runner.h>
#import <cf/run_loop.h>
@interface HOJSShellCommand : NSObject
{
OBJC_WATCH_LEAKS(HOJSShellCommand);
std::string command;
std::map<std::string, std::string> environment;
std::vector<char> outputData, errorData;
int status;
oak::process_t* process;
bool didCloseInput;
io::reader_t* outputReader;
io::reader_t* errorReader;
cf::run_loop_t runLoop;
size_t completeCounter;
// unused dummy keys to get them exposed to javascript
NSString* outputString;
NSString* errorString;
id onreadoutput, onreaderror;
}
+ (HOJSShellCommand*)runShellCommand:(NSString*)aCommand withEnvironment:(const std::map<std::string, std::string>&)someEnvironment andExitHandler:(id)aHandler;
- (id)initShellCommand:(NSString*)aCommand withEnvironment:(const std::map<std::string, std::string>&)someEnvironment andExitHandler:(id)aHandler;
@end
/*
@@ -130,7 +105,7 @@ OAK_DEBUG_VAR(HTMLOutput_JSBridge);
- (id)system:(NSString*)aCommand handler:(id)aHandler
{
return [HOJSShellCommand runShellCommand:aCommand withEnvironment:[self environment] andExitHandler:[aHandler isKindOfClass:[WebUndefined class]] ? nil : aHandler];
return [[HOJSShellCommand alloc] initShellCommand:aCommand withEnvironment:[self environment] andExitHandler:[aHandler isKindOfClass:[WebUndefined class]] ? nil : aHandler];
}
@end
@@ -139,7 +114,7 @@ OAK_DEBUG_VAR(HTMLOutput_JSBridge);
# Synchronous Operation
Example: obj = TextMate.system("/usr/bin/id -un", null);
Example: obj = TextMate.system("/usr/bin/id -un", null);
Result is an object with following properties:
@@ -173,178 +148,143 @@ OAK_DEBUG_VAR(HTMLOutput_JSBridge);
OAK_DEBUG_VAR(HTMLOutput_JSShellCommand);
@interface HOJSShellCommand ()
- (void)closeInput;
{
OBJC_WATCH_LEAKS(HOJSShellCommand);
@property (nonatomic, retain) id outputHandler;
@property (nonatomic, retain) id errorHandler;
@property (nonatomic, retain) id exitHandler;
io::process_t process;
std::string output, error;
// unused dummy keys to get them exposed to javascript
NSString* outputString;
NSString* errorString;
}
@property (nonatomic) id exitHandler;
@property (nonatomic) id onreadoutput;
@property (nonatomic) id onreaderror;
@property (nonatomic) int status;
@end
@implementation HOJSShellCommand
- (id)initWithCommand:(NSString*)aCommand andEnvironment:(const std::map<std::string, std::string>&)someEnvironment
- (id)initShellCommand:(NSString*)aCommand withEnvironment:(const std::map<std::string, std::string>&)someEnvironment andExitHandler:(id)aHandler
{
D(DBF_HTMLOutput_JSShellCommand, bug("run %s with exit handler %s\n", to_s(aCommand).c_str(), BSTR(aHandler)););
if(self = [super init])
{
command = [aCommand UTF8String];
environment = someEnvironment;
self.exitHandler = aHandler;
if(process = io::spawn(std::vector<std::string>{ "/bin/sh", "-c", to_s(aCommand) }, someEnvironment))
{
auto runLoop = new cf::run_loop_t(kCFRunLoopDefaultMode, 15);
auto group = dispatch_group_create();
auto queue = aHandler ? dispatch_get_main_queue() : dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
command::fix_shebang(&command);
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
std::vector<char> buf(1024);
while(ssize_t len = read(process.out, buf.data(), buf.size()))
{
if(len < 0)
break;
dispatch_sync(queue, ^{
if(self.onreadoutput)
output.erase(output.begin(), utf8::find_safe_end(output.begin(), output.end()));
output.insert(output.end(), buf.begin(), buf.begin() + len);
if(self.onreadoutput)
[self.onreadoutput callWebScriptMethod:@"call" withArguments:@[ self.onreadoutput, [self outputString] ]];
});
}
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
std::vector<char> buf(1024);
while(ssize_t len = read(process.err, buf.data(), buf.size()))
{
if(len < 0)
break;
dispatch_sync(queue, ^{
if(self.onreaderror)
error.erase(error.begin(), utf8::find_safe_end(error.begin(), error.end()));
error.insert(error.end(), buf.begin(), buf.begin() + len);
if(self.onreaderror)
[self.onreaderror callWebScriptMethod:@"call" withArguments:@[ self.onreaderror, [self errorString] ]];
});
}
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
int status = 0;
if(waitpid(process.pid, &status, 0) != process.pid)
perror("waitpid");
process.pid = -1;
dispatch_sync(queue, ^{
self.status = WIFEXITED(status) ? WEXITSTATUS(status) : -1;
});
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
close(process.out);
close(process.err);
if(self.exitHandler)
[self.exitHandler callWebScriptMethod:@"call" withArguments:@[ self.exitHandler, self ]];
else runLoop->stop();
});
if(!self.exitHandler)
{
[self closeInput];
while(runLoop->start() == false) // timeout
{
NSInteger choice = NSRunAlertPanel(@"JavaScript Warning", @"The command %@ has been running for 15 seconds. Would you like to stop it?", @"Stop Command", @"Cancel", nil, aCommand);
if(choice == NSAlertDefaultReturn) // "Stop Command"
{
[self cancelCommand];
break;
}
}
}
dispatch_release(group);
delete runLoop;
}
}
return self;
}
- (void)increaseCompleteCounter
{
if(++completeCounter == 3)
{
if(_exitHandler)
[_exitHandler callWebScriptMethod:@"call" withArguments:@[ _exitHandler, self ]];
else runLoop.stop();
}
}
- (void)outputDataReceived:(char const*)bytes length:(size_t)len
{
D(DBF_HTMLOutput_JSShellCommand, bug("%zu bytes\n", len););
if(_exitHandler)
outputData.erase(outputData.begin(), utf8::find_safe_end(outputData.begin(), outputData.end()));
outputData.insert(outputData.end(), bytes, bytes + len);
if(len == 0)
[self increaseCompleteCounter];
else if(_outputHandler)
[_outputHandler callWebScriptMethod:@"call" withArguments:@[ _outputHandler, [self valueForKey:@"outputString"] ]];
}
- (void)errorDataReceived:(char const*)bytes length:(size_t)len
{
D(DBF_HTMLOutput_JSShellCommand, bug("%zu bytes\n", len););
if(_exitHandler)
errorData.erase(errorData.begin(), utf8::find_safe_end(errorData.begin(), errorData.end()));
errorData.insert(errorData.end(), bytes, bytes + len);
if(len == 0)
[self increaseCompleteCounter];
else if(_errorHandler)
[_errorHandler callWebScriptMethod:@"call" withArguments:@[ _errorHandler, [self valueForKey:@"errorString"] ]];
}
- (void)processDidExit:(int)rc
{
D(DBF_HTMLOutput_JSShellCommand, bug("rc: %d\n", rc););
status = rc;
[self increaseCompleteCounter];
}
- (void)cancelCommand
{
D(DBF_HTMLOutput_JSShellCommand, bug("%p\n", process););
D(DBF_HTMLOutput_JSShellCommand, bug("%p\n", &process););
self.outputHandler = nil;
self.errorHandler = nil;
self.exitHandler = nil;
self.onreadoutput = nil;
self.onreaderror = nil;
self.exitHandler = nil;
[self closeInput];
if(process)
{
[self closeInput];
delete outputReader;
outputReader = NULL;
delete errorReader;
errorReader = NULL;
oak::kill_process_group_in_background(process->process_id);
delete process;
process = NULL;
}
kill(process.pid, SIGINT);
}
- (void)writeToInput:(NSString*)someData
{
D(DBF_HTMLOutput_JSShellCommand, bug("%zu bytes\n", strlen([someData UTF8String])););
ASSERT(process);
char const* bytes = [someData UTF8String];
write(process->input_fd, bytes, strlen(bytes));
if(process.in != -1)
{
char const* bytes = [someData UTF8String];
write(process.in, bytes, strlen(bytes));
}
}
- (void)closeInput
{
if(didCloseInput)
return;
D(DBF_HTMLOutput_JSShellCommand, bug("\n"););
ASSERT(process);
close(process->input_fd);
didCloseInput = true;
}
- (void)launchAndWait:(BOOL)shouldWait
{
struct process_t : oak::process_t
if(process.in != -1)
{
WATCH_LEAKS(process_t);
HOJSShellCommand* self;
process_t (HOJSShellCommand* self) : self(self) { }
void did_exit (int rc)
{
oak::process_t::did_exit(rc);
[self processDidExit:rc];
}
};
struct reader_t : io::reader_t
{
WATCH_LEAKS(reader_t);
bool regularOutput;
HOJSShellCommand* self;
reader_t (int fd, bool regularOutput, HOJSShellCommand* self) : io::reader_t(fd), regularOutput(regularOutput), self(self) { }
void receive_data (char const* bytes, size_t len)
{
if(regularOutput)
[self outputDataReceived:bytes length:len];
else [self errorDataReceived:bytes length:len];
}
};
process = new process_t(self);
process->command = command;
process->environment = environment;
process->launch();
outputReader = new reader_t(process->output_fd, true, self);
errorReader = new reader_t(process->error_fd, false, self);
while(shouldWait)
{
[self closeInput];
runLoop.set_timeout(15);
if(runLoop.start())
break;
fprintf(stderr, "*** Shell command still running after 30 seconds: %s\n", command.c_str());
fprintf(stderr, "*** Completion counter %zu, running %s, %s %d\n", completeCounter, process->is_running ? "YES" : "NO", process->is_running ? "pid" : "rc", process->is_running ? process->process_id : status);
fprintf(stderr, "*** stdout (%d): %.*s\n", process->output_fd, (int)outputData.size(), &outputData[0]);
fprintf(stderr, "*** stderr (%d): %.*s\n", process->error_fd, (int)errorData.size(), &errorData[0]);
int choice = NSRunAlertPanel(@"JavaScript Warning", @"The command %@ has been running for 15 seconds. Would you like to stop it?", @"Stop Command", @"Cancel", nil, [NSString stringWithCxxString:command]);
if(choice == NSAlertDefaultReturn) // "Stop Command"
{
[self cancelCommand];
break;
}
close(process.in);
process.in = -1;
}
}
+ (HOJSShellCommand*)runShellCommand:(NSString*)aCommand withEnvironment:(const std::map<std::string, std::string>&)someEnvironment andExitHandler:(id)aHandler
{
D(DBF_HTMLOutput_JSShellCommand, bug("%s (handler: %s)\n", [aCommand UTF8String], [[aHandler description] UTF8String]););
HOJSShellCommand* res = [[self alloc] initWithCommand:aCommand andEnvironment:someEnvironment];
res.exitHandler = aHandler;
[res launchAndWait:aHandler == nil];
return res;
}
- (void)dealloc
{
D(DBF_HTMLOutput_JSShellCommand, bug("\n"););
@@ -387,26 +327,21 @@ OAK_DEBUG_VAR(HTMLOutput_JSShellCommand);
return @"undefined";
}
- (NSString*)outputString { return outputData.empty() ? @"" : [NSString stringWithUTF8String:&outputData[0] length:utf8::find_safe_end(outputData.begin(), outputData.end()) - outputData.begin()]; }
- (NSString*)errorString { return errorData.empty() ? @"" : [NSString stringWithUTF8String:&errorData[0] length:utf8::find_safe_end(errorData.begin(), errorData.end()) - errorData.begin()]; }
- (void)setOutputString { ASSERT(false); }
- (void)setErrorString { ASSERT(false); }
- (id)onreadOutput { return self.outputHandler; }
- (id)onreadError { return self.errorHandler; }
- (NSString*)outputString { return output.empty() ? @"" : [NSString stringWithUTF8String:output.data() length:utf8::find_safe_end(output.begin(), output.end()) - output.begin()]; }
- (NSString*)errorString { return error.empty() ? @"" : [NSString stringWithUTF8String:error.data() length:utf8::find_safe_end(error.begin(), error.end()) - error.begin()]; }
- (void)setOnreadoutput:(id)aHandler
{
D(DBF_HTMLOutput_JSShellCommand, bug("%s\n", [[aHandler description] UTF8String]););
self.outputHandler = aHandler;
[_outputHandler callWebScriptMethod:@"call" withArguments:@[ _outputHandler, [self outputString] ]];
if(_onreadoutput = aHandler)
[_onreadoutput callWebScriptMethod:@"call" withArguments:@[ _onreadoutput, [self outputString] ]];
}
- (void)setOnreaderror:(id)aHandler
{
D(DBF_HTMLOutput_JSShellCommand, bug("%s\n", [[aHandler description] UTF8String]););
self.errorHandler = aHandler;
[_errorHandler callWebScriptMethod:@"call" withArguments:@[ _errorHandler, [self errorString] ]];
if(_onreaderror = aHandler)
[_onreaderror callWebScriptMethod:@"call" withArguments:@[ _onreaderror, [self errorString] ]];
}
- (void)finalizeForWebScript