mirror of
https://github.com/textmate/textmate.git
synced 2026-04-28 03:00:34 -04:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user