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:
Allan Odgaard
2016-08-25 14:20:00 +02:00
parent 974db67258
commit f3745fe034
3 changed files with 736 additions and 0 deletions

View 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

View 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 wasnt 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 wasnt 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

View 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