From f40657ea043793670caa3305f9c7cc9bd5309be2 Mon Sep 17 00:00:00 2001 From: Andrew Spiering Date: Tue, 19 Sep 2023 20:58:12 -0700 Subject: [PATCH] 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 --- Source/Editor/Scripting/ScriptsBuilder.cpp | 9 +- .../Engine/Platform/Apple/ApplePlatform.cpp | 40 ++++++ Source/Engine/Platform/Apple/AppleUtils.h | 3 + Source/Engine/Platform/FileSystemWatcher.h | 2 + Source/Engine/Platform/Mac/MacFileSystem.cpp | 2 +- .../Platform/Mac/MacFileSystemWatcher.cpp | 100 +++++++++++++++ .../Platform/Mac/MacFileSystemWatcher.h | 39 ++++++ Source/Engine/Platform/Mac/MacPlatform.cpp | 115 ++++++++++++------ Source/Engine/Platform/Types.h | 4 +- 9 files changed, 268 insertions(+), 46 deletions(-) create mode 100644 Source/Engine/Platform/Mac/MacFileSystemWatcher.cpp create mode 100644 Source/Engine/Platform/Mac/MacFileSystemWatcher.h diff --git a/Source/Editor/Scripting/ScriptsBuilder.cpp b/Source/Editor/Scripting/ScriptsBuilder.cpp index 3082a531e..8807379cf 100644 --- a/Source/Editor/Scripting/ScriptsBuilder.cpp +++ b/Source/Editor/Scripting/ScriptsBuilder.cpp @@ -246,20 +246,17 @@ bool ScriptsBuilder::RunBuildTool(const StringView& args, const StringView& work Log::FileNotFoundException(monoPath).SetLevel(LogType::Fatal); return true; } - //const String monoPath = TEXT("mono"); - cmdLine.Append(TEXT("\"")); + const String monoPath = TEXT("mono"); cmdLine.Append(monoPath); - cmdLine.Append(TEXT("\" ")); + cmdLine.Append(TEXT(" ")); // TODO: Set env var for the mono MONO_GC_PARAMS=nursery-size64m to boost build performance -> profile it #endif - cmdLine.Append(TEXT("\"")); cmdLine.Append(buildToolPath); - cmdLine.Append(TEXT("\" ")); - cmdLine.Append(args.Get(), args.Length()); // Call build tool CreateProcessSettings procSettings; procSettings.FileName = StringView(*cmdLine, cmdLine.Length()); + procSettings.Arguments = args.Get(); procSettings.WorkingDirectory = workingDir; const int32 result = Platform::CreateProcess(procSettings); if (result != 0) diff --git a/Source/Engine/Platform/Apple/ApplePlatform.cpp b/Source/Engine/Platform/Apple/ApplePlatform.cpp index 8ae5a484f..eeef5b91f 100644 --- a/Source/Engine/Platform/Apple/ApplePlatform.cpp +++ b/Source/Engine/Platform/Apple/ApplePlatform.cpp @@ -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:¤tArg]; + [scanner scanString:@"\"" intoString:NULL]; + insideQuotes = NO; + } else { + [scanner scanUpToString:@" " intoString:¤tArg]; + [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)) diff --git a/Source/Engine/Platform/Apple/AppleUtils.h b/Source/Engine/Platform/Apple/AppleUtils.h index d6f639042..7144ba14b 100644 --- a/Source/Engine/Platform/Apple/AppleUtils.h +++ b/Source/Engine/Platform/Apple/AppleUtils.h @@ -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); diff --git a/Source/Engine/Platform/FileSystemWatcher.h b/Source/Engine/Platform/FileSystemWatcher.h index c6b44ca80..1f02f5725 100644 --- a/Source/Engine/Platform/FileSystemWatcher.h +++ b/Source/Engine/Platform/FileSystemWatcher.h @@ -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 diff --git a/Source/Engine/Platform/Mac/MacFileSystem.cpp b/Source/Engine/Platform/Mac/MacFileSystem.cpp index 5efa1905b..c765feef4 100644 --- a/Source/Engine/Platform/Mac/MacFileSystem.cpp +++ b/Source/Engine/Platform/Mac/MacFileSystem.cpp @@ -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 diff --git a/Source/Engine/Platform/Mac/MacFileSystemWatcher.cpp b/Source/Engine/Platform/Mac/MacFileSystemWatcher.cpp new file mode 100644 index 000000000..be13e2737 --- /dev/null +++ b/Source/Engine/Platform/Mac/MacFileSystemWatcher.cpp @@ -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 diff --git a/Source/Engine/Platform/Mac/MacFileSystemWatcher.h b/Source/Engine/Platform/Mac/MacFileSystemWatcher.h new file mode 100644 index 000000000..97ca20bda --- /dev/null +++ b/Source/Engine/Platform/Mac/MacFileSystemWatcher.h @@ -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 + + +/// +/// Mac platform implementation of the file system watching object. +/// +class FLAXENGINE_API MacFileSystemWatcher : public FileSystemWatcherBase +{ +public: + + /// + /// Initializes a new instance of the class. + /// + /// The directory to watch. + /// True if monitor the directory tree rooted at the specified directory or just a given directory. + MacFileSystemWatcher(const String& directory, bool withSubDirs); + + /// + /// Finalizes an instance of the class. + /// + ~MacFileSystemWatcher(); + +public: + + + +private: + + FSEventStreamRef EventStream; + bool IsRunning; +}; +#endif diff --git a/Source/Engine/Platform/Mac/MacPlatform.cpp b/Source/Engine/Platform/Mac/MacPlatform.cpp index 8cba5e1dc..4c965f549 100644 --- a/Source/Engine/Platform/Mac/MacPlatform.cpp +++ b/Source/Engine/Platform/Mac/MacPlatform.cpp @@ -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 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; } diff --git a/Source/Engine/Platform/Types.h b/Source/Engine/Platform/Types.h index 76135dcaf..863743121 100644 --- a/Source/Engine/Platform/Types.h +++ b/Source/Engine/Platform/Types.h @@ -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;