| CARVIEW |
Select Language
HTTP/2 302
date: Thu, 01 Jan 2026 08:31:28 GMT
content-type: text/html; charset=utf-8
content-length: 0
vary: X-PJAX, X-PJAX-Container, Turbo-Visit, Turbo-Frame, X-Requested-With,Accept-Encoding, Accept, X-Requested-With
location: https://github.com/Pagghiu/SaneCppLibraries/releases/download/release/2025/11/SaneCppFileSystemWatcher.h
cache-control: no-cache
strict-transport-security: max-age=31536000; includeSubdomains; preload
x-frame-options: deny
x-content-type-options: nosniff
x-xss-protection: 0
referrer-policy: no-referrer-when-downgrade
content-security-policy: default-src 'none'; base-uri 'self'; child-src github.githubassets.com github.com/assets-cdn/worker/ github.com/assets/ gist.github.com/assets-cdn/worker/; connect-src 'self' uploads.github.com www.githubstatus.com collector.github.com raw.githubusercontent.com api.github.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com *.rel.tunnels.api.visualstudio.com wss://*.rel.tunnels.api.visualstudio.com github.githubassets.com objects-origin.githubusercontent.com copilot-proxy.githubusercontent.com proxy.individual.githubcopilot.com proxy.business.githubcopilot.com proxy.enterprise.githubcopilot.com *.actions.githubusercontent.com wss://*.actions.githubusercontent.com productionresultssa0.blob.core.windows.net/ productionresultssa1.blob.core.windows.net/ productionresultssa2.blob.core.windows.net/ productionresultssa3.blob.core.windows.net/ productionresultssa4.blob.core.windows.net/ productionresultssa5.blob.core.windows.net/ productionresultssa6.blob.core.windows.net/ productionresultssa7.blob.core.windows.net/ productionresultssa8.blob.core.windows.net/ productionresultssa9.blob.core.windows.net/ productionresultssa10.blob.core.windows.net/ productionresultssa11.blob.core.windows.net/ productionresultssa12.blob.core.windows.net/ productionresultssa13.blob.core.windows.net/ productionresultssa14.blob.core.windows.net/ productionresultssa15.blob.core.windows.net/ productionresultssa16.blob.core.windows.net/ productionresultssa17.blob.core.windows.net/ productionresultssa18.blob.core.windows.net/ productionresultssa19.blob.core.windows.net/ github-production-repository-image-32fea6.s3.amazonaws.com github-production-release-asset-2e65be.s3.amazonaws.com insights.github.com wss://alive.github.com wss://alive-staging.github.com api.githubcopilot.com api.individual.githubcopilot.com api.business.githubcopilot.com api.enterprise.githubcopilot.com; font-src github.githubassets.com; form-action 'self' github.com gist.github.com copilot-workspace.githubnext.com objects-origin.githubusercontent.com; frame-ancestors 'none'; frame-src viewscreen.githubusercontent.com notebooks.githubusercontent.com; img-src 'self' data: blob: github.githubassets.com media.githubusercontent.com camo.githubusercontent.com identicons.github.com avatars.githubusercontent.com private-avatars.githubusercontent.com github-cloud.s3.amazonaws.com objects.githubusercontent.com release-assets.githubusercontent.com secured-user-images.githubusercontent.com/ user-images.githubusercontent.com/ private-user-images.githubusercontent.com opengraph.githubassets.com marketplace-screenshots.githubusercontent.com/ copilotprodattachments.blob.core.windows.net/github-production-copilot-attachments/ github-production-user-asset-6210df.s3.amazonaws.com customer-stories-feed.github.com spotlights-feed.github.com objects-origin.githubusercontent.com *.githubusercontent.com; manifest-src 'self'; media-src github.com user-images.githubusercontent.com/ secured-user-images.githubusercontent.com/ private-user-images.githubusercontent.com github-production-user-asset-6210df.s3.amazonaws.com gist.github.com github.githubassets.com; script-src github.githubassets.com; style-src 'unsafe-inline' github.githubassets.com; upgrade-insecure-requests; worker-src github.githubassets.com github.com/assets-cdn/worker/ github.com/assets/ gist.github.com/assets-cdn/worker/
server: github.com
set-cookie: _gh_sess=FCHmlzCvH3PUh7OjqKHBd2nEtQvMGrp1qb8RVsBP%2Bw6OMrkgs%2B60RP8Gjnb5c%2FGXcmUDc8sXDtze5giEIGDEw0Yisno%2Bl1qbpfxqHduMZN9qgXXRJcALKtMi5yjrZ3ih6hoLY1RyI%2B4av11bOPzPTSTQjqHSIa8cw6ls2ulTFycWpNj1Tiwk0B5ibs4080%2FIOECYmJPW18d1pzGsKPhgcSCGKZELC4jjcZEM6n0evyNBA%2ByaTu9Iu78Ea8MMby5cOnyPehUB52M4u3IfftfkOg%3D%3D--jgyiyCerQmdwUGoX--QXRAWs1vxLedOf8UgPNq9Q%3D%3D; Path=/; HttpOnly; Secure; SameSite=Lax
set-cookie: _octo=GH1.1.1724289791.1767256287; Path=/; Domain=github.com; Expires=Fri, 01 Jan 2027 08:31:27 GMT; Secure; SameSite=Lax
set-cookie: logged_in=no; Path=/; Domain=github.com; Expires=Fri, 01 Jan 2027 08:31:27 GMT; HttpOnly; Secure; SameSite=Lax
x-github-request-id: 9E76:1924FC:10AC299:1355CB5:695630DF
HTTP/2 302
date: Thu, 01 Jan 2026 08:31:28 GMT
content-type: text/html; charset=utf-8
content-length: 0
vary: X-PJAX, X-PJAX-Container, Turbo-Visit, Turbo-Frame, X-Requested-With,Accept-Encoding, Accept, X-Requested-With
location: https://release-assets.githubusercontent.com/github-production-release-asset/734522771/33721cae-90d8-4f1b-bec4-fbd971f5ae88?sp=r&sv=2018-11-09&sr=b&spr=https&se=2026-01-01T09%3A19%3A58Z&rscd=attachment%3B+filename%3DSaneCppFileSystemWatcher.h&rsct=application%2Foctet-stream&skoid=96c2d410-5711-43a1-aedd-ab1947aa7ab0&sktid=398a6654-997b-47e9-b12b-9515b896b4de&skt=2026-01-01T08%3A19%3A14Z&ske=2026-01-01T09%3A19%3A58Z&sks=b&skv=2018-11-09&sig=p%2FKlud1KItXUkInexxY2yV1tVze4LF3FksBnvKFU7LA%3D&jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmVsZWFzZS1hc3NldHMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwia2V5Ijoia2V5MSIsImV4cCI6MTc2NzI1NjU4OCwibmJmIjoxNzY3MjU2Mjg4LCJwYXRoIjoicmVsZWFzZWFzc2V0cHJvZHVjdGlvbi5ibG9iLmNvcmUud2luZG93cy5uZXQifQ.0cNotp9w2ePrBA3K0vl1An9QkNkZMmruFvwb0_-xghA&response-content-disposition=attachment%3B%20filename%3DSaneCppFileSystemWatcher.h&response-content-type=application%2Foctet-stream
cache-control: no-cache
strict-transport-security: max-age=31536000; includeSubdomains; preload
x-frame-options: deny
x-content-type-options: nosniff
x-xss-protection: 0
referrer-policy: no-referrer-when-downgrade
content-security-policy: default-src 'none'; base-uri 'self'; child-src github.githubassets.com github.com/assets-cdn/worker/ github.com/assets/ gist.github.com/assets-cdn/worker/; connect-src 'self' uploads.github.com www.githubstatus.com collector.github.com raw.githubusercontent.com api.github.com github-cloud.s3.amazonaws.com github-production-repository-file-5c1aeb.s3.amazonaws.com github-production-upload-manifest-file-7fdce7.s3.amazonaws.com github-production-user-asset-6210df.s3.amazonaws.com *.rel.tunnels.api.visualstudio.com wss://*.rel.tunnels.api.visualstudio.com github.githubassets.com objects-origin.githubusercontent.com copilot-proxy.githubusercontent.com proxy.individual.githubcopilot.com proxy.business.githubcopilot.com proxy.enterprise.githubcopilot.com *.actions.githubusercontent.com wss://*.actions.githubusercontent.com productionresultssa0.blob.core.windows.net/ productionresultssa1.blob.core.windows.net/ productionresultssa2.blob.core.windows.net/ productionresultssa3.blob.core.windows.net/ productionresultssa4.blob.core.windows.net/ productionresultssa5.blob.core.windows.net/ productionresultssa6.blob.core.windows.net/ productionresultssa7.blob.core.windows.net/ productionresultssa8.blob.core.windows.net/ productionresultssa9.blob.core.windows.net/ productionresultssa10.blob.core.windows.net/ productionresultssa11.blob.core.windows.net/ productionresultssa12.blob.core.windows.net/ productionresultssa13.blob.core.windows.net/ productionresultssa14.blob.core.windows.net/ productionresultssa15.blob.core.windows.net/ productionresultssa16.blob.core.windows.net/ productionresultssa17.blob.core.windows.net/ productionresultssa18.blob.core.windows.net/ productionresultssa19.blob.core.windows.net/ github-production-repository-image-32fea6.s3.amazonaws.com github-production-release-asset-2e65be.s3.amazonaws.com insights.github.com wss://alive.github.com wss://alive-staging.github.com api.githubcopilot.com api.individual.githubcopilot.com api.business.githubcopilot.com api.enterprise.githubcopilot.com; font-src github.githubassets.com; form-action 'self' github.com gist.github.com copilot-workspace.githubnext.com objects-origin.githubusercontent.com; frame-ancestors 'none'; frame-src viewscreen.githubusercontent.com notebooks.githubusercontent.com; img-src 'self' data: blob: github.githubassets.com media.githubusercontent.com camo.githubusercontent.com identicons.github.com avatars.githubusercontent.com private-avatars.githubusercontent.com github-cloud.s3.amazonaws.com objects.githubusercontent.com release-assets.githubusercontent.com secured-user-images.githubusercontent.com/ user-images.githubusercontent.com/ private-user-images.githubusercontent.com opengraph.githubassets.com marketplace-screenshots.githubusercontent.com/ copilotprodattachments.blob.core.windows.net/github-production-copilot-attachments/ github-production-user-asset-6210df.s3.amazonaws.com customer-stories-feed.github.com spotlights-feed.github.com objects-origin.githubusercontent.com *.githubusercontent.com; manifest-src 'self'; media-src github.com user-images.githubusercontent.com/ secured-user-images.githubusercontent.com/ private-user-images.githubusercontent.com github-production-user-asset-6210df.s3.amazonaws.com gist.github.com github.githubassets.com; script-src github.githubassets.com; style-src 'unsafe-inline' github.githubassets.com; upgrade-insecure-requests; worker-src github.githubassets.com github.com/assets-cdn/worker/ github.com/assets/ gist.github.com/assets-cdn/worker/
server: github.com
x-github-request-id: 9E76:1924FC:10AC2D8:1355D03:695630DF
HTTP/2 200
last-modified: Sun, 30 Nov 2025 12:15:52 GMT
etag: "0x8DE300A34CD94B3"
server: Windows-Azure-Blob/1.0 Microsoft-HTTPAPI/2.0
x-ms-request-id: d0d6be21-601e-0003-71f9-7a0031000000
x-ms-version: 2018-11-09
x-ms-creation-time: Sun, 30 Nov 2025 12:15:52 GMT
x-ms-blob-content-md5: Mov8lhSNFeW4+E/AcMqK3w==
x-ms-lease-status: unlocked
x-ms-lease-state: available
x-ms-blob-type: BlockBlob
x-ms-server-encrypted: true
via: 1.1 varnish, 1.1 varnish
accept-ranges: bytes
age: 0
date: Thu, 01 Jan 2026 08:31:28 GMT
x-served-by: cache-iad-kjyo7100086-IAD, cache-bom-vanm7210088-BOM
x-cache: MISS, MISS
x-cache-hits: 0, 0
x-timer: S1767256289.583464,VS0,VE253
content-disposition: attachment; filename=SaneCppFileSystemWatcher.h
content-type: application/octet-stream
content-length: 70597
//----------------------------------------------------------------------------------------------------------------------
// SaneCppFileSystemWatcher.h - Sane C++ FileSystemWatcher Library (single file build)
//----------------------------------------------------------------------------------------------------------------------
// Dependencies: SaneCppFoundation.h
// Version: release/2025/11 (cf7313e5)
// LOC header: 126 (code) + 118 (comments)
// LOC implementation: 1305 (code) + 299 (comments)
// Documentation: https://pagghiu.github.io/SaneCppLibraries
// Source Code: https://github.com/pagghiu/SaneCppLibraries
//----------------------------------------------------------------------------------------------------------------------
// All copyrights and SPDX information for this library (each amalgamated section has its own copyright attributions):
// Copyright (c) Stefano Cristiano
// SPDX-License-Identifier: MIT
//----------------------------------------------------------------------------------------------------------------------
#include "SaneCppFoundation.h"
#if !defined(SANE_CPP_FILESYSTEMWATCHER_HEADER)
#define SANE_CPP_FILESYSTEMWATCHER_HEADER 1
//----------------------------------------------------------------------------------------------------------------------
// FileSystemWatcher/FileSystemWatcher.h
//----------------------------------------------------------------------------------------------------------------------
// Copyright (c) Stefano Cristiano
// SPDX-License-Identifier: MIT
namespace SC
{
//! @defgroup group_file_system_watcher FileSystem Watcher
//! @copybrief library_file_system_watcher (see @ref library_file_system_watcher for more details)
//! @addtogroup group_file_system_watcher
//! @{
/// @brief Notifies about events (add, remove, rename, modified) on files and directories.
/// Caller can specify a callback for receiving notifications the SC::FileSystemWatcher::watch method.
///
/// Changes are grouped in two categories:
/// - Added, removed and renamed files and directories
/// - Modified files
///
/// @warning Modifications to files that do not affect directory entries may not trigger notifications. @n
/// This includes modifications made through symbolic or hard links located outside the watched directory,
/// pointing to a file of the watched directory. @n
/// Modifications made through memory-mapped file operations (mmap) can also exhibit the same behaviour. @n
/// The underlying OS APIs monitor directory entries rather than all possible file access methods.
///
/// There are two modes in which FileSystemWatcher can be initialized, defining how notifications are delivered:
///
/// | Mode | Description |
/// |:--------------------------------------|:--------------------------------------------------|
/// | SC::FileSystemWatcher::ThreadRunner | @copybrief SC::FileSystemWatcher::ThreadRunner |
/// | SC::FileSystemWatcher::EventLoopRunner| @copybrief SC::FileSystemWatcher::EventLoopRunner |
///
/// Example using SC::FileSystemWatcher::ThreadRunner:
/// \snippet Tests/Libraries/FileSystemWatcher/FileSystemWatcherTest.cpp fileSystemWatcherThreadRunnerSnippet
///
/// Example using SC::FileSystemWatcherAsync (that implements SC::FileSystemWatcher::EventLoopRunner):
/// \snippet Tests/Libraries/FileSystemWatcherAsync/FileSystemWatcherAsyncTest.cpp fileSystemWatcherAsyncSnippet
//! [OpaqueDeclarationSnippet]
struct FileSystemWatcher
{
private:
struct Internal;
struct InternalDefinition
{
static constexpr int Windows = 3 * sizeof(void*);
static constexpr int Apple = 42 * sizeof(void*);
static constexpr int Linux = sizeof(void*) * 4;
static constexpr int Default = Linux;
static constexpr size_t Alignment = alignof(void*);
using Object = Internal;
};
public:
// Must be public to avoid GCC complaining
using InternalOpaque = OpaqueObject;
private:
InternalOpaque internal;
//...
//! [OpaqueDeclarationSnippet]
struct ThreadRunnerInternal;
struct ThreadRunnerDefinition
{
static constexpr int MaxWatchablePaths = 1024;
static constexpr int Windows = (2 * MaxWatchablePaths + 2) * sizeof(void*) + sizeof(uint64_t);
static constexpr int Apple = sizeof(void*);
static constexpr int Linux = sizeof(void*) * 6;
static constexpr int Default = Linux;
static constexpr size_t Alignment = alignof(void*);
using Object = ThreadRunnerInternal;
};
struct FolderWatcherInternal;
struct FolderWatcherSizes
{
static constexpr int MaxNumberOfSubdirs = 128; // Max number of subfolders tracked in a watcher
static constexpr int MaxChangesBufferSize = 1024;
static constexpr int Windows = MaxChangesBufferSize + sizeof(void*) + sizeof(void*);
static constexpr int Apple = sizeof(void*);
static constexpr int Linux = 1056 + 1024 + 8;
static constexpr int Default = Linux;
static constexpr size_t Alignment = alignof(void*);
using Object = FolderWatcherInternal;
};
public:
/// @brief Specifies the event classes. Some events are grouped in a single one because
/// it's non-trivial providing precise notifications that are consistent across platforms.
enum class Operation
{
Modified, ///< A file or directory has been modified in its contents and/or timestamp
AddRemoveRename, ///< A file or directory has been added, removed or renamed
};
/// @brief Notification holding type and path
struct Notification
{
StringSpan basePath; ///< Reference to the watched directory
StringSpan relativePath; ///< Relative path of the file being notified from `basePath`
Operation operation = Operation::Modified; ///< Notification type
/// @brief Get the full path of the file being watched.
/// @param path StringPath that will hold full (absolute) path
/// @return Invalid result if it's not possible building the full path
SC::Result getFullPath(StringPath& path) const;
private:
friend struct Internal;
#if SC_PLATFORM_APPLE
StringSpan fullPath;
#endif
};
/// @brief Represents a single folder being watched.
/// While in use, the address of this object must not change, as it's inserted in a linked list.
/// @note You can create an SC::ArenaMap to create a buffer of these objects, that can be easily reused.
struct FolderWatcher
{
/// @brief Constructs a folder watcher
/// @param subFolderRelativePathsBuffer User provided buffer used to sub-folders relative paths.
/// When an empty span is passed, the internal 1Kb buffer is used.
/// This buffer is used on Linux only when watching folders recursively, it's unused on Windows / macOS.
FolderWatcher(Span subFolderRelativePathsBuffer = {});
Function notifyCallback; ///< Function that will be called on a notification
/// @brief Stop watching this directory. After calling it the FolderWatcher can be reused or released.
/// @return Valid result if directory was unwatched successfully.
Result stopWatching();
/// @brief Sets debug name for AsyncFilePoll used on Windows (used only for debug purposes)
void setDebugName(const char* debugName);
private:
friend struct FileSystemWatcher;
friend struct FileSystemWatcherAsync;
#if SC_PLATFORM_WINDOWS
#if SC_ASYNC_ENABLE_LOG
AlignedStorage<120> asyncStorage;
#else
AlignedStorage<112> asyncStorage;
#endif
#endif
OpaqueObject internal;
FileSystemWatcher* parent = nullptr;
FolderWatcher* next = nullptr;
FolderWatcher* prev = nullptr;
StringPath path;
#if SC_PLATFORM_LINUX
Span subFolderRelativePathsBuffer;
#endif
};
/// @brief Abstract class to use event loop notifications (see SC::FileSystemWatcherAsync).
struct EventLoopRunner
{
virtual ~EventLoopRunner() {}
protected:
#if SC_PLATFORM_APPLE
virtual Result appleStartWakeUp() = 0;
virtual void appleSignalEventObject() = 0;
virtual Result appleWakeUpAndWait() = 0;
#elif SC_PLATFORM_LINUX
virtual Result linuxStartSharedFilePoll() = 0;
virtual Result linuxStopSharedFilePoll() = 0;
int notifyFd = -1;
#else
virtual Result windowsStartFolderFilePoll(FolderWatcher& watcher, void* handle) = 0;
virtual Result windowsStopFolderFilePoll(FolderWatcher& watcher) = 0;
virtual void* windowsGetOverlapped(FolderWatcher& watcher) = 0;
#endif
friend struct Internal;
FileSystemWatcher* fileSystemWatcher = nullptr;
void internalInit(FileSystemWatcher& fsWatcher, int handle);
};
/// @brief Delivers notifications on a background thread.
using ThreadRunner = OpaqueObject;
/// @brief Setup watcher to receive notifications from a background thread
/// @param runner Address of a ThreadRunner object that must be valid until close()
/// @return Valid Result if the watcher has been initialized correctly
Result init(ThreadRunner& runner);
/// @brief Setup watcher to receive async notifications on an event loop
/// @param runner Address of a EventLoopRunner object that must be valid until close()
/// @return Valid Result if the watcher has been initialized correctly
Result init(EventLoopRunner& runner);
/// @brief Stops all watchers and frees the ThreadRunner or EventLoopRunner passed in init
/// @return Valid Result if resources have been freed successfully
Result close();
/// @brief Starts watching a single directory, calling FolderWatcher::notifyCallback on file events.
/// @param watcher Reference to a (not already used) watcher, with a valid FolderWatcher::notifyCallback.
/// Its address must not change until FolderWatcher::stopWatching or FileSystemWatcher::close
/// @param path The directory being monitored
/// @return Valid Result if directory is accessible and the watcher is initialized properly.
Result watch(FolderWatcher& watcher, StringSpan path);
void asyncNotify(FolderWatcher* watcher);
private:
friend decltype(internal);
friend decltype(FolderWatcher::internal);
friend struct EventLoopRunner;
// Trimmed duplicate of IntrusiveDoubleLinkedList
struct WatcherLinkedList
{
FolderWatcher* back = nullptr; // has no next
FolderWatcher* front = nullptr; // has no prev
void queueBack(FolderWatcher& watcher);
void remove(FolderWatcher& watcher);
};
WatcherLinkedList watchers;
};
//! @}
} // namespace SC
#endif // SANE_CPP_FILESYSTEMWATCHER_HEADER
#if defined(SANE_CPP_IMPLEMENTATION) && !defined(SANE_CPP_FILESYSTEMWATCHER_IMPLEMENTATION)
#define SANE_CPP_FILESYSTEMWATCHER_IMPLEMENTATION 1
//----------------------------------------------------------------------------------------------------------------------
// FileSystemWatcher/FileSystemWatcher.cpp
//----------------------------------------------------------------------------------------------------------------------
// Copyright (c) Stefano Cristiano
// SPDX-License-Identifier: MIT
//----------------------------------------------------------------------------------------------------------------------
// FileSystemWatcher/Internal/FileSystemWatcherThreading.h
//----------------------------------------------------------------------------------------------------------------------
// Copyright (c) Stefano Cristiano
// SPDX-License-Identifier: MIT
#if _WIN32
#define WIN32_LEAN_AND_MEAN
#include
namespace SC
{
struct FSWAtomicBool
{
volatile bool value = false;
bool load() { return InterlockedOr(reinterpret_cast(&value), 0) != 0; }
bool exchange(bool desired) { return InterlockedExchange(reinterpret_cast(&value), desired) != 0; }
};
struct FSWThread
{
HANDLE thread = nullptr;
Result start(DWORD (*func)(LPVOID arg), LPVOID param)
{
thread = ::CreateThread(nullptr, 0, func, param, 0, nullptr);
return thread ? Result(true) : Result::Error("CreateThread failed");
}
Result join()
{
if (!thread)
return Result(true);
DWORD res = ::WaitForSingleObject(thread, INFINITE);
::CloseHandle(thread);
thread = nullptr;
return res == WAIT_OBJECT_0 ? Result(true) : Result::Error("WaitForSingleObject failed");
}
bool wasStarted() const { return thread != nullptr; }
void setThreadName(const wchar_t* name) { ::SetThreadDescription(::GetCurrentThread(), name); }
};
} // namespace SC
#else
#include // errno
#include // pthread_
#include // usleep
namespace SC
{
// Atomic bool for thread safety
struct FSWAtomicBool
{
volatile bool value = false;
bool load() const { return __atomic_load_n(&value, __ATOMIC_SEQ_CST); }
void store(bool desired) { __atomic_store_n(&value, desired, __ATOMIC_SEQ_CST); }
bool exchange(bool desired) { return __atomic_exchange_n(&value, desired, __ATOMIC_SEQ_CST); }
};
// Minimal Mutex wrapper
struct FSWMutex
{
pthread_mutex_t mutex;
FSWMutex() { ::pthread_mutex_init(&mutex, nullptr); }
~FSWMutex() { ::pthread_mutex_destroy(&mutex); }
void lock() { ::pthread_mutex_lock(&mutex); }
void unlock() { ::pthread_mutex_unlock(&mutex); }
};
// Minimal Condition Variable wrapper
struct FSWCondition
{
pthread_cond_t cond;
FSWCondition() { ::pthread_cond_init(&cond, nullptr); }
~FSWCondition() { ::pthread_cond_destroy(&cond); }
void wait(FSWMutex& mutex) { ::pthread_cond_wait(&cond, &mutex.mutex); }
void signal() { ::pthread_cond_signal(&cond); }
void broadcast() { ::pthread_cond_broadcast(&cond); }
};
// Minimal Event Object
struct FSWEventObject
{
FSWMutex mutex;
FSWCondition cond;
bool signaled = false;
bool autoReset = true;
void wait()
{
mutex.lock();
while (not signaled)
{
cond.wait(mutex);
}
if (autoReset)
{
signaled = false;
}
mutex.unlock();
}
void signal()
{
mutex.lock();
signaled = true;
cond.signal();
mutex.unlock();
}
};
// Minimal Thread wrapper
struct FSWThread
{
pthread_t thread = 0;
Function userFunction;
static void* threadFunc(void* arg)
{
FSWThread& self = *static_cast(arg);
self.userFunction(self);
return 0;
}
Result start(Function func)
{
SC_TRY_MSG(thread == 0, "Thread already started");
userFunction = move(func);
const int res = pthread_create(&thread, nullptr, &FSWThread::threadFunc, this);
SC_TRY_MSG(res == 0, "pthread_create error");
return Result(true);
}
Result join()
{
if (thread != 0)
{
const int res = pthread_join(thread, nullptr);
thread = 0;
SC_TRY_MSG(res == 0, "pthread_join error");
}
return Result(true);
}
bool wasStarted() const { return thread != 0; }
void setThreadName(const char* name)
{
#if SC_PLATFORM_APPLE
pthread_setname_np(name);
#else
pthread_setname_np(pthread_self(), name);
#endif
}
};
} // namespace SC
#endif
#if SC_PLATFORM_WINDOWS
//----------------------------------------------------------------------------------------------------------------------
// FileSystemWatcher/Internal/FileSystemWatcherWindows.inl
//----------------------------------------------------------------------------------------------------------------------
// Copyright (c) Stefano Cristiano
// SPDX-License-Identifier: MIT
#define WIN32_LEAN_AND_MEAN
#include
struct SC::FileSystemWatcher::FolderWatcherInternal
{
uint8_t changesBuffer[FolderWatcherSizes::MaxChangesBufferSize];
FolderWatcher* parentEntry = nullptr; // We could in theory use SC_COMPILER_FIELD_OFFSET somehow to obtain it...
HANDLE fileHandle;
};
struct SC::FileSystemWatcher::ThreadRunnerInternal
{
FSWThread thread;
static constexpr int N = ThreadRunnerDefinition::MaxWatchablePaths;
HANDLE hEvents[N] = {0};
FolderWatcher* entries[N] = {nullptr};
DWORD numEntries = 0;
FSWAtomicBool shouldStop;
};
struct SC::FileSystemWatcher::Internal
{
FileSystemWatcher* self = nullptr;
EventLoopRunner* eventLoopRunner = nullptr;
ThreadRunnerInternal* threadingRunner = nullptr;
OVERLAPPED* getOverlapped(FolderWatcher& watcher)
{
if (eventLoopRunner)
{
return reinterpret_cast(eventLoopRunner->windowsGetOverlapped(watcher));
}
else
{
return &watcher.asyncStorage.reinterpret_as();
}
}
Result init(FileSystemWatcher& parent, ThreadRunner& runner)
{
self = &parent;
threadingRunner = &runner.get();
return Result(true);
}
Result init(FileSystemWatcher& parent, EventLoopRunner& runner)
{
self = &parent;
eventLoopRunner = &runner;
eventLoopRunner->internalInit(parent, 0);
return Result(true);
}
Result close()
{
if (threadingRunner)
{
if (threadingRunner->thread.wasStarted())
{
threadingRunner->shouldStop.exchange(true);
do
{
for (DWORD idx = 0; idx < threadingRunner->numEntries; ++idx)
{
::SetEvent(threadingRunner->hEvents[idx]);
}
} while (threadingRunner->shouldStop.load());
SC_TRY(threadingRunner->thread.join());
}
}
for (FolderWatcher* entry = self->watchers.front; entry != nullptr; entry = entry->next)
{
SC_TRY(stopWatching(*entry));
}
return Result(true);
}
void signalWatcherEvent(FolderWatcher& watcher)
{
OVERLAPPED* overlapped = getOverlapped(watcher);
::SetEvent(overlapped->hEvent);
}
void closeWatcherEvent(FolderWatcher& watcher)
{
OVERLAPPED* overlapped = getOverlapped(watcher);
::CloseHandle(overlapped->hEvent);
overlapped->hEvent = INVALID_HANDLE_VALUE;
}
void closeFileHandle(FolderWatcher& watcher)
{
auto& opaque = watcher.internal.get();
::CloseHandle(opaque.fileHandle);
opaque.fileHandle = INVALID_HANDLE_VALUE;
}
Result stopWatching(FolderWatcher& folderWatcher)
{
folderWatcher.parent->watchers.remove(folderWatcher);
folderWatcher.parent = nullptr;
if (threadingRunner)
{
signalWatcherEvent(folderWatcher);
closeWatcherEvent(folderWatcher);
}
else
{
SC_TRUST_RESULT(eventLoopRunner->windowsStopFolderFilePoll(folderWatcher));
}
closeFileHandle(folderWatcher);
return Result(true);
}
Result startWatching(FolderWatcher* entry)
{
if (threadingRunner)
{
threadingRunner->numEntries = 0;
}
// TODO: we should probably check if we are leaking on some partial failure code path...some RAII would help
if (threadingRunner)
{
SC_TRY_MSG(threadingRunner->numEntries < ThreadRunnerDefinition::MaxWatchablePaths,
"startWatching exceeded MaxWatchablePaths");
}
HANDLE newHandle = ::CreateFileW(entry->path.view().getNullTerminatedNative(), //
FILE_LIST_DIRECTORY, //
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, //
nullptr, //
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED, //
nullptr);
SC_TRY_MSG(newHandle != INVALID_HANDLE_VALUE, "CreateFileW failed");
FolderWatcherInternal& opaque = entry->internal.get();
opaque.fileHandle = newHandle;
opaque.parentEntry = entry;
OVERLAPPED* overlapped = getOverlapped(*entry);
if (threadingRunner)
{
overlapped->hEvent = ::CreateEventW(nullptr, FALSE, 0, nullptr);
threadingRunner->hEvents[threadingRunner->numEntries] = overlapped->hEvent;
threadingRunner->entries[threadingRunner->numEntries] = entry;
threadingRunner->numEntries++;
}
else
{
SC_TRY(eventLoopRunner->windowsStartFolderFilePoll(*entry, newHandle));
}
BOOL success = ::ReadDirectoryChangesW(newHandle, //
opaque.changesBuffer, //
sizeof(opaque.changesBuffer), //
TRUE, // watchSubtree
FILE_NOTIFY_CHANGE_FILE_NAME | //
FILE_NOTIFY_CHANGE_DIR_NAME | //
FILE_NOTIFY_CHANGE_LAST_WRITE, //
nullptr, // lpBytesReturned
overlapped, // lpOverlapped
nullptr); // lpCompletionRoutine
SC_TRY_MSG(success == TRUE, "ReadDirectoryChangesW");
if (threadingRunner and not threadingRunner->thread.wasStarted())
{
threadingRunner->shouldStop.exchange(false);
SC_TRY(threadingRunner->thread.start(&threadDispatch, this));
}
return Result(true);
}
static DWORD threadDispatch(LPVOID arg)
{
Internal& self = *static_cast(arg);
self.threadRun(self.threadingRunner->thread);
return 0;
}
void threadRun(FSWThread& thread)
{
thread.setThreadName(SC_NATIVE_STR("FileSystemWatcher::init"));
ThreadRunnerInternal& runner = *threadingRunner;
while (not runner.shouldStop.load())
{
const DWORD result = ::WaitForMultipleObjects(runner.numEntries, runner.hEvents, TRUE, INFINITE);
if (result != WAIT_FAILED and not runner.shouldStop.load())
{
const DWORD index = result - WAIT_OBJECT_0;
FolderWatcher& entry = *runner.entries[index];
FolderWatcherInternal& opaque = entry.internal.get();
DWORD transferredBytes;
SC_ASSERT_DEBUG(opaque.fileHandle != INVALID_HANDLE_VALUE);
OVERLAPPED* overlapped = getOverlapped(entry);
::GetOverlappedResult(opaque.fileHandle, overlapped, &transferredBytes, FALSE);
notifyEntry(entry);
}
}
threadingRunner->shouldStop.exchange(false);
}
static void notifyEntry(FolderWatcher& entry)
{
FolderWatcherInternal& opaque = entry.internal.get();
FILE_NOTIFY_INFORMATION* event = reinterpret_cast(opaque.changesBuffer);
Notification notification;
notification.basePath = entry.path.view();
do
{
notification.relativePath = {
{reinterpret_cast(event->FileName), static_cast(event->FileNameLength)},
true,
StringEncoding::Utf16};
switch (event->Action)
{
case FILE_ACTION_MODIFIED: notification.operation = Operation::Modified; break;
default: notification.operation = Operation::AddRemoveRename; break;
}
entry.notifyCallback(notification);
if (not event->NextEntryOffset)
break;
*reinterpret_cast(&event) += event->NextEntryOffset;
} while (true);
OVERLAPPED* overlapped = entry.parent->internal.get().getOverlapped(entry);
::memset(overlapped, 0, sizeof(OVERLAPPED));
SC_ASSERT_DEBUG(opaque.fileHandle != INVALID_HANDLE_VALUE);
BOOL success = ::ReadDirectoryChangesW(opaque.fileHandle, //
opaque.changesBuffer, //
sizeof(opaque.changesBuffer), //
TRUE, // watchSubtree
FILE_NOTIFY_CHANGE_FILE_NAME | //
FILE_NOTIFY_CHANGE_DIR_NAME | //
FILE_NOTIFY_CHANGE_LAST_WRITE, //
nullptr, // lpBytesReturned
overlapped, // lpOverlapped
nullptr); // lpCompletionRoutine
// TODO: Handle ReadDirectoryChangesW error
(void)success;
}
};
SC::Result SC::FileSystemWatcher::Notification::getFullPath(StringPath& buffer) const
{
SC_TRY_MSG(buffer.assign(basePath), "Buffer too small to hold full path");
SC_TRY_MSG(buffer.append(L"\\"), "Buffer too small to hold full path");
SC_TRY_MSG(buffer.append(relativePath), "Buffer too small to hold full path");
return Result(true);
}
void SC::FileSystemWatcher::asyncNotify(FolderWatcher* watcher)
{
SC_ASSERT_DEBUG(watcher != nullptr);
SC_ASSERT_DEBUG(watcher->internal.get().fileHandle != INVALID_HANDLE_VALUE);
FileSystemWatcher::Internal::notifyEntry(*watcher->internal.get().parentEntry);
}
#elif SC_PLATFORM_APPLE
//----------------------------------------------------------------------------------------------------------------------
// FileSystemWatcher/Internal/FileSystemWatcherApple.inl
//----------------------------------------------------------------------------------------------------------------------
// Copyright (c) Stefano Cristiano
// SPDX-License-Identifier: MIT
//! [OpaqueDefinition1Snippet]
#include // FSEvents
#include
#include
#if TARGET_OS_IPHONE
// TODO: Figure out another API for ios as this is a private API and it will not be accepted on app store.
//----------------------------------------------------------------------------------------------------------------------
// FileSystemWatcher/Internal/FSEventsIOS.h
//----------------------------------------------------------------------------------------------------------------------
// On iOS this is a private API so to use it we must copy at least the API definitions
#ifndef __CFRUNLOOP__
#include
#endif
#ifndef __CFUUID__
#include
#endif
#include
#include
#include
#include
extern "C"
{
CF_ASSUME_NONNULL_BEGIN
#pragma pack(push, 2)
typedef UInt32 FSEventStreamCreateFlags;
enum
{
kFSEventStreamCreateFlagNone = 0x00000000,
kFSEventStreamCreateFlagUseCFTypes = 0x00000001,
kFSEventStreamCreateFlagNoDefer = 0x00000002,
kFSEventStreamCreateFlagWatchRoot = 0x00000004,
kFSEventStreamCreateFlagIgnoreSelf = 0x00000008,
kFSEventStreamCreateFlagFileEvents = 0x00000010,
kFSEventStreamCreateFlagMarkSelf = 0x00000020,
kFSEventStreamCreateFlagUseExtendedData = 0x00000040,
kFSEventStreamCreateFlagFullHistory = 0x00000080,
};
#define kFSEventStreamEventExtendedDataPathKey CFSTR("path")
#define kFSEventStreamEventExtendedFileIDKey CFSTR("fileID")
typedef UInt32 FSEventStreamEventFlags;
enum
{
kFSEventStreamEventFlagNone = 0x00000000,
kFSEventStreamEventFlagMustScanSubDirs = 0x00000001,
kFSEventStreamEventFlagUserDropped = 0x00000002,
kFSEventStreamEventFlagKernelDropped = 0x00000004,
kFSEventStreamEventFlagEventIdsWrapped = 0x00000008,
kFSEventStreamEventFlagHistoryDone = 0x00000010,
kFSEventStreamEventFlagRootChanged = 0x00000020,
kFSEventStreamEventFlagMount = 0x00000040,
kFSEventStreamEventFlagUnmount = 0x00000080,
kFSEventStreamEventFlagItemCreated = 0x00000100,
kFSEventStreamEventFlagItemRemoved = 0x00000200,
kFSEventStreamEventFlagItemInodeMetaMod = 0x00000400,
kFSEventStreamEventFlagItemRenamed = 0x00000800,
kFSEventStreamEventFlagItemModified = 0x00001000,
kFSEventStreamEventFlagItemFinderInfoMod = 0x00002000,
kFSEventStreamEventFlagItemChangeOwner = 0x00004000,
kFSEventStreamEventFlagItemXattrMod = 0x00008000,
kFSEventStreamEventFlagItemIsFile = 0x00010000,
kFSEventStreamEventFlagItemIsDir = 0x00020000,
kFSEventStreamEventFlagItemIsSymlink = 0x00040000,
kFSEventStreamEventFlagOwnEvent = 0x00080000,
kFSEventStreamEventFlagItemIsHardlink = 0x00100000,
kFSEventStreamEventFlagItemIsLastHardlink = 0x00200000,
kFSEventStreamEventFlagItemCloned = 0x00400000
};
typedef UInt64 FSEventStreamEventId;
enum
{
kFSEventStreamEventIdSinceNow = 0xFFFFFFFFFFFFFFFFULL
};
typedef struct __FSEventStream* FSEventStreamRef;
typedef const struct __FSEventStream* ConstFSEventStreamRef;
struct FSEventStreamContext
{
CFIndex version;
void* __nullable info;
CFAllocatorRetainCallBack __nullable retain;
CFAllocatorReleaseCallBack __nullable release;
CFAllocatorCopyDescriptionCallBack __nullable copyDescription;
};
typedef struct FSEventStreamContext FSEventStreamContext;
typedef CALLBACK_API_C(void, FSEventStreamCallback)(ConstFSEventStreamRef streamRef,
void* __nullable clientCallBackInfo, size_t numEvents,
void* eventPaths,
const FSEventStreamEventFlags* _Nonnull eventFlags,
const FSEventStreamEventId* _Nonnull eventIds);
extern FSEventStreamRef __nullable FSEventStreamCreate(CFAllocatorRef __nullable allocator,
FSEventStreamCallback callback,
FSEventStreamContext* __nullable context,
CFArrayRef pathsToWatch, FSEventStreamEventId sinceWhen,
CFTimeInterval latency, FSEventStreamCreateFlags flags);
extern FSEventStreamRef __nullable FSEventStreamCreateRelativeToDevice(
CFAllocatorRef __nullable allocator, FSEventStreamCallback callback, FSEventStreamContext* __nullable context,
dev_t deviceToWatch, CFArrayRef pathsToWatchRelativeToDevice, FSEventStreamEventId sinceWhen,
CFTimeInterval latency, FSEventStreamCreateFlags flags);
extern FSEventStreamEventId FSEventStreamGetLatestEventId(ConstFSEventStreamRef streamRef);
extern dev_t FSEventStreamGetDeviceBeingWatched(ConstFSEventStreamRef streamRef);
extern CF_RETURNS_RETAINED CFArrayRef FSEventStreamCopyPathsBeingWatched(ConstFSEventStreamRef streamRef);
extern FSEventStreamEventId FSEventsGetCurrentEventId(void);
extern CF_RETURNS_RETAINED CFUUIDRef __nullable FSEventsCopyUUIDForDevice(dev_t dev);
extern FSEventStreamEventId FSEventsGetLastEventIdForDeviceBeforeTime(dev_t dev, CFAbsoluteTime time);
extern Boolean FSEventsPurgeEventsForDeviceUpToEventId(dev_t dev, FSEventStreamEventId eventId);
extern void FSEventStreamRetain(FSEventStreamRef streamRef);
extern void FSEventStreamRelease(FSEventStreamRef streamRef);
extern void FSEventStreamScheduleWithRunLoop(FSEventStreamRef streamRef, CFRunLoopRef runLoop,
CFStringRef runLoopMode)
API_DEPRECATED("Use FSEventStreamSetDispatchQueue instead.", macos(10.5, 13.0), ios(6.0, 16.0));
extern void FSEventStreamUnscheduleFromRunLoop(FSEventStreamRef streamRef, CFRunLoopRef runLoop,
CFStringRef runLoopMode)
API_DEPRECATED("Use FSEventStreamSetDispatchQueue instead.", macos(10.5, 13.0), ios(6.0, 16.0));
extern void FSEventStreamSetDispatchQueue(FSEventStreamRef streamRef, dispatch_queue_t __nullable q);
extern void FSEventStreamInvalidate(FSEventStreamRef streamRef);
extern Boolean FSEventStreamStart(FSEventStreamRef streamRef);
extern FSEventStreamEventId FSEventStreamFlushAsync(FSEventStreamRef streamRef);
extern void FSEventStreamFlushSync(FSEventStreamRef streamRef);
extern void FSEventStreamStop(FSEventStreamRef streamRef);
extern void FSEventStreamShow(ConstFSEventStreamRef streamRef);
extern CF_RETURNS_RETAINED CFStringRef FSEventStreamCopyDescription(ConstFSEventStreamRef streamRef);
#pragma pack(pop)
CF_ASSUME_NONNULL_END
}
#endif
struct SC::FileSystemWatcher::Internal
{
FileSystemWatcher* self = nullptr;
CFRunLoopRef runLoop = nullptr;
CFRunLoopSourceRef refreshSignal = nullptr;
FSEventStreamRef fsEventStream = nullptr;
FSWThread pollingThread;
Result signalReturnCode = Result(false);
FSWEventObject refreshSignalFinished;
FSWMutex mutex;
EventLoopRunner* eventLoopRunner = nullptr;
// Used to pass data from thread to async callback
Notification notification;
FolderWatcher* watcher;
FSWAtomicBool closing;
//...
//! [OpaqueDefinition1Snippet]
Result init(FileSystemWatcher& parent, ThreadRunner& runner)
{
SC_COMPILER_UNUSED(runner);
self = &parent;
return Result(true);
}
Result init(FileSystemWatcher& parent, EventLoopRunner& runner)
{
self = &parent;
eventLoopRunner = &runner;
eventLoopRunner->internalInit(parent, 0);
return eventLoopRunner->appleStartWakeUp();
}
Result initThread()
{
closing.exchange(false);
// Create Signal to go from Loop --> CFRunLoop
CFRunLoopSourceContext signalContext;
::memset(&signalContext, 0, sizeof(signalContext));
signalContext.info = this;
signalContext.perform = &Internal::threadExecuteRefresh;
refreshSignal = CFRunLoopSourceCreate(nullptr, 0, &signalContext);
SC_TRY_MSG(refreshSignal != nullptr, "CFRunLoopSourceCreate failed");
FSWEventObject eventObject;
auto pollingFunction = [&](FSWThread& thread)
{
thread.setThreadName("FileSystemWatcher::init");
threadInit(); // Obtain the CFRunLoop for this thread
eventObject.signal();
threadRun();
};
SC_TRY(pollingThread.start(pollingFunction));
eventObject.wait();
return Result(true);
}
Result close()
{
if (pollingThread.wasStarted())
{
closing.exchange(true);
if (eventLoopRunner)
{
eventLoopRunner->appleSignalEventObject();
}
// send close signal
wakeUpFSEventThread();
// Wait for thread to finish
SC_TRY(pollingThread.join());
releaseResources();
}
return Result(true);
}
void wakeUpFSEventThread()
{
CFRunLoopSourceSignal(refreshSignal);
CFRunLoopWakeUp(runLoop);
refreshSignalFinished.wait();
}
void releaseResources()
{
CFRelease(refreshSignal);
refreshSignal = nullptr;
}
// This gets executed before Thread::start returns
void threadInit()
{
runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, refreshSignal, kCFRunLoopDefaultMode);
}
void threadRun()
{
CFRunLoopRef copyRunLoop = runLoop;
CFRunLoopRun();
CFRunLoopRemoveSource(copyRunLoop, refreshSignal, kCFRunLoopDefaultMode);
}
Result threadCreateFSEvent()
{
SC_TRY(runLoop);
CFArrayRef pathsArray = nullptr;
CFStringRef* watchedPaths =
(CFStringRef*)malloc(sizeof(CFStringRef) * ThreadRunnerDefinition::MaxWatchablePaths);
SC_TRY_MSG(watchedPaths != nullptr, "Cannot allocate paths");
// TODO: Loop to convert paths
auto deferFreeMalloc = MakeDeferred([&] { free(watchedPaths); });
size_t numAllocatedPaths = 0;
auto deferDeletePaths = MakeDeferred(
[&]
{
for (size_t idx = 0; idx < numAllocatedPaths; ++idx)
{
CFRelease(watchedPaths[idx]);
}
});
for (FolderWatcher* it = self->watchers.front; it != nullptr; it = it->next)
{
watchedPaths[numAllocatedPaths] =
CFStringCreateWithFileSystemRepresentation(nullptr, it->path.view().bytesIncludingTerminator());
if (not watchedPaths[numAllocatedPaths])
return Result::Error("CFStringCreateWithFileSystemRepresentation failed");
numAllocatedPaths++;
SC_TRY_MSG(numAllocatedPaths <= ThreadRunnerDefinition::MaxWatchablePaths,
"Exceeded max size of 1024 paths to watch");
}
if (numAllocatedPaths == 0)
{
return Result(true);
}
pathsArray = CFArrayCreate(nullptr, reinterpret_cast(watchedPaths),
static_cast(numAllocatedPaths), nullptr);
if (not pathsArray)
{
return Result::Error("CFArrayCreate failed");
}
deferDeletePaths.disarm();
deferFreeMalloc.disarm();
// Create Stream
constexpr CFAbsoluteTime watchLatency = 0.2;
constexpr FSEventStreamCreateFlags watchFlags =
kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagNoDefer;
FSEventStreamContext fsEventContext;
::memset(&fsEventContext, 0, sizeof(fsEventContext));
fsEventContext.info = this;
fsEventStream = FSEventStreamCreate(nullptr, //
&Internal::threadOnNewFSEvent, //
&fsEventContext, //
pathsArray, //
kFSEventStreamEventIdSinceNow, //
watchLatency, //
watchFlags);
SC_TRY_MSG(fsEventStream != nullptr, "FSEventStreamCreate failed");
#if SC_COMPILER_CLANG
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#endif
// Add it to runLoop
FSEventStreamScheduleWithRunLoop(fsEventStream, runLoop, kCFRunLoopDefaultMode);
#if SC_COMPILER_CLANG
#pragma clang diagnostic pop
#endif
if (not FSEventStreamStart(fsEventStream))
{
FSEventStreamInvalidate(fsEventStream);
FSEventStreamRelease(fsEventStream);
return Result::Error("FSEventStreamStart failed");
}
return Result(true);
}
void threadDestroyFSEvent()
{
FSEventStreamStop(fsEventStream);
FSEventStreamInvalidate(fsEventStream);
FSEventStreamRelease(fsEventStream);
fsEventStream = nullptr;
}
Result stopWatching(FolderWatcher& folderWatcher)
{
mutex.lock();
folderWatcher.parent->watchers.remove(folderWatcher);
folderWatcher.parent = nullptr;
mutex.unlock();
return startWatching(nullptr);
}
Result startWatching(FolderWatcher*)
{
if (not pollingThread.wasStarted())
{
SC_TRY(initThread());
}
wakeUpFSEventThread();
return signalReturnCode;
}
static constexpr int EVENT_MODIFIED = kFSEventStreamEventFlagItemChangeOwner | //
kFSEventStreamEventFlagItemFinderInfoMod | //
kFSEventStreamEventFlagItemInodeMetaMod | //
kFSEventStreamEventFlagItemModified | //
kFSEventStreamEventFlagItemXattrMod;
static constexpr int EVENT_RENAMED = kFSEventStreamEventFlagItemCreated | //
kFSEventStreamEventFlagItemRemoved | //
kFSEventStreamEventFlagItemRenamed;
static constexpr int EVENT_SYSTEM = kFSEventStreamEventFlagUserDropped | //
kFSEventStreamEventFlagKernelDropped | //
kFSEventStreamEventFlagEventIdsWrapped | //
kFSEventStreamEventFlagHistoryDone | //
kFSEventStreamEventFlagMount | //
kFSEventStreamEventFlagUnmount | //
kFSEventStreamEventFlagRootChanged;
static void threadOnNewFSEvent(ConstFSEventStreamRef streamRef, //
void* info, //
size_t numEvents, //
void* eventPaths, //
const FSEventStreamEventFlags* eventFlags, //
const FSEventStreamEventId* eventIds)
{
SC_COMPILER_UNUSED(streamRef);
SC_COMPILER_UNUSED(eventIds);
Internal& internal = *reinterpret_cast(info);
const char** paths = reinterpret_cast(eventPaths);
for (size_t idx = 0; idx < numEvents; ++idx)
{
const FSEventStreamEventFlags flags = eventFlags[idx];
if (flags & EVENT_SYSTEM)
continue;
const StringSpan path = StringSpan::fromNullTerminated(paths[idx], StringEncoding::Utf8);
bool sendNotification = true;
for (size_t prevIdx = 0; prevIdx < idx; ++prevIdx)
{
const StringSpan otherPath = StringSpan::fromNullTerminated(paths[prevIdx], StringEncoding::Utf8);
if (path == otherPath)
{
// Filter out multiple events for the same file in this batch
sendNotification = false;
break;
}
}
if (sendNotification)
{
notify(path, internal, flags);
}
if (internal.closing.load())
{
break;
}
}
}
static void notify(const StringSpan path, Internal& internal, const FSEventStreamEventFlags flags)
{
internal.notification.fullPath = path;
const bool isDirectory = flags & kFSEventStreamEventFlagItemIsDir;
const bool isRenamed = flags & EVENT_RENAMED;
const bool isModified = flags & EVENT_MODIFIED;
// FSEvent coalesces events in ways that makes it impossible to figure out exactly what happened
// see https://github.com/atom/watcher/blob/master/docs/macos.md
if (isRenamed)
{
internal.notification.operation = Operation::AddRemoveRename;
}
else
{
if (isModified or not isDirectory)
{
internal.notification.operation = Operation::Modified;
}
else
{
internal.notification.operation = Operation::AddRemoveRename;
}
}
internal.mutex.lock();
FolderWatcher* watcher = internal.self->watchers.front;
internal.mutex.unlock();
while (watcher != nullptr)
{
Span rootSpan;
const bool sliceStart = path.toCharSpan().sliceStartLength(0, watcher->path.view().sizeInBytes(), rootSpan);
internal.notification.basePath = {rootSpan, false, StringEncoding::Utf8};
if (sliceStart and watcher->path.view() == internal.notification.basePath)
{
Span relativeSpan;
(void)path.toCharSpan().sliceStartLength(
watcher->path.view().sizeInBytes(),
path.toCharSpan().sizeInBytes() - watcher->path.view().sizeInBytes(), relativeSpan);
if (relativeSpan.data()[0] == '/')
{
(void)relativeSpan.sliceStart(1, relativeSpan);
}
internal.notification.relativePath = {relativeSpan, true, path.getEncoding()};
internal.notification.basePath = watcher->path.view();
if (internal.eventLoopRunner)
{
EventLoopRunner& eventLoopRunner = *internal.eventLoopRunner;
internal.watcher = watcher;
const Result res = eventLoopRunner.appleWakeUpAndWait();
if (internal.closing.load())
{
break;
}
if (not res)
{
// TODO: print error for wakeup
}
}
else
{
watcher->notifyCallback(internal.notification);
}
}
// TODO: If someone removes this watcher in the callback we will skip notifying remaining ones.
internal.mutex.lock();
watcher = watcher->next;
internal.mutex.unlock();
}
}
static void threadExecuteRefresh(void* arg)
{
Internal& self = *static_cast(arg);
if (self.fsEventStream)
{
self.threadDestroyFSEvent();
}
if (self.closing.load())
{
CFRunLoopStop(self.runLoop);
self.runLoop = nullptr;
}
else
{
self.signalReturnCode = self.threadCreateFSEvent();
}
self.refreshSignalFinished.signal();
}
};
SC::Result SC::FileSystemWatcher::Notification::getFullPath(StringPath& path) const
{
return Result(path.assign(fullPath));
}
struct SC::FileSystemWatcher::ThreadRunnerInternal
{
};
struct SC::FileSystemWatcher::FolderWatcherInternal
{
};
void SC::FileSystemWatcher::asyncNotify(FolderWatcher*)
{
internal.get().watcher->notifyCallback(internal.get().notification);
}
#else
//----------------------------------------------------------------------------------------------------------------------
// FileSystemWatcher/Internal/FileSystemWatcherLinux.inl
//----------------------------------------------------------------------------------------------------------------------
// Copyright (c) Stefano Cristiano
// SPDX-License-Identifier: MIT
#include // opendir, readdir, closedir
#include // errno
#include // open, O_RDONLY, O_DIRECTORY
#include // strlen
#include // inotify
#include // fd_set / FD_ZERO
#include // fstat
#include // read
struct SC::FileSystemWatcher::FolderWatcherInternal
{
struct Pair
{
int32_t notifyID = 0;
int32_t nameOffset = 0;
bool operator==(Pair other) const { return notifyID == other.notifyID; }
};
Pair notifyHandles[FolderWatcherSizes::MaxNumberOfSubdirs];
size_t notifyHandlesCount = 0;
char relativePathsStorage[1024];
StringSpan::NativeWritable relativePaths;
FolderWatcher* parentEntry = nullptr; // We could in theory use SC_COMPILER_FIELD_OFFSET somehow to obtain it...
FolderWatcherInternal() { relativePaths.writableSpan = {relativePathsStorage}; }
};
struct SC::FileSystemWatcher::ThreadRunnerInternal
{
FSWThread thread;
FSWAtomicBool shouldStop;
// Allows unblocking the ::read() when stopping the watcher [0]=read end, [1]=write end
int shutdownPipe[2] = {-1, -1};
};
struct SC::FileSystemWatcher::Internal
{
FileSystemWatcher* self = nullptr;
EventLoopRunner* eventLoopRunner = nullptr;
ThreadRunnerInternal* threadingRunner = nullptr;
int notifyFd = -1; // inotify file descriptor
Result init(FileSystemWatcher& parent, ThreadRunner& runner)
{
self = &parent;
threadingRunner = &runner.get();
if (::pipe2(threadingRunner->shutdownPipe, O_CLOEXEC) == -1)
{
return Result::Error("pipe2 failed");
}
notifyFd = ::inotify_init1(IN_CLOEXEC);
return notifyFd != -1 ? Result(true) : Result::Error("inotify_init1 failed");
}
Result init(FileSystemWatcher& parent, EventLoopRunner& runner)
{
self = &parent;
eventLoopRunner = &runner;
notifyFd = ::inotify_init1(IN_NONBLOCK | IN_CLOEXEC);
if (notifyFd == -1)
{
return Result::Error("inotify_init1 failed");
}
eventLoopRunner->internalInit(parent, notifyFd);
return eventLoopRunner->linuxStartSharedFilePoll();
}
Result close()
{
if (eventLoopRunner)
{
SC_TRY(eventLoopRunner->linuxStopSharedFilePoll());
}
for (FolderWatcher* entry = self->watchers.front; entry != nullptr; entry = entry->next)
{
SC_TRY(stopWatching(*entry));
}
if (threadingRunner)
{
if (threadingRunner->thread.wasStarted())
{
threadingRunner->shouldStop.exchange(true);
// Write to shutdownPipe to unblock the ::select() in the dedicated thread
char dummy = 1;
while (::write(threadingRunner->shutdownPipe[1], &dummy, sizeof(dummy)) == -1)
{
if (errno != EINTR)
{
return Result::Error("write to shutdown pipe failed");
}
}
if (threadingRunner->shutdownPipe[0] != -1)
{
::close(threadingRunner->shutdownPipe[0]);
threadingRunner->shutdownPipe[0] = -1;
}
if (threadingRunner->shutdownPipe[1] != -1)
{
::close(threadingRunner->shutdownPipe[1]);
threadingRunner->shutdownPipe[1] = -1;
}
SC_TRY(threadingRunner->thread.join());
}
}
if (notifyFd != -1)
{
::close(notifyFd);
notifyFd = -1;
}
return Result(true);
}
Result stopWatching(FolderWatcher& folderWatcher)
{
folderWatcher.parent->watchers.remove(folderWatcher);
folderWatcher.parent = nullptr;
FolderWatcherInternal& folderInternal = folderWatcher.internal.get();
if (notifyFd == -1)
{
return Result::Error("invalid notifyFd");
}
for (size_t idx = 0; idx < folderInternal.notifyHandlesCount; ++idx)
{
const int res = ::inotify_rm_watch(notifyFd, folderInternal.notifyHandles[idx].notifyID);
SC_TRY_MSG(res != -1, "inotify_rm_watch");
}
folderInternal.notifyHandlesCount = 0; // Reset the count to zero
folderInternal.relativePaths.length = 0;
return Result(true);
}
static Result getSubFolderPath(StringPath& path, const StringPath& entryPath, const char* name,
FolderWatcherInternal& opaque, int notifyHandleId)
{
// Append '/' and the subdirectory name
path = entryPath;
const char* dirStart =
opaque.notifyHandles[notifyHandleId].nameOffset >= 0
? opaque.relativePaths.writableSpan.data() + opaque.notifyHandles[notifyHandleId].nameOffset
: "";
const StringSpan relativeDirectory = StringSpan::fromNullTerminated(dirStart, StringEncoding::Utf8);
const StringSpan relativeName = StringSpan::fromNullTerminated(name, StringEncoding::Utf8);
if (not relativeDirectory.isEmpty())
{
SC_TRY_MSG(path.append("/"), "Relative path too long");
SC_TRY_MSG(path.append(relativeDirectory), "Relative path too long");
}
SC_TRY_MSG(path.append("/"), "Relative path too long");
SC_TRY_MSG(path.append(relativeName), "Relative path too long");
return Result(true);
}
Result startWatching(FolderWatcher* entry)
{
// TODO: Add check for trying to watch folders already being watched or children of recursive watches
SC_TRY_MSG(entry->path.view().getEncoding() != StringEncoding::Utf16,
"FolderWatcher on Linux does not support UTF16 encoded paths. Use UTF8 or ASCII encoding instead.");
FolderWatcherInternal& opaque = entry->internal.get();
if (not entry->subFolderRelativePathsBuffer.empty())
{
opaque.relativePaths.writableSpan = entry->subFolderRelativePathsBuffer;
}
opaque.relativePaths.length = 0;
StringPath currentPath = entry->path;
int rootNotifyFd;
if (notifyFd == -1)
{
return Result::Error("invalid notifyFd");
}
rootNotifyFd = notifyFd;
constexpr int mask =
IN_ATTRIB | IN_CREATE | IN_MODIFY | IN_DELETE | IN_DELETE_SELF | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO;
const int newHandle = ::inotify_add_watch(rootNotifyFd, currentPath.view().bytesIncludingTerminator(), mask);
if (newHandle == -1)
{
return Result::Error("inotify_add_watch");
}
FolderWatcherInternal::Pair pair;
pair.notifyID = newHandle;
pair.nameOffset = -1;
SC_TRY_MSG(opaque.notifyHandlesCount < FolderWatcherSizes::MaxNumberOfSubdirs,
"Too many subdirectories being watched");
opaque.notifyHandles[opaque.notifyHandlesCount++] = pair;
// Watch all subfolders of current directory using a stack of file descriptors and names
constexpr int MaxStackDepth = FolderWatcherSizes::MaxNumberOfSubdirs;
struct DirStackEntry
{
int fd;
int notifyHandleId;
};
DirStackEntry stack[MaxStackDepth];
int stackSize = 0;
// Push the root directory onto the stack
DIR* rootDir = ::opendir(currentPath.view().bytesIncludingTerminator());
if (!rootDir)
{
return Result::Error("Failed to open root directory");
}
const int rootFd = ::dirfd(rootDir);
auto closeRootDir = MakeDeferred([rootDir] { ::closedir(rootDir); });
stack[stackSize++] = {rootFd, static_cast(opaque.notifyHandlesCount - 1)};
const size_t rootPathLength = entry->path.view().sizeInBytes();
// Clean the stack of fd in case any of the return Result::Error below is hit.
auto deferredCleanStack = MakeDeferred(
[&]
{
for (size_t idx = 0; idx < stackSize; ++idx)
{
::close(stack[idx].fd);
}
});
while (stackSize > 0)
{
DirStackEntry entryStack = stack[--stackSize];
DIR* dir = ::fdopendir(entryStack.fd);
if (!dir)
{
::close(entryStack.fd); // Prevent file descriptor leak if fdopendir fails
continue; // Cannot open directory, just skip it
}
auto closeDir = MakeDeferred([dir] { ::closedir(dir); });
struct dirent* subDirectory;
while ((subDirectory = ::readdir(dir)) != nullptr)
{
if (::strcmp(subDirectory->d_name, ".") == 0 || ::strcmp(subDirectory->d_name, "..") == 0)
{
continue;
}
SC_TRY(getSubFolderPath(currentPath, entry->path, subDirectory->d_name, opaque,
entryStack.notifyHandleId));
struct stat st;
if (::stat(currentPath.view().bytesIncludingTerminator(), &st) != 0)
{
continue;
}
if (S_ISDIR(st.st_mode))
{
const int newHandle =
::inotify_add_watch(rootNotifyFd, currentPath.view().bytesIncludingTerminator(), mask);
if (newHandle == -1)
{
(void)stopWatching(*entry);
return Result::Error("inotify_add_watch (subdirectory)");
}
if (stackSize < MaxStackDepth)
{
// Open the subdirectory and push onto the stack
const int subFd = ::open(currentPath.view().bytesIncludingTerminator(), O_RDONLY | O_DIRECTORY);
if (subFd != -1)
{
stack[stackSize].fd = subFd;
stack[stackSize].notifyHandleId = opaque.notifyHandlesCount;
stackSize++;
}
}
else
{
(void)stopWatching(*entry);
return Result::Error("Exceeded maximum stack depth for nested directories");
}
const char* relativePath = currentPath.view().bytesIncludingTerminator() + rootPathLength;
if (relativePath[0] == '/')
{
relativePath++;
}
pair.notifyID = newHandle;
pair.nameOffset = opaque.relativePaths.length == 0 ? 0 : opaque.relativePaths.length + 1;
StringSpan relativePathSpan = StringSpan::fromNullTerminated(relativePath, StringEncoding::Utf8);
SC_TRY_MSG(relativePathSpan.appendNullTerminatedTo(opaque.relativePaths, false),
"Not enough buffer space to hold sub-folders relative paths");
SC_TRY_MSG(opaque.notifyHandlesCount < FolderWatcherSizes::MaxNumberOfSubdirs,
"Too many subdirectories being watched");
opaque.notifyHandles[opaque.notifyHandlesCount++] = pair;
}
}
}
opaque.parentEntry = entry;
// Launch the thread that monitors the inotify watch if we're on thread runner
if (threadingRunner and not threadingRunner->thread.wasStarted())
{
threadingRunner->shouldStop.exchange(false);
Function threadFunction;
threadFunction.bind(*this);
SC_TRY(threadingRunner->thread.start(move(threadFunction)))
}
return Result(true);
}
void threadRun(FSWThread& thread)
{
thread.setThreadName(SC_NATIVE_STR("FileSystemWatcher"));
ThreadRunnerInternal& runner = *threadingRunner;
while (not runner.shouldStop.load())
{
// Setup a select fd_set to listen on both notifyFd and shutdownPipe simultaneously
fd_set fds;
FD_ZERO(&fds);
FD_SET(notifyFd, &fds);
FD_SET(runner.shutdownPipe[0], &fds);
const int maxFd = notifyFd > runner.shutdownPipe[0] ? notifyFd : runner.shutdownPipe[0];
int selectRes;
do
{
// Block until some events are received on the notifyFd or when shutdownPipe is written to
selectRes = ::select(maxFd + 1, &fds, nullptr, nullptr, nullptr);
} while (selectRes == -1 and errno == EINTR);
if (threadingRunner->shutdownPipe[0] == -1 or FD_ISSET(runner.shutdownPipe[0], &fds))
{
return; // Interrupted by write to shutdown pipe (from close())
}
// Here select has received data on notifyHandle
readAndNotify(notifyFd, self->watchers);
}
threadingRunner->shouldStop.exchange(false);
}
static void readAndNotify(const int notifyFd, WatcherLinkedList watchers)
{
int numReadBytes;
char inotifyBuffer[3 * 1024];
// TODO: Handle the case where kernel is sending more than 3kb of events.
do
{
numReadBytes = ::read(notifyFd, inotifyBuffer, sizeof(inotifyBuffer));
} while (numReadBytes == -1 and errno == EINTR);
Span actuallyRead = {inotifyBuffer, static_cast(numReadBytes)};
notifyWatchers(actuallyRead, watchers);
}
static void notifyWatchers(Span actuallyRead, WatcherLinkedList watchers)
{
const struct inotify_event* event = nullptr;
const struct inotify_event* prevEvent = nullptr;
// Loop through all inotify_event and find the associated FolderWatcher to notify
for (const char* iterator = actuallyRead.data(); //
iterator < actuallyRead.data() + actuallyRead.sizeInBytes(); //
iterator += sizeof(*event) + event->len)
{
event = reinterpret_cast(iterator);
for (const FolderWatcher* entry = watchers.front; entry != nullptr; entry = entry->next)
{
// Check if current FolderWatcher has a watcher matching the one from current event
FolderWatcherInternal::Pair pair{event->wd, 0};
for (size_t idx = 0; idx < entry->internal.get().notifyHandlesCount; ++idx)
{
if (entry->internal.get().notifyHandles[idx] == pair)
{
(void)notifySingleEvent(event, prevEvent, entry, idx);
prevEvent = event;
break;
}
}
}
}
}
[[nodiscard]] static Result notifySingleEvent(const struct inotify_event* event,
const struct inotify_event* prevEvent, const FolderWatcher* entry,
size_t foundIndex)
{
StringPath eventPath;
Notification notification;
notification.basePath = entry->path.view();
// 1. Compute relative Path
if (foundIndex == 0)
{
// Something changed in the original root folder being watched
notification.relativePath = StringSpan({event->name, ::strlen(event->name)}, true, StringEncoding::Utf8);
}
else
{
// Something changed in any of the sub folders of the original root folder being watched
const FolderWatcherInternal& internal = entry->internal.get();
const char* dirStart =
internal.notifyHandles[foundIndex].nameOffset >= 0
? internal.relativePaths.writableSpan.data() + internal.notifyHandles[foundIndex].nameOffset
: "";
const StringSpan relativeDirectory = StringSpan::fromNullTerminated(dirStart, StringEncoding::Utf8);
const StringSpan relativeName = StringSpan::fromNullTerminated(event->name, StringEncoding::Utf8);
SC_TRY_MSG(eventPath.assign(relativeDirectory), "Relative path too long");
SC_TRY_MSG(eventPath.append("/"), "Relative path too long");
SC_TRY_MSG(eventPath.append(relativeName), "Relative path too long");
notification.relativePath = eventPath.view();
}
// 2. Compute event Type
if (event->mask & (IN_ATTRIB | IN_MODIFY))
{
// Try to coalesce Modified after AddRemoveRename for consistency with the other backends
// I'm not really sure that Modified is consistently pushed after AddRemoveRename from Linux Kernel.
if (prevEvent != nullptr and (prevEvent->wd == event->wd))
{
return Result(false);
}
notification.operation = Operation::Modified;
}
if (event->mask & ~(IN_ATTRIB | IN_MODIFY))
{
notification.operation = Operation::AddRemoveRename;
}
// 3. Finally invoke user callback with the notification
entry->notifyCallback(notification);
return Result(true);
}
};
SC::Result SC::FileSystemWatcher::Notification::getFullPath(StringPath& buffer) const
{
SC_TRY_MSG(buffer.assign(basePath), "Buffer too small to hold full path");
SC_TRY_MSG(buffer.append("/"), "Buffer too small to hold full path");
SC_TRY_MSG(buffer.append(relativePath), "Buffer too small to hold full path");
return Result(true);
}
void SC::FileSystemWatcher::asyncNotify(FolderWatcher*)
{
internal.get().readAndNotify(internal.get().notifyFd, internal.get().self->watchers);
}
#endif
SC::FileSystemWatcher::FolderWatcher::FolderWatcher(Span buffer)
{
#if SC_PLATFORM_LINUX
subFolderRelativePathsBuffer = buffer;
#else
(void)buffer;
#endif
}
SC::Result SC::FileSystemWatcher::init(EventLoopRunner& runner) { return internal.get().init(*this, runner); }
SC::Result SC::FileSystemWatcher::init(ThreadRunner& runner) { return internal.get().init(*this, runner); }
SC::Result SC::FileSystemWatcher::close() { return internal.get().close(); }
SC::Result SC::FileSystemWatcher::watch(FolderWatcher& watcher, StringSpan path)
{
SC_TRY_MSG(watcher.parent == nullptr, "Watcher belongs to other FileSystemWatcher");
watcher.parent = this;
SC_TRY_MSG(watcher.path.assign(path), "FileSystemWatcher::watch - Error assigning path");
watchers.queueBack(watcher);
return internal.get().startWatching(&watcher);
}
SC::Result SC::FileSystemWatcher::FolderWatcher::stopWatching()
{
SC_TRY_MSG(parent != nullptr, "FolderWatcher already unwatched");
return parent->internal.get().stopWatching(*this);
}
void SC::FileSystemWatcher::FolderWatcher::setDebugName(const char* debugName) { (void)debugName; }
//! [OpaqueDefinition2Snippet]
template <>
void SC::FileSystemWatcher::InternalOpaque::construct(Handle& buffer)
{
placementNew(buffer.reinterpret_as