mirror of
https://github.com/textmate/textmate.git
synced 2026-04-28 03:00:34 -04:00
Add new OakCommand framework
A lot of the code is copy/pasted from the document::run implementation, our command::runner and HTMLOutput (custom URL protocol).
This commit is contained in:
18
Frameworks/OakCommand/src/OakCommand.h
Normal file
18
Frameworks/OakCommand/src/OakCommand.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#include <command/parser.h>
|
||||
|
||||
extern NSString* const OakCommandDidTerminateNotification;
|
||||
extern NSString* const OakCommandErrorDomain;
|
||||
|
||||
NS_ENUM(NSInteger) {
|
||||
OakCommandRequirementsMissingError,
|
||||
OakCommandAbnormalTerminationError
|
||||
};
|
||||
|
||||
PUBLIC @interface OakCommand : NSObject
|
||||
@property (nonatomic, weak) NSResponder* firstResponder;
|
||||
@property (nonatomic, getter = isAsyncCommand, readonly) BOOL asyncCommand;
|
||||
@property (nonatomic, readonly) NSUUID* identifier;
|
||||
- (instancetype)initWithBundleCommand:(bundle_command_t const&)aCommand;
|
||||
- (void)executeWithInput:(NSFileHandle*)fileHandleForReading variables:(std::map<std::string, std::string> const&)someVariables completionHandler:(void(^)(std::string const& out, output::type placement, output_format::type format, output_caret::type outputCaret, std::map<std::string, std::string> const& environment))handler;
|
||||
- (void)waitUntilExit;
|
||||
@end
|
||||
715
Frameworks/OakCommand/src/OakCommand.mm
Normal file
715
Frameworks/OakCommand/src/OakCommand.mm
Normal file
@@ -0,0 +1,715 @@
|
||||
#import "OakCommand.h"
|
||||
#import <document/collection.h> // document::show()
|
||||
#import <document/document.h>
|
||||
#import <oak/datatypes.h>
|
||||
#import <cf/cf.h>
|
||||
#import <ns/ns.h>
|
||||
#import <io/environment.h>
|
||||
#import <io/pipe.h>
|
||||
#import <text/tokenize.h>
|
||||
#import <text/trim.h>
|
||||
#import <command/runner.h> // bundle_command_t, fix_shebang
|
||||
#import <bundles/wrappers.h>
|
||||
#import <regexp/format_string.h>
|
||||
#import <OakAppKit/OakToolTip.h>
|
||||
#import <HTMLOutput/HTMLOutput.h>
|
||||
#import <HTMLOutputWindow/HTMLOutputWindow.h>
|
||||
#import <OakSystem/process.h>
|
||||
#import <settings/settings.h>
|
||||
#import <BundleEditor/BundleEditor.h>
|
||||
|
||||
NSString* const OakCommandDidTerminateNotification = @"OakCommandDidTerminateNotification";
|
||||
NSString* const OakCommandErrorDomain = @"com.macromates.TextMate.ErrorDomain";
|
||||
|
||||
static NSString* const kOakFileHandleURLScheme = @"x-txmt-filehandle";
|
||||
|
||||
@protocol OakCommandDelegate
|
||||
- (void)updateEnvironment:(std::map<std::string, std::string>&)res forCommand:(OakCommand*)aCommand;
|
||||
- (void)saveAllEditedDocuments:(BOOL)includeAllFlag completionHandler:(void(^)(BOOL didSave))callback;
|
||||
|
||||
- (OakHTMLOutputView*)htmlOutputView:(BOOL)createFlag forIdentifier:(NSUUID*)identifier;
|
||||
- (void)discardHTMLOutputView:(OakHTMLOutputView*)htmlOutputView;
|
||||
|
||||
- (void)showToolTip:(NSString*)aToolTip;
|
||||
- (void)showDocument:(document::document_ptr)aDocument;
|
||||
|
||||
// Missing requirements and execution failure.
|
||||
- (BOOL)presentError:(NSError*)anError;
|
||||
@end
|
||||
|
||||
static std::tuple<pid_t, int, int> my_fork (char const* cmd, int inputRead, std::map<std::string, std::string> const& environment, char const* workingDir)
|
||||
{
|
||||
for(auto const& pair : environment)
|
||||
{
|
||||
if(pair.first.size() + pair.second.size() + 2 < ARG_MAX)
|
||||
continue;
|
||||
|
||||
std::map<std::string, std::string> newEnv;
|
||||
for(auto const& pair : environment)
|
||||
{
|
||||
if(pair.first.size() + pair.second.size() + 2 < ARG_MAX)
|
||||
{
|
||||
newEnv.insert(pair);
|
||||
}
|
||||
else
|
||||
{
|
||||
newEnv.emplace(pair.first, "(truncated)");
|
||||
fprintf(stderr, "*** variable exceeds ARG_MAX: %s\n", pair.first.c_str());
|
||||
}
|
||||
}
|
||||
return my_fork(cmd, inputRead, newEnv, workingDir);
|
||||
}
|
||||
|
||||
int outputRead, outputWrite, errorRead, errorWrite;
|
||||
std::tie(outputRead, outputWrite) = io::create_pipe();
|
||||
std::tie(errorRead, errorWrite) = io::create_pipe();
|
||||
|
||||
oak::c_array env(environment);
|
||||
|
||||
pid_t pid = vfork();
|
||||
if(pid == 0)
|
||||
{
|
||||
int const signals[] = { SIGINT, SIGTERM, SIGPIPE, SIGUSR1 };
|
||||
for(int sig : signals) signal(sig, SIG_DFL);
|
||||
|
||||
int const oldOutErr[] = { STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO };
|
||||
int const newOutErr[] = { inputRead, outputWrite, errorWrite };
|
||||
|
||||
for(int fd = getdtablesize(); --fd > STDERR_FILENO; )
|
||||
{
|
||||
int flags = fcntl(fd, F_GETFD);
|
||||
if((flags == -1 && errno == EBADF) || (flags & FD_CLOEXEC) == FD_CLOEXEC)
|
||||
continue;
|
||||
|
||||
if(close(fd) == -1)
|
||||
{
|
||||
perror("close");
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
}
|
||||
|
||||
for(int fd : oldOutErr) close(fd);
|
||||
for(int fd : newOutErr) dup(fd);
|
||||
|
||||
setpgid(0, getpid());
|
||||
chdir(workingDir);
|
||||
|
||||
char* argv[] = { (char*)cmd, NULL };
|
||||
execve(argv[0], argv, env);
|
||||
perror("execve");
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
int const fds[] = { inputRead, outputWrite, errorWrite };
|
||||
for(int fd : fds) close(fd);
|
||||
|
||||
return { pid, outputRead, errorRead };
|
||||
}
|
||||
|
||||
static void exhaust_fd_in_queue (dispatch_group_t group, int fd, CFRunLoopRef runLoop, void(^handler)(char const* bytes, size_t len))
|
||||
{
|
||||
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
char buf[8192];
|
||||
ssize_t len = 0;
|
||||
while((len = read(fd, buf, sizeof(buf))) > 0)
|
||||
{
|
||||
dispatch_semaphore_t sem = dispatch_semaphore_create(0);
|
||||
char const* bytes = buf;
|
||||
CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, ^{
|
||||
handler(bytes, len);
|
||||
dispatch_semaphore_signal(sem);
|
||||
});
|
||||
CFRunLoopWakeUp(runLoop);
|
||||
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
|
||||
}
|
||||
if(len == -1)
|
||||
perror("OakCommand: read");
|
||||
close(fd);
|
||||
});
|
||||
}
|
||||
|
||||
static pid_t run_command (dispatch_group_t rootGroup, std::string const& cmd, int inputFd, std::map<std::string, std::string> const& env, std::string const& cwd, CFRunLoopRef runLoop, void(^stdoutHandler)(char const* bytes, size_t len), void(^stderrHandler)(char const* bytes, size_t len), void(^completionHandler)(int status))
|
||||
{
|
||||
pid_t pid;
|
||||
int outputFd, errorFd;
|
||||
std::tie(pid, outputFd, errorFd) = my_fork(cmd.c_str(), inputFd, env, cwd.c_str());
|
||||
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
exhaust_fd_in_queue(group, outputFd, runLoop, stdoutHandler);
|
||||
exhaust_fd_in_queue(group, errorFd, runLoop, stderrHandler);
|
||||
|
||||
__block int status = 0;
|
||||
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
if(waitpid(pid, &status, 0) != pid)
|
||||
perror("OakCommand: waitpid");
|
||||
});
|
||||
|
||||
dispatch_group_enter(rootGroup);
|
||||
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, ^{
|
||||
completionHandler(status);
|
||||
});
|
||||
CFRunLoopWakeUp(runLoop);
|
||||
dispatch_group_leave(rootGroup);
|
||||
});
|
||||
|
||||
return pid;
|
||||
}
|
||||
|
||||
@interface OakCommand ()
|
||||
{
|
||||
bundle_command_t _bundleCommand;
|
||||
|
||||
dispatch_group_t _dispatchGroup;
|
||||
std::map<std::string, std::string> _environment;
|
||||
pid_t _processIdentifier;
|
||||
|
||||
BOOL _didCheckRequirements;
|
||||
BOOL _didSaveChanges;
|
||||
BOOL _didFindHTMLOutputView;
|
||||
|
||||
OakHTMLOutputView* _htmlOutputView;
|
||||
NSMutableURLRequest* _urlRequest;
|
||||
NSFileHandle* _fileHandleForWritingHTML;
|
||||
dispatch_queue_t _queueForWritingHTML;
|
||||
HTMLOutputWindowController* _htmlOutputWindowController;
|
||||
|
||||
BOOL _userDidAbort;
|
||||
}
|
||||
@end
|
||||
|
||||
@implementation OakCommand
|
||||
- (instancetype)initWithBundleCommand:(bundle_command_t const&)aCommand
|
||||
{
|
||||
if(self = [super init])
|
||||
{
|
||||
_bundleCommand = aCommand;
|
||||
command::fix_shebang(&_bundleCommand.command);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)isAsyncCommand
|
||||
{
|
||||
return _bundleCommand.output == output::new_window && _bundleCommand.output_format == output_format::html;
|
||||
}
|
||||
|
||||
- (NSUUID*)identifier
|
||||
{
|
||||
return _bundleCommand.uuid ? [[NSUUID alloc] initWithUUIDString:to_ns(_bundleCommand.uuid)] : nil;
|
||||
}
|
||||
|
||||
- (void)writeHTMLOutput:(char const*)bytes length:(size_t)len
|
||||
{
|
||||
if(!_htmlOutputView)
|
||||
_htmlOutputView = [self htmlOutputView:YES forIdentifier:self.identifier];
|
||||
|
||||
if(!_fileHandleForWritingHTML)
|
||||
{
|
||||
NSPipe* pipe = [NSPipe pipe];
|
||||
_fileHandleForWritingHTML = pipe.fileHandleForWriting;
|
||||
_queueForWritingHTML = dispatch_queue_create("org.textmate.write-html", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
static NSInteger UniqueKey = 0; // Make each URL unique to avoid caching
|
||||
|
||||
_urlRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@://job/%@/%ld", kOakFileHandleURLScheme, [to_ns(_bundleCommand.name) stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding], ++UniqueKey]] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:6000];
|
||||
[NSURLProtocol setProperty:self.identifier forKey:@"commandIdentifier" inRequest:_urlRequest];
|
||||
[NSURLProtocol setProperty:pipe.fileHandleForReading forKey:@"fileHandle" inRequest:_urlRequest];
|
||||
[NSURLProtocol setProperty:@(_processIdentifier) forKey:@"processIdentifier" inRequest:_urlRequest];
|
||||
[NSURLProtocol setProperty:to_ns(_bundleCommand.name) forKey:@"processName" inRequest:_urlRequest];
|
||||
[NSURLProtocol setProperty:self forKey:@"command" inRequest:_urlRequest];
|
||||
|
||||
[_htmlOutputView loadRequest:_urlRequest environment:_environment autoScrolls:_bundleCommand.auto_scroll_output];
|
||||
}
|
||||
|
||||
NSData* data = [NSData dataWithBytes:bytes length:len];
|
||||
NSFileHandle* fh = _fileHandleForWritingHTML;
|
||||
dispatch_async(_queueForWritingHTML, ^{
|
||||
ssize_t bytesWritten = write(fh.fileDescriptor, [data bytes], [data length]);
|
||||
if(bytesWritten == -1)
|
||||
perror("HTMLOutput: write");
|
||||
});
|
||||
}
|
||||
|
||||
- (void)executeWithInput:(NSFileHandle*)fileHandleForReading variables:(std::map<std::string, std::string> const&)someVariables completionHandler:(void(^)(std::string const& out, output::type placement, output_format::type format, output_caret::type outputCaret, std::map<std::string, std::string> const& environment))handler
|
||||
{
|
||||
_dispatchGroup = dispatch_group_create();
|
||||
_processIdentifier = 0;
|
||||
_didCheckRequirements = _didSaveChanges = _didFindHTMLOutputView = NO;
|
||||
|
||||
_environment = someVariables;
|
||||
_environment << oak::basic_environment();
|
||||
[self updateEnvironment:_environment];
|
||||
|
||||
[self executeWithInput:(fileHandleForReading ?: [[NSFileHandle alloc] initWithFileDescriptor:open("/dev/null", O_RDONLY|O_CLOEXEC) closeOnDealloc:YES]) completionHandler:handler];
|
||||
}
|
||||
|
||||
- (void)executeWithInput:(NSFileHandle*)inputFH completionHandler:(void(^)(std::string const& out, output::type placement, output_format::type format, output_caret::type outputCaret, std::map<std::string, std::string> const& environment))handler
|
||||
{
|
||||
if(_didCheckRequirements == NO)
|
||||
{
|
||||
bundles::required_command_t failedRequirement;
|
||||
bundles::item_ptr item = bundles::lookup(_bundleCommand.uuid);
|
||||
if(item && bundles::missing_requirement(item, _environment, &failedRequirement))
|
||||
{
|
||||
std::vector<std::string> paths;
|
||||
std::string const tmp = _environment["PATH"];
|
||||
for(auto path : text::tokenize(tmp.begin(), tmp.end(), ':'))
|
||||
{
|
||||
if(path != "" && path::is_directory(path))
|
||||
paths.push_back(path::with_tilde(path));
|
||||
}
|
||||
|
||||
std::string message;
|
||||
if(failedRequirement.variable != NULL_STR)
|
||||
message = text::format("This command requires ‘%1$s’ which wasn’t found on your system.\n\nThe following locations were searched:%2$s\n\nIf ‘%1$s’ is installed elsewhere then you need to set %3$s in Preferences → Variables to the full path of where you installed it.", failedRequirement.command.c_str(), ("\n\u2003• " + text::join(paths, "\n\u2003• ")).c_str(), failedRequirement.variable.c_str());
|
||||
else message = text::format("This command requires ‘%1$s’ which wasn’t found on your system.\n\nThe following locations were searched:%2$s\n\nIf ‘%1$s’ is installed elsewhere then you need to set PATH in Preferences → Variables to include the folder in which it can be found.", failedRequirement.command.c_str(), ("\n\u2003• " + text::join(paths, "\n\u2003• ")).c_str());
|
||||
|
||||
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithDictionary:@{
|
||||
NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Unable to run “%.*s”.", (int)_bundleCommand.name.size(), _bundleCommand.name.data()],
|
||||
NSLocalizedRecoverySuggestionErrorKey : to_ns(message),
|
||||
}];
|
||||
|
||||
if(failedRequirement.more_info_url != NULL_STR)
|
||||
{
|
||||
dict[@"moreInfoURL"] = [NSURL URLWithString:to_ns(failedRequirement.more_info_url)];
|
||||
dict[NSLocalizedRecoveryOptionsErrorKey] = @[ @"OK", @"More Info…" ];
|
||||
dict[NSRecoveryAttempterErrorKey] = self;
|
||||
}
|
||||
|
||||
NSError* error = [NSError errorWithDomain:OakCommandErrorDomain code:OakCommandRequirementsMissingError userInfo:dict];
|
||||
[self presentError:error];
|
||||
|
||||
return;
|
||||
}
|
||||
_didCheckRequirements = YES;
|
||||
}
|
||||
|
||||
if(_didSaveChanges == NO)
|
||||
{
|
||||
if(_bundleCommand.pre_exec != pre_exec::nop)
|
||||
{
|
||||
[self saveAllEditedDocuments:(_bundleCommand.pre_exec == pre_exec::save_project) completionHandler:^(BOOL didSave){
|
||||
if(didSave)
|
||||
{
|
||||
_didSaveChanges = YES;
|
||||
[self executeWithInput:inputFH completionHandler:handler];
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
_didSaveChanges = YES;
|
||||
}
|
||||
|
||||
bool hasHTMLOutput = _bundleCommand.output == output::new_window && _bundleCommand.output_format == output_format::html;
|
||||
if(_didFindHTMLOutputView == NO)
|
||||
{
|
||||
if(hasHTMLOutput && (_bundleCommand.output_reuse == output_reuse::reuse_busy || _bundleCommand.output_reuse == output_reuse::abort_and_reuse_busy))
|
||||
{
|
||||
_htmlOutputView = [self htmlOutputView:NO forIdentifier:self.identifier];
|
||||
if(_htmlOutputView && _htmlOutputView.isRunningCommand)
|
||||
{
|
||||
BOOL askUser = _bundleCommand.output_reuse != output_reuse::abort_and_reuse_busy;
|
||||
[_htmlOutputView stopLoadingWithUserInteraction:askUser completionHandler:^(BOOL didStop){
|
||||
if(didStop)
|
||||
{
|
||||
_didFindHTMLOutputView = YES;
|
||||
[self executeWithInput:inputFH completionHandler:handler];
|
||||
}
|
||||
}];
|
||||
return;
|
||||
}
|
||||
}
|
||||
_didFindHTMLOutputView = YES;
|
||||
}
|
||||
|
||||
__block std::string out, err;
|
||||
auto stdoutHandler = ^(char const* bytes, size_t len) { out.insert(out.end(), bytes, bytes + len); };
|
||||
auto stderrHandler = ^(char const* bytes, size_t len) { err.insert(err.end(), bytes, bytes + len); };
|
||||
auto htmlOutHandler = ^(char const* bytes, size_t len) { [self writeHTMLOutput:bytes length:len]; };
|
||||
|
||||
std::string const directory = format_string::expand("${TM_DIRECTORY:-${TM_PROJECT_DIRECTORY:-$TMPDIR}}", _environment);
|
||||
std::string scriptPath = path::temp("command", _bundleCommand.command);
|
||||
ASSERT(scriptPath != NULL_STR);
|
||||
|
||||
_processIdentifier = run_command(_dispatchGroup, scriptPath, inputFH.fileDescriptor, _environment, directory, CFRunLoopGetCurrent(), hasHTMLOutput ? htmlOutHandler : stdoutHandler, stderrHandler, ^(int status) {
|
||||
_processIdentifier = 0;
|
||||
unlink(scriptPath.c_str());
|
||||
|
||||
std::string newOut, newErr;
|
||||
oak::replace_copy(out.begin(), out.end(), scriptPath.begin(), scriptPath.end(), _bundleCommand.name.begin(), _bundleCommand.name.end(), back_inserter(newOut));
|
||||
oak::replace_copy(err.begin(), err.end(), scriptPath.begin(), scriptPath.end(), _bundleCommand.name.begin(), _bundleCommand.name.end(), back_inserter(newErr));
|
||||
newOut.swap(out);
|
||||
newErr.swap(err);
|
||||
|
||||
if(WIFSIGNALED(status))
|
||||
fprintf(stderr, "*** process terminated: %s\n", strsignal(WTERMSIG(status)));
|
||||
else if(!WIFEXITED(status))
|
||||
fprintf(stderr, "*** process terminated abnormally %d\n", status);
|
||||
|
||||
output::type placement = _bundleCommand.output;
|
||||
output_format::type format = _bundleCommand.output_format;
|
||||
output_caret::type outputCaret = _bundleCommand.output_caret;
|
||||
|
||||
int rc = WIFEXITED(status) ? WEXITSTATUS(status) : (WIFSIGNALED(status) ? 0 : -1);
|
||||
enum { exit_discard = 200, exit_replace_text, exit_replace_document, exit_insert_text, exit_insert_snippet, exit_show_html, exit_show_tool_tip, exit_create_new_document };
|
||||
switch(rc)
|
||||
{
|
||||
case exit_discard: placement = output::discard; break;
|
||||
case exit_replace_text: placement = output::replace_input; format = output_format::text; outputCaret = output_caret::heuristic; break;
|
||||
case exit_replace_document: placement = output::replace_document; format = output_format::text; outputCaret = output_caret::interpolate_by_line; break;
|
||||
case exit_insert_text: placement = output::after_input; format = output_format::text; outputCaret = output_caret::after_output; break;
|
||||
case exit_show_html: placement = output::new_window; format = output_format::html; break;
|
||||
case exit_show_tool_tip: placement = output::tool_tip; format = output_format::text; break;
|
||||
case exit_create_new_document: placement = output::new_window; format = output_format::text; break;
|
||||
case exit_insert_snippet:
|
||||
{
|
||||
format = output_format::snippet;
|
||||
if(_bundleCommand.input == input::selection)
|
||||
placement = output::replace_input;
|
||||
else if(_bundleCommand.input == input::entire_document)
|
||||
placement = output::at_caret;
|
||||
else
|
||||
placement = output::after_input;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
BOOL discardHTML = NO;
|
||||
if(rc != 0 && !_userDidAbort && !(200 <= rc && rc <= 207))
|
||||
{
|
||||
NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithDictionary:@{
|
||||
NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Failure running “%@”.", to_ns(_bundleCommand.name)],
|
||||
NSLocalizedRecoverySuggestionErrorKey : to_ns(text::trim(err + out).empty() ? text::format("Command returned status code %d.", rc) : err + out),
|
||||
}];
|
||||
|
||||
if(bundles::lookup(_bundleCommand.uuid))
|
||||
{
|
||||
dict[NSLocalizedRecoveryOptionsErrorKey] = @[ @"OK", @"Edit Command" ];
|
||||
dict[NSRecoveryAttempterErrorKey] = self;
|
||||
}
|
||||
|
||||
NSError* error = [NSError errorWithDomain:OakCommandErrorDomain code:OakCommandAbnormalTerminationError userInfo:dict];
|
||||
[self presentError:error];
|
||||
}
|
||||
else if(placement == output::new_window)
|
||||
{
|
||||
if(format == output_format::text)
|
||||
{
|
||||
[self showDocument:document::from_content(err + out)];
|
||||
}
|
||||
else if(format == output_format::html)
|
||||
{
|
||||
if(!hasHTMLOutput)
|
||||
[self writeHTMLOutput:out.data() length:out.size()];
|
||||
|
||||
if(!err.empty())
|
||||
[self writeHTMLOutput:err.data() length:err.size()];
|
||||
}
|
||||
}
|
||||
else if(placement == output::tool_tip)
|
||||
{
|
||||
std::string str = err + out;
|
||||
auto len = str.find_last_not_of(" \t\n");
|
||||
if(len != std::string::npos)
|
||||
[self showToolTip:to_ns(str.substr(0, len+1))];
|
||||
}
|
||||
else if(placement != output::discard)
|
||||
{
|
||||
if(format == output_format::snippet && _bundleCommand.disable_output_auto_indent)
|
||||
format = output_format::snippet_no_auto_indent;
|
||||
|
||||
if(handler)
|
||||
handler(out, placement, format, outputCaret, _environment);
|
||||
else if(out.size() || err.size())
|
||||
[self showDocument:document::from_content(err + out)];
|
||||
}
|
||||
else if(_htmlOutputView)
|
||||
{
|
||||
discardHTML = YES;
|
||||
}
|
||||
|
||||
if(NSFileHandle* fh = std::exchange(_fileHandleForWritingHTML, nil))
|
||||
{
|
||||
dispatch_async(_queueForWritingHTML, ^{
|
||||
[fh closeFile];
|
||||
});
|
||||
_queueForWritingHTML = nil;
|
||||
}
|
||||
|
||||
if(NSMutableURLRequest* request = std::exchange(_urlRequest, nil))
|
||||
{
|
||||
[NSURLProtocol removePropertyForKey:@"command" inRequest:request];
|
||||
[NSURLProtocol removePropertyForKey:@"fileHandle" inRequest:request];
|
||||
[NSURLProtocol removePropertyForKey:@"processIdentifier" inRequest:request];
|
||||
}
|
||||
|
||||
if(discardHTML)
|
||||
{
|
||||
[self discardHTMLOutputView:_htmlOutputView];
|
||||
}
|
||||
|
||||
// Wake potential event loop
|
||||
[NSApp postEvent:[NSEvent otherEventWithType:NSApplicationDefined location:NSZeroPoint modifierFlags:0 timestamp:0 windowNumber:0 context:NULL subtype:0 data1:0 data2:0] atStart:NO];
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:OakCommandDidTerminateNotification object:self];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)waitUntilExit
|
||||
{
|
||||
if(self.isAsyncCommand == YES && _processIdentifier > 0)
|
||||
{
|
||||
__block BOOL shouldWait = YES;
|
||||
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
|
||||
|
||||
dispatch_group_notify(_dispatchGroup, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
shouldWait = NO;
|
||||
CFRunLoopStop(runLoop);
|
||||
});
|
||||
|
||||
while(shouldWait)
|
||||
CFRunLoopRun();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableArray* queuedEvents = [NSMutableArray array];
|
||||
while(_processIdentifier > 0)
|
||||
{
|
||||
// We use CFRunLoopRunInMode() to handle dispatch queues and nextEventMatchingMask:… to catcn ⌃C
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 5, true);
|
||||
if(NSEvent* event = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:nil inMode:NSDefaultRunLoopMode dequeue:YES])
|
||||
{
|
||||
static NSEventType const events[] = { NSLeftMouseDown, NSLeftMouseUp, NSRightMouseDown, NSRightMouseUp, NSOtherMouseDown, NSOtherMouseUp, NSLeftMouseDragged, NSRightMouseDragged, NSOtherMouseDragged, NSKeyDown, NSKeyUp, NSFlagsChanged };
|
||||
if(!oak::contains(std::begin(events), std::end(events), [event type]))
|
||||
{
|
||||
[NSApp sendEvent:event];
|
||||
}
|
||||
else if([event type] == NSKeyDown && (([[event charactersIgnoringModifiers] isEqualToString:@"c"] && ([event modifierFlags] & (NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask)) == NSControlKeyMask) || ([[event charactersIgnoringModifiers] isEqualToString:@"."] && ([event modifierFlags] & (NSShiftKeyMask|NSControlKeyMask|NSAlternateKeyMask|NSCommandKeyMask)) == NSCommandKeyMask)))
|
||||
{
|
||||
NSInteger choice = NSRunAlertPanel([NSString stringWithFormat:@"Stop “%@”", to_ns(_bundleCommand.name)], @"Would you like to kill the current shell command?", @"Kill Command", @"Cancel", nil);
|
||||
if(choice == NSAlertDefaultReturn) // "Kill Command"
|
||||
{
|
||||
_userDidAbort = YES;
|
||||
oak::kill_process_group_in_background(_processIdentifier);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
[queuedEvents addObject:event];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(NSEvent* event in queuedEvents)
|
||||
[NSApp postEvent:event atStart:NO];
|
||||
}
|
||||
|
||||
// =============================
|
||||
// = NSErrorRecoveryAttempting =
|
||||
// =============================
|
||||
|
||||
- (BOOL)attemptRecoveryFromError:(NSError*)error optionIndex:(NSUInteger)recoveryOptionIndex
|
||||
{
|
||||
BOOL didRecover = NO;
|
||||
switch(error.code)
|
||||
{
|
||||
case OakCommandRequirementsMissingError:
|
||||
{
|
||||
if(recoveryOptionIndex == 1)
|
||||
[[NSWorkspace sharedWorkspace] openURL:error.userInfo[@"moreInfoURL"]];
|
||||
}
|
||||
break;
|
||||
|
||||
case OakCommandAbnormalTerminationError:
|
||||
{
|
||||
if(recoveryOptionIndex == 1)
|
||||
{
|
||||
Class cl = NSClassFromString(@"BundleEditor");
|
||||
[[cl sharedInstance] revealBundleItem:bundles::lookup(_bundleCommand.uuid)];
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
return didRecover;
|
||||
}
|
||||
|
||||
- (void)attemptRecoveryFromError:(NSError*)error optionIndex:(NSUInteger)recoveryOptionIndex delegate:(id)delegate didRecoverSelector:(SEL)didRecoverSelector contextInfo:(void*)contextInfo
|
||||
{
|
||||
BOOL didRecover = [self attemptRecoveryFromError:error optionIndex:recoveryOptionIndex];
|
||||
if(delegate && didRecoverSelector)
|
||||
{
|
||||
auto fn = (void(*)(id, SEL, BOOL, void*))[delegate methodForSelector:didRecoverSelector];
|
||||
fn(delegate, didRecoverSelector, didRecover, contextInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// ===========================
|
||||
// = Call to first Responder =
|
||||
// ===========================
|
||||
|
||||
- (id)targetForAction:(SEL)action
|
||||
{
|
||||
NSResponder* responder = _firstResponder;
|
||||
while(responder)
|
||||
{
|
||||
if([responder respondsToSelector:action])
|
||||
return responder;
|
||||
|
||||
if(responder == NSApp.keyWindow || responder == NSApp.mainWindow || responder == NSApp)
|
||||
{
|
||||
if([[responder performSelector:@selector(delegate)] respondsToSelector:action])
|
||||
return [responder performSelector:@selector(delegate)];
|
||||
}
|
||||
|
||||
if(responder == NSApp.mainWindow)
|
||||
responder = NSApp;
|
||||
else if(responder == NSApp.keyWindow)
|
||||
responder = NSApp.mainWindow.firstResponder ?: NSApp.mainWindow;
|
||||
else
|
||||
responder = responder.nextResponder;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)updateEnvironment:(std::map<std::string, std::string>&)res
|
||||
{
|
||||
if(id target = [self targetForAction:@selector(updateEnvironment:forCommand:)])
|
||||
return [target updateEnvironment:res forCommand:self];
|
||||
res = bundles::scope_variables(res); // Bundle items with a shellVariables setting
|
||||
res = variables_for_path(res); // .tm_properties
|
||||
}
|
||||
|
||||
- (void)saveAllEditedDocuments:(BOOL)includeAllFlag completionHandler:(void(^)(BOOL didSave))callback
|
||||
{
|
||||
if(id target = [self targetForAction:_cmd])
|
||||
[target saveAllEditedDocuments:includeAllFlag completionHandler:callback];
|
||||
else if(callback)
|
||||
callback(YES);
|
||||
}
|
||||
|
||||
- (OakHTMLOutputView*)htmlOutputView:(BOOL)createFlag forIdentifier:(NSUUID*)identifier
|
||||
{
|
||||
OakHTMLOutputView* view;
|
||||
if(id target = [self targetForAction:_cmd])
|
||||
{
|
||||
view = [target htmlOutputView:createFlag forIdentifier:identifier];
|
||||
}
|
||||
else
|
||||
{
|
||||
view = _htmlOutputWindowController.htmlOutputView;
|
||||
if(view.needsNewWebView || ![view.commandIdentifier isEqual:identifier])
|
||||
view = nil;
|
||||
|
||||
if(createFlag && (!view || view.isRunningCommand))
|
||||
{
|
||||
_htmlOutputWindowController = [[HTMLOutputWindowController alloc] initWithIdentifier:identifier];
|
||||
view = _htmlOutputWindowController.htmlOutputView;
|
||||
}
|
||||
}
|
||||
|
||||
if(view)
|
||||
{
|
||||
if([view.window.delegate respondsToSelector:@selector(showWindow:)])
|
||||
[view.window.delegate performSelector:@selector(showWindow:) withObject:self];
|
||||
[view.window makeFirstResponder:view.webView];
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
- (void)discardHTMLOutputView:(OakHTMLOutputView*)htmlOutputView
|
||||
{
|
||||
if(id target = [self targetForAction:_cmd])
|
||||
[target discardHTMLOutputView:htmlOutputView];
|
||||
else if(id delegate = htmlOutputView.webView.UIDelegate)
|
||||
[delegate performSelector:@selector(webViewClose:) withObject:htmlOutputView.webView];
|
||||
}
|
||||
|
||||
- (void)showToolTip:(NSString*)aToolTip
|
||||
{
|
||||
if(id target = [self targetForAction:_cmd])
|
||||
return [target showToolTip:aToolTip];
|
||||
OakShowToolTip(aToolTip, [NSEvent mouseLocation]);
|
||||
}
|
||||
|
||||
- (void)showDocument:(document::document_ptr)aDocument
|
||||
{
|
||||
if(id target = [self targetForAction:_cmd])
|
||||
return [target showDocument:aDocument];
|
||||
document::show(aDocument, document::kCollectionAny);
|
||||
}
|
||||
|
||||
- (BOOL)presentError:(NSError*)anError
|
||||
{
|
||||
if(id target = [self targetForAction:_cmd])
|
||||
return [target presentError:anError];
|
||||
return NO;
|
||||
}
|
||||
@end
|
||||
|
||||
// =====================
|
||||
// = Custom URL Scheme =
|
||||
// =====================
|
||||
|
||||
@interface OakFileHandleURLProtocol : NSURLProtocol
|
||||
@end
|
||||
|
||||
@implementation OakFileHandleURLProtocol
|
||||
+ (void)load
|
||||
{
|
||||
[self registerClass:self];
|
||||
[WebView registerURLSchemeAsLocal:kOakFileHandleURLScheme];
|
||||
}
|
||||
|
||||
+ (BOOL)canInitWithRequest:(NSURLRequest*)request { return [request.URL.scheme isEqualToString:kOakFileHandleURLScheme]; }
|
||||
+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request { return request; }
|
||||
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest*)a toRequest:(NSURLRequest*)b { return NO; }
|
||||
|
||||
// =============================================
|
||||
// = These methods might be called in a thread =
|
||||
// =============================================
|
||||
|
||||
- (void)startLoading
|
||||
{
|
||||
NSFileHandle* fileHandle = [NSURLProtocol propertyForKey:@"fileHandle" inRequest:self.request];
|
||||
if(!fileHandle)
|
||||
{
|
||||
NSURLResponse* response = [[NSHTTPURLResponse alloc] initWithURL:self.request.URL statusCode:404 HTTPVersion:@"HTTP/1.1" headerFields:nil];
|
||||
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
|
||||
[self.client URLProtocolDidFinishLoading:self];
|
||||
NSLog(@"No command output for ‘%@’", self.request.URL);
|
||||
return;
|
||||
}
|
||||
|
||||
NSURLResponse* response = [[NSURLResponse alloc] initWithURL:self.request.URL MIMEType:@"text/html" expectedContentLength:-1 textEncodingName:@"utf-8"];
|
||||
[self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
|
||||
|
||||
// WebView seems to stall until it has received at least 1024 bytes
|
||||
static std::string const dummy("<!--" + std::string(1017, ' ') + "-->");
|
||||
[self.client URLProtocol:self didLoadData:[NSData dataWithBytes:dummy.data() length:dummy.size()]];
|
||||
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
int len;
|
||||
char buf[8192];
|
||||
while((len = read(fileHandle.fileDescriptor, buf, sizeof(buf))) > 0)
|
||||
{
|
||||
NSData* data = [NSData dataWithBytesNoCopy:buf length:len freeWhenDone:NO];
|
||||
[self.client URLProtocol:self didLoadData:data];
|
||||
}
|
||||
|
||||
if(len == -1)
|
||||
perror("HTMLOutput: read");
|
||||
|
||||
[fileHandle closeFile];
|
||||
[self.client URLProtocolDidFinishLoading:self];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)stopLoading
|
||||
{
|
||||
[[NSURLProtocol propertyForKey:@"fileHandle" inRequest:self.request] closeFile];
|
||||
if(pid_t pid = [[NSURLProtocol propertyForKey:@"processIdentifier" inRequest:self.request] intValue])
|
||||
oak::kill_process_group_in_background(pid);
|
||||
}
|
||||
@end
|
||||
3
Frameworks/OakCommand/target
Normal file
3
Frameworks/OakCommand/target
Normal file
@@ -0,0 +1,3 @@
|
||||
SOURCES = src/*.mm
|
||||
LINK += HTMLOutput HTMLOutputWindow OakAppKit OakSystem bundles cf command document io ns regexp settings text
|
||||
EXPORT = src/*.h
|
||||
Reference in New Issue
Block a user