macOS support fixes

* Adding macOS FileSystemWatcher, this should allow files to be monitored and update like the other OSs
* Reworked how macOS launches processes to use NSTask which just deals with escaped and unescaped paths better
* Made a change to the ScriptsBuilder::RunBuildTool, this was adding the escaped values to the path, in reality it should be up to the underlying OS to make sure things are properly escaped, so removed those as they just end up causing issues. Also instead of appending the args to the fileName we just properly use the Arguments variable on the CreateProcessSettings
* No longer use open in order to show files in the finder, we use the proper method selectFile
* made a slight cleanup change to the MacPlatform Tick function
* Added ToNSString functions just to make that easier
* Added a ParseArguments function that will take a string and turn it into an array for NSTask
This commit is contained in:
Andrew Spiering
2023-09-19 20:58:12 -07:00
parent 92733d1a69
commit f40657ea04
9 changed files with 268 additions and 46 deletions

View File

@@ -70,6 +70,46 @@ CFStringRef AppleUtils::ToString(const StringView& str)
return CFStringCreateWithBytes(nullptr, (const UInt8*)str.GetText(), str.Length() * sizeof(Char), kCFStringEncodingUTF16LE, false);
}
NSString* AppleUtils::ToNSString(const StringView& str)
{
NSString* ret = !str.IsEmpty() ? [[NSString alloc] initWithBytes: (const UInt8*)str.Get() length: str.Length() * sizeof(Char) encoding: NSUTF16LittleEndianStringEncoding] : nil;
return ret ? ret : @"";
}
NSString* AppleUtils::ToNSString(const char* string)
{
NSString* ret = string ? [NSString stringWithUTF8String: string] : nil;
return ret ? ret : @"";
}
NSArray* AppleUtils::ParseArguments(NSString* argsString) {
NSMutableArray *argsArray = [NSMutableArray array];
NSScanner *scanner = [NSScanner scannerWithString:argsString];
NSString *currentArg = nil;
BOOL insideQuotes = NO;
while (![scanner isAtEnd]) {
if (insideQuotes) {
[scanner scanUpToString:@"\"" intoString:&currentArg];
[scanner scanString:@"\"" intoString:NULL];
insideQuotes = NO;
} else {
[scanner scanUpToString:@" " intoString:&currentArg];
[scanner scanString:@" " intoString:NULL];
}
if ([currentArg isEqualToString:@"\""]) {
insideQuotes = YES;
} else if (currentArg) {
[argsArray addObject:currentArg];
}
}
return [argsArray copy];
}
typedef uint16_t offset_t;
#define align_mem_up(num, align) (((num) + ((align) - 1)) & ~((align) - 1))

View File

@@ -13,6 +13,9 @@ class AppleUtils
public:
static String ToString(CFStringRef str);
static CFStringRef ToString(const StringView& str);
static NSString* ToNSString(const StringView& str);
static NSString* ToNSString(const char* string);
static NSArray* ParseArguments(NSString* argsString);
#if PLATFORM_MAC
static Float2 PosToCoca(const Float2& pos);
static Float2 CocaToPos(const Float2& pos);

View File

@@ -6,6 +6,8 @@
#include "Windows/WindowsFileSystemWatcher.h"
#elif PLATFORM_LINUX
#include "Linux/LinuxFileSystemWatcher.h"
#elif PLATFORM_MAC
#include "Mac/MacFileSystemWatcher.h"
#else
#include "Base/FileSystemWatcherBase.h"
#endif

View File

@@ -132,7 +132,7 @@ bool MacFileSystem::ShowBrowseFolderDialog(Window* parentWindow, const StringVie
bool MacFileSystem::ShowFileExplorer(const StringView& path)
{
return Platform::StartProcess(TEXT("open"), String::Format(TEXT("\"{0}\""), path), StringView::Empty) != 0;
return [[NSWorkspace sharedWorkspace] selectFile: AppleUtils::ToNSString(FileSystem::ConvertRelativePathToAbsolute(path)) inFileViewerRootedAtPath: @""];
}
#endif

View File

@@ -0,0 +1,100 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#if PLATFORM_MAC
#include "MacFileSystemWatcher.h"
#include "Engine/Platform/Apple/AppleUtils.h"
#include "Engine/Platform/CriticalSection.h"
#include "Engine/Platform/Thread.h"
#include "Engine/Threading/ThreadSpawner.h"
#include "Engine/Core/Collections/Array.h"
#include "Engine/Core/Types/StringView.h"
void DirectoryWatchCallback( ConstFSEventStreamRef StreamRef, void* FileWatcherPtr, size_t EventCount, void* EventPaths, const FSEventStreamEventFlags EventFlags[], const FSEventStreamEventId EventIDs[] )
{
MacFileSystemWatcher* macFileSystemWatcher = (MacFileSystemWatcher*)FileWatcherPtr;
if (macFileSystemWatcher)
{
CFArrayRef EventPathArray = (CFArrayRef)EventPaths;
for( size_t EventIndex = 0; EventIndex < EventCount; ++EventIndex )
{
const FSEventStreamEventFlags Flags = EventFlags[EventIndex];
if( !(Flags & kFSEventStreamEventFlagItemIsFile) && !(Flags & kFSEventStreamEventFlagItemIsDir) )
{
// events about symlinks don't concern us
continue;
}
auto action = FileSystemAction::Unknown;
const bool added = ( Flags & kFSEventStreamEventFlagItemCreated );
const bool renamed = ( Flags & kFSEventStreamEventFlagItemRenamed );
const bool modified = ( Flags & kFSEventStreamEventFlagItemModified );
const bool removed = ( Flags & kFSEventStreamEventFlagItemRemoved );
if (added)
{
action = FileSystemAction::Create;
}
if (renamed || modified)
{
action = FileSystemAction::Delete;
}
if (removed)
{
action = FileSystemAction::Modify;
}
const String resolvedPath = AppleUtils::ToString((CFStringRef)CFArrayGetValueAtIndex(EventPathArray,EventIndex));
macFileSystemWatcher->OnEvent(resolvedPath, action);
}
}
}
MacFileSystemWatcher::MacFileSystemWatcher(const String& directory, bool withSubDirs)
: FileSystemWatcherBase(directory, withSubDirs)
{
CFStringRef FullPathMac = AppleUtils::ToString(StringView(directory));
CFArrayRef PathsToWatch = CFArrayCreate(NULL, (const void**)&FullPathMac, 1, NULL);
CFAbsoluteTime Latency = 0.2;
FSEventStreamContext Context;
Context.version = 0;
Context.info = this;
Context.retain = NULL;
Context.release = NULL;
Context.copyDescription = NULL;
EventStream = FSEventStreamCreate( NULL,
&DirectoryWatchCallback,
&Context,
PathsToWatch,
kFSEventStreamEventIdSinceNow,
Latency,
kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents
);
CFRelease(PathsToWatch);
CFRelease(FullPathMac);
FSEventStreamScheduleWithRunLoop( EventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode );
FSEventStreamStart( EventStream );
IsRunning = true;
}
MacFileSystemWatcher::~MacFileSystemWatcher()
{
if (IsRunning)
{
FSEventStreamStop(EventStream);
FSEventStreamUnscheduleFromRunLoop(EventStream, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
FSEventStreamInvalidate(EventStream);
FSEventStreamRelease(EventStream);
}
}
#endif

View File

@@ -0,0 +1,39 @@
// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
#pragma once
#if PLATFORM_MAC
#include "Engine/Platform/Base/FileSystemWatcherBase.h"
#include <CoreServices/CoreServices.h>
/// <summary>
/// Mac platform implementation of the file system watching object.
/// </summary>
class FLAXENGINE_API MacFileSystemWatcher : public FileSystemWatcherBase
{
public:
/// <summary>
/// Initializes a new instance of the <see cref="MacFileSystemWatcher"/> class.
/// </summary>
/// <param name="directory">The directory to watch.</param>
/// <param name="withSubDirs">True if monitor the directory tree rooted at the specified directory or just a given directory.</param>
MacFileSystemWatcher(const String& directory, bool withSubDirs);
/// <summary>
/// Finalizes an instance of the <see cref="MacFileSystemWatcher"/> class.
/// </summary>
~MacFileSystemWatcher();
public:
private:
FSEventStreamRef EventStream;
bool IsRunning;
};
#endif

View File

@@ -324,13 +324,16 @@ void MacPlatform::BeforeRun()
void MacPlatform::Tick()
{
// Process system events
while (true)
NSEvent* event = nil;
do
{
NSEvent* event = [NSApp nextEventMatchingMask:NSEventMaskAny untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES];
if (event == nil)
break;
[NSApp sendEvent:event];
}
NSEvent* event = [NSApp nextEventMatchingMask: NSEventMaskAny untilDate: nil inMode: NSDefaultRunLoopMode dequeue: YES];
if (event)
{
[NSApp sendEvent:event];
}
} while(event);
ApplePlatform::Tick();
}
@@ -461,50 +464,88 @@ int32 MacPlatform::CreateProcess(CreateProcessSettings& settings)
}
}
}
// Sanatize the string if the exePath has spaces with properly espcaped spaces for popen
exePath.Replace(TEXT(" "), TEXT("\\ "));
const String cmdLine = exePath + TEXT(" ") + settings.Arguments;
const StringAsANSI<> cmdLineAnsi(*cmdLine, cmdLine.Length());
FILE* pipe = popen(cmdLineAnsi.Get(), "r");
NSTask *task = [[NSTask alloc] init];
task.launchPath = AppleUtils::ToNSString(exePath);
task.arguments = AppleUtils::ParseArguments(AppleUtils::ToNSString(settings.Arguments));
if (cwd.Length() != 0)
{
Platform::SetWorkingDirectory(cwd);
}
if (!pipe)
{
LOG(Warning, "Failed to start process, errno={}", errno);
return -1;
}
// TODO: environment
task.currentDirectoryPath = AppleUtils::ToNSString(cwd);
int32 returnCode = 0;
if (settings.WaitForEnd)
{
id<NSObject> outputObserver = nil;
if (captureStdOut)
{
char lineBuffer[1024];
while (fgets(lineBuffer, sizeof(lineBuffer), pipe) != NULL)
NSPipe *stdoutPipe = [NSPipe pipe];
[task setStandardOutput:stdoutPipe];
outputObserver = [[NSNotificationCenter defaultCenter]
addObserverForName: NSFileHandleDataAvailableNotification
object: [stdoutPipe fileHandleForReading]
queue: nil
usingBlock:^(NSNotification* notification)
{
char* p = lineBuffer + strlen(lineBuffer) - 1;
if (*p == '\n') *p = 0;
String line(lineBuffer);
if (settings.SaveOutput)
settings.Output.Add(line.Get(), line.Length());
if (settings.LogOutput)
Log::Logger::Write(LogType::Info, line);
NSData* data = [stdoutPipe fileHandleForReading].availableData;
if (data.length)
{
String line((char*)data.bytes);
if (settings.SaveOutput)
settings.Output.Add(line.Get(), line.Length());
if (settings.LogOutput)
Log::Logger::Write(LogType::Info, line);
[[stdoutPipe fileHandleForReading] waitForDataInBackgroundAndNotify];
}
}
];
[[stdoutPipe fileHandleForReading] waitForDataInBackgroundAndNotify];
}
else
String exception;
@try
{
while (!feof(pipe))
{
sleep(1);
}
[task launch];
[task waitUntilExit];
}
@catch (NSException* e)
{
exception = e.reason.UTF8String;
}
if (!exception.IsEmpty())
{
LOG(Error, "Failed to run command {0} {1} with error {2}", settings.FileName, settings.Arguments, exception);
return -1;
}
returnCode = [task terminationStatus];
}
else {
String exception;
@try
{
[task launch];
}
@catch (NSException* e)
{
exception = e.reason.UTF8String;
}
if (!exception.IsEmpty())
{
LOG(Error, "Failed to run command {0} {1} with error {2}", settings.FileName, settings.Arguments, exception);
return -1;
}
}
return returnCode;
}

View File

@@ -237,8 +237,8 @@ class UnixConditionVariable;
typedef UnixConditionVariable ConditionVariable;
class MacFileSystem;
typedef MacFileSystem FileSystem;
class FileSystemWatcherBase;
typedef FileSystemWatcherBase FileSystemWatcher;
class MacFileSystemWatcher;
typedef MacFileSystemWatcher FileSystemWatcher;
class UnixFile;
typedef UnixFile File;
class MacPlatform;