diff --git a/.teamcity/NativePlatformBuild.kt b/.teamcity/NativePlatformBuild.kt index 55822284..180a2dfb 100644 --- a/.teamcity/NativePlatformBuild.kt +++ b/.teamcity/NativePlatformBuild.kt @@ -33,7 +33,7 @@ open class NativePlatformBuild(agent: Agent, buildReceiptSource: Boolean = false steps { gradle { - tasks = "clean build -PagentName=${agent.name}${agent.extraTestTasks}${agent.allPublishTasks}" + tasks = "clean build -PagentName=${agent.name}${agent.allPublishTasks}" if (buildReceiptSource) { gradleParams = "-PignoreIncomingBuildReceipt" } diff --git a/.teamcity/NativePlatformCompatibilityTest.kt b/.teamcity/NativePlatformCompatibilityTest.kt index 83601f99..009b67b9 100644 --- a/.teamcity/NativePlatformCompatibilityTest.kt +++ b/.teamcity/NativePlatformCompatibilityTest.kt @@ -28,7 +28,7 @@ class NativePlatformCompatibilityTest(agent: Agent, buildDependencies: List, buildAndTest: List, buildReceiptSource: BuildType) : NativePlatformPublishSnapshot( releaseType, - listOf(":native-platform:uploadMain :file-events:uploadMain", ":test-app:uploadMain"), + listOf(":native-platform:uploadMain", ":test-app:uploadMain"), buildAndTest, buildReceiptSource, { diff --git a/.teamcity/extensions.kt b/.teamcity/extensions.kt index 740e561f..a04c2e17 100644 --- a/.teamcity/extensions.kt +++ b/.teamcity/extensions.kt @@ -44,7 +44,6 @@ const val buildReceipt = "build-receipt.properties" val archiveReports = listOf( "native-platform", - "file-events", "buildSrc", "test-app" ).joinToString("\n") { "$it/build/reports/** => $it/reports" } @@ -71,45 +70,24 @@ val agentsForAllNativePlatformJniPublications = listOf( Agent.AmazonLinuxAarch64, Agent.FreeBsdAmd64 ) -val agentsForAllFileEventsJniPublications = listOf( - Agent.UbuntuAmd64, - Agent.MacOsAmd64, - Agent.MacOsAarch64, - Agent.WindowsAmd64, - Agent.AmazonLinuxAarch64 -) val agentsForNcursesOnlyPublications = listOf( Agent.UbuntuAarch64, Agent.AmazonLinuxAmd64 ) -val Agent.nativePlatformPublishJniTask +val Agent.publishJniTasks get() = when (this) { in agentsForAllNativePlatformJniPublications -> " :native-platform:uploadJni" in agentsForNcursesOnlyPublications -> " :native-platform:uploadNcurses" else -> "" } -val Agent.fileEventsPublishJniTask - get() = when (this) { - in agentsForAllFileEventsJniPublications -> " :file-events:uploadJni" - else -> "" - } - -val Agent.publishJniTasks - get() = nativePlatformPublishJniTask + fileEventsPublishJniTask val Agent.allPublishTasks get() = when (this) { - agentForJavaPublication -> "$publishJniTasks :native-platform:uploadMain :file-events:uploadMain" + agentForJavaPublication -> "$publishJniTasks :native-platform:uploadMain" else -> publishJniTasks } -val Agent.extraTestTasks - get() = when (this) { - Agent.UbuntuAmd64 -> " :file-events:testBtrfs :file-events:testXfs" - else -> "" - } - fun BuildFeatures.lowerRequiredFreeDiskSpace() { freeDiskSpace { // Configure less than the default 3GB, since the disk of the agents is only 5GB big. diff --git a/file-events/build.gradle b/file-events/build.gradle deleted file mode 100755 index 842bd42d..00000000 --- a/file-events/build.gradle +++ /dev/null @@ -1,73 +0,0 @@ -plugins { - id 'groovy' - id 'cpp' - id 'gradlebuild.jni' -} - -nativeVersion { - versionClassPackageName = "net.rubygrapefruit.platform.internal.jni" - versionClassName = "FileEventsVersion" -} - -dependencies { - compileOnly 'com.google.code.findbugs:jsr305:3.0.2' - implementation project(':native-platform') - testImplementation testFixtures(project(":native-platform")) -} - -javadoc { - exclude '**/internal/**' -} - -model { - components { - nativePlatformFileEvents(NativeLibrarySpec) { - baseName 'native-platform-file-events' - $.platforms.each { p -> - targetPlatform p.name - } - binaries.all { - if (targetPlatform.operatingSystem.macOsX) { - cppCompiler.args "--std=c++17" // Enable C++17 - } else if (targetPlatform.operatingSystem.linux) { - cppCompiler.args "-std=c++11" // Enable C++11 - } else if (targetPlatform.operatingSystem.windows) { - cppCompiler.args "/std:c++17" // Won't hurt - } - - if (targetPlatform.operatingSystem.macOsX - || targetPlatform.operatingSystem.linux) { - cppCompiler.args "-g" // Produce debug output - cppCompiler.args "-pthread" // Force nicer threading - cppCompiler.args "-pedantic" // Disable non-standard things - cppCompiler.args "-Wall" // All warnings - cppCompiler.args "-Wextra" // Plus extra - cppCompiler.args "-Wformat=2" // Check printf format strings - cppCompiler.args "-Werror" // Warnings are errors - cppCompiler.args "-Wno-format-nonliteral" // Allow printf to have dynamic format string - cppCompiler.args "-Wno-unguarded-availability-new" // Newly introduced flags are not available on older macOS versions - linker.args "-pthread" - } else if (targetPlatform.operatingSystem.windows) { - cppCompiler.args "/DEBUG" // Produce debug output - cppCompiler.args "/permissive-" // Make compiler more standards compatible - cppCompiler.args "/EHsc" // Force exception handling mode - cppCompiler.args "/Zi" // Force PDB debugging - cppCompiler.args "/FS" // Force synchronous PDB writes - cppCompiler.args "/Zc:inline" // Hack - cppCompiler.args "/Zc:throwingNew" // Assume new throws on error - cppCompiler.args "/W3" // Enable lots of warnings, disbale individual warnings with /WD - cppCompiler.args "/WX" // Warnings are errors - cppCompiler.args "/D_SILENCE_CXX17_CODECVT_HEADER_DEPRECATION_WARNING" - // Don't issue warnings for wstring_convert in generic_fsnotifier.cpp - linker.args "/DEBUG:FULL" // Generate all PDBs - } - } - sources { - cpp { - source.srcDirs = ['src/file-events/cpp'] - exportedHeaders.srcDirs = ['src/file-events/headers'] - } - } - } - } -} diff --git a/file-events/src/file-events/cpp/apple_fsnotifier.cpp b/file-events/src/file-events/cpp/apple_fsnotifier.cpp deleted file mode 100644 index 1641bc3f..00000000 --- a/file-events/src/file-events/cpp/apple_fsnotifier.cpp +++ /dev/null @@ -1,256 +0,0 @@ -#if defined(__APPLE__) - -#include "apple_fsnotifier.h" - -using namespace std; - -WatchPoint::WatchPoint(Server* server, dispatch_queue_t dispatchQueue, const u16string& path, long latencyInMillis) { - CFStringRef cfPath = CFStringCreateWithCharacters(NULL, (UniChar*) path.c_str(), path.length()); - if (cfPath == nullptr) { - throw FileWatcherException("Could not allocate CFString for path", path); - } - CFMutableArrayRef pathArray = CFArrayCreateMutable(NULL, 1, &kCFTypeArrayCallBacks); - if (pathArray == NULL) { - CFRelease(cfPath); - throw FileWatcherException("Could not allocate array to store root to watch", path); - } - CFArrayAppendValue(pathArray, cfPath); - - FSEventStreamContext context = { - 0, // version, must be 0 - (void*) server, // info - NULL, // retain - NULL, // release - NULL // copyDescription - }; - - FSEventStreamRef watcherStream = FSEventStreamCreate( - NULL, - &handleEventsCallback, - &context, - pathArray, - kFSEventStreamEventIdSinceNow, - latencyInMillis / 1000.0, - kFSEventStreamCreateFlagNoDefer | kFSEventStreamCreateFlagFileEvents | kFSEventStreamCreateFlagWatchRoot); - - CFRelease(pathArray); - CFRelease(cfPath); - - if (watcherStream == NULL) { - throw FileWatcherException("Couldn't add watch", path); - } - - FSEventStreamSetDispatchQueue(watcherStream, dispatchQueue); - - if (!FSEventStreamStart(watcherStream)) { - FSEventStreamInvalidate(watcherStream); - FSEventStreamRelease(watcherStream); - throw FileWatcherException("Could not start the FSEvents stream", path); - } - - this->watcherStream = watcherStream; -} - -WatchPoint::~WatchPoint() { - // Reading the Apple docs it seems we should call FSEventStreamFlushSync() here. - // But doing so produces this log: - // - // 2020-02-17 23:02 java[50430] (FSEvents.framework) FSEventStreamFlushSync(): failed assertion '(SInt64)last_id > 0LL' - // - // According to this comment we should not use flush at all, and it's probably broken: - // https://github.com/nodejs/node/issues/854#issuecomment-294892950 - // As the comment mentions, even Watchman doesn't flush: - // https://github.com/facebook/watchman/blob/b397e00cf566f361282a456122eef4e909f26182/watcher/fsevents.cpp#L276-L285 - // FSEventStreamFlushSync(watcherStream); - FSEventStreamStop(watcherStream); - FSEventStreamInvalidate(watcherStream); - FSEventStreamRelease(watcherStream); -} - -// -// Server -// - -Server::Server(JNIEnv* env, jobject watcherCallback, long latencyInMillis) - : AbstractServer(env, watcherCallback) - , latencyInMillis(latencyInMillis) - , dispatchQueue(dispatch_queue_create("org.gradle.vfs", DISPATCH_QUEUE_SERIAL)) { -} - -Server::~Server() { - dispatch_release(dispatchQueue); -} - -static void handleEventsCallback( - ConstFSEventStreamRef, - void* clientCallBackInfo, - size_t numEvents, - void* eventPaths, - const FSEventStreamEventFlags eventFlags[], - const FSEventStreamEventId eventIds[]) { - Server* server = (Server*) clientCallBackInfo; - server->handleEvents(numEvents, (char**) eventPaths, eventFlags, eventIds); -} - -void Server::handleEvents( - size_t numEvents, - char** eventPaths, - const FSEventStreamEventFlags eventFlags[], - const FSEventStreamEventId eventIds[]) { - try { - for (size_t i = 0; i < numEvents; i++) { - // This code runs on an arbitrary thread, so we can't pass it back to Java from here, - // as the JNIEnv is not available on this thread. We pass it to the Java run loop thread - // using our own queue instead. - eventQueue.enqueue(FileEvent { - eventPaths[i], - eventFlags[i], - eventIds[i] }); - } - } catch (const exception& ex) { - eventQueue.enqueue(ErrorEvent { ex.what() }); - } -} - -void Server::initializeRunLoop() { - // We don't need to do anything here, as we're using a dispatch queue instead of a run loop. -} - -void Server::runLoop() { - JNIEnv* env = getThreadEnv(); - while (true) { - QueueItem item = eventQueue.dequeue(); - if (holds_alternative(item)) { - break; - } - if (holds_alternative(item)) { - FileEvent event = get(item); - handleEvent(env, event.eventPath.c_str(), event.eventFlags, event.eventId); - } else if (holds_alternative(item)) { - ErrorEvent event = get(item); - reportFailure(env, event.message.c_str()); - } - } -} - -void doNothing(void*) { - // Dummy function used to test the dispatch queue being emptied -} - -void Server::shutdownRunLoop() { - // Make sure we stop watching before we stop the run loop - watchPoints.clear(); - // This waits for the dispatch queue to empty completely; without it we might get events - // after the server has been destroyed. - dispatch_sync_f(dispatchQueue, nullptr, doNothing); - eventQueue.enqueue(PoisonPill()); -} - -/** - * List of events ignored by our implementation. - * Anything not ignored here should be handled. - * If macOS later adds more flags, we'll report those as unknown events this way. - */ -static constexpr FSEventStreamEventFlags IGNORED_FLAGS = kFSEventStreamCreateFlagNone - // | kFSEventStreamEventFlagMustScanSubDirs - | kFSEventStreamEventFlagUserDropped - | kFSEventStreamEventFlagKernelDropped - | kFSEventStreamEventFlagEventIdsWrapped - | kFSEventStreamEventFlagHistoryDone - // | kFSEventStreamEventFlagRootChanged - // | kFSEventStreamEventFlagMount - // | kFSEventStreamEventFlagUnmount - // | kFSEventStreamEventFlagItemCreated - // | kFSEventStreamEventFlagItemRemoved - // | kFSEventStreamEventFlagItemInodeMetaMod - // | kFSEventStreamEventFlagItemRenamed - // | kFSEventStreamEventFlagItemModified - // | kFSEventStreamEventFlagItemFinderInfoMod - // | kFSEventStreamEventFlagItemChangeOwner - // | kFSEventStreamEventFlagItemXattrMod - | kFSEventStreamEventFlagItemIsFile - | kFSEventStreamEventFlagItemIsDir - | kFSEventStreamEventFlagItemIsSymlink - | kFSEventStreamEventFlagOwnEvent - | kFSEventStreamEventFlagItemIsHardlink - | kFSEventStreamEventFlagItemIsLastHardlink - | kFSEventStreamEventFlagItemCloned; - -void Server::handleEvent(JNIEnv* env, const char* path, FSEventStreamEventFlags flags, FSEventStreamEventId eventId) { - logToJava(LogLevel::FINE, "Event flags: 0x%x (ID %d) for '%s'", flags, eventId, path); - - u16string pathStr = utf8ToUtf16String(path); - - if ((flags & ~IGNORED_FLAGS) == kFSEventStreamCreateFlagNone) { - logToJava(LogLevel::FINE, "Ignoring event 0x%x (ID %d) for '%s'", flags, eventId, path); - return; - } - - if (IS_SET(flags, kFSEventStreamEventFlagMustScanSubDirs)) { - reportOverflow(env, pathStr); - return; - } - - ChangeType type; - if (IS_SET(flags, - kFSEventStreamEventFlagRootChanged - | kFSEventStreamEventFlagMount - | kFSEventStreamEventFlagUnmount)) { - type = ChangeType::INVALIDATED; - } else if (IS_SET(flags, kFSEventStreamEventFlagItemRenamed)) { - if (IS_SET(flags, kFSEventStreamEventFlagItemCreated)) { - type = ChangeType::REMOVED; - } else { - type = ChangeType::CREATED; - } - } else if (IS_SET(flags, kFSEventStreamEventFlagItemModified)) { - type = ChangeType::MODIFIED; - } else if (IS_SET(flags, kFSEventStreamEventFlagItemRemoved)) { - type = ChangeType::REMOVED; - } else if (IS_SET(flags, - kFSEventStreamEventFlagItemInodeMetaMod // file locked - | kFSEventStreamEventFlagItemFinderInfoMod - | kFSEventStreamEventFlagItemChangeOwner - | kFSEventStreamEventFlagItemXattrMod)) { - type = ChangeType::MODIFIED; - } else if (IS_SET(flags, kFSEventStreamEventFlagItemCreated)) { - type = ChangeType::CREATED; - } else { - logToJava(LogLevel::WARNING, "Unknown event 0x%x (ID %d) for '%s'", flags, eventId, path); - reportUnknownEvent(env, pathStr); - return; - } - - reportChangeEvent(env, type, pathStr); -} - -void Server::registerPaths(const vector& paths) { - unique_lock lock(mutationMutex); - for (auto& path : paths) { - if (watchPoints.find(path) != watchPoints.end()) { - throw FileWatcherException("Already watching path", path); - } - watchPoints.emplace(piecewise_construct, - forward_as_tuple(path), - forward_as_tuple(this, dispatchQueue, path, latencyInMillis)); - } -} - -bool Server::unregisterPaths(const vector& paths) { - unique_lock lock(mutationMutex); - bool success = true; - for (auto& path : paths) { - if (watchPoints.erase(path) == 0) { - logToJava(LogLevel::INFO, "Path is not watched: %s", utf16ToUtf8String(path).c_str()); - success = false; - } - } - return success; -} - -JNIEXPORT jobject JNICALL -Java_net_rubygrapefruit_platform_internal_jni_OsxFileEventFunctions_startWatcher0(JNIEnv* env, jclass, long latencyInMillis, jobject javaCallback) { - return wrapServer(env, new Server(env, javaCallback, latencyInMillis)); -} - -#endif diff --git a/file-events/src/file-events/cpp/file-events-version.cpp b/file-events/src/file-events/cpp/file-events-version.cpp deleted file mode 100644 index 8eca735f..00000000 --- a/file-events/src/file-events/cpp/file-events-version.cpp +++ /dev/null @@ -1,7 +0,0 @@ -#include "native_platform_version.h" -#include "net_rubygrapefruit_platform_internal_jni_AbstractNativeFileEventFunctions.h" - -JNIEXPORT jstring JNICALL -Java_net_rubygrapefruit_platform_internal_jni_AbstractNativeFileEventFunctions_getVersion0(JNIEnv* env, jclass) { - return env->NewStringUTF(NATIVE_VERSION); -} diff --git a/file-events/src/file-events/cpp/generic_fsnotifier.cpp b/file-events/src/file-events/cpp/generic_fsnotifier.cpp deleted file mode 100644 index 8db4520c..00000000 --- a/file-events/src/file-events/cpp/generic_fsnotifier.cpp +++ /dev/null @@ -1,189 +0,0 @@ -#include - -#include "generic_fsnotifier.h" - -InsufficientResourcesFileWatcherException::InsufficientResourcesFileWatcherException(const string& message) - : FileWatcherException(message) { -} - -AbstractServer::AbstractServer(JNIEnv* env, jobject watcherCallback) - : JniSupport(env) - , watcherCallback(env, watcherCallback) { - jclass callbackClass = env->GetObjectClass(watcherCallback); - this->watcherReportChangeEventMethod = env->GetMethodID(callbackClass, "reportChangeEvent", "(ILjava/lang/String;)V"); - this->watcherReportUnknownEventMethod = env->GetMethodID(callbackClass, "reportUnknownEvent", "(Ljava/lang/String;)V"); - this->watcherReportOverflowMethod = env->GetMethodID(callbackClass, "reportOverflow", "(Ljava/lang/String;)V"); - this->watcherReportFailureMethod = env->GetMethodID(callbackClass, "reportFailure", "(Ljava/lang/Throwable;)V"); -} - -AbstractServer::~AbstractServer() { -} - -void AbstractServer::reportChangeEvent(JNIEnv* env, ChangeType type, const u16string& path) { - jstring javaPath = env->NewString((jchar*) path.c_str(), (jsize) path.length()); - env->CallVoidMethod(watcherCallback.get(), watcherReportChangeEventMethod, type, javaPath); - env->DeleteLocalRef(javaPath); - getJavaExceptionAndPrintStacktrace(env); -} - -void AbstractServer::reportUnknownEvent(JNIEnv* env, const u16string& path) { - jstring javaPath = env->NewString((jchar*) path.c_str(), (jsize) path.length()); - env->CallVoidMethod(watcherCallback.get(), watcherReportUnknownEventMethod, javaPath); - env->DeleteLocalRef(javaPath); - getJavaExceptionAndPrintStacktrace(env); -} - -void AbstractServer::reportOverflow(JNIEnv* env, const u16string& path) { - logToJava(LogLevel::INFO, "Detected overflow for %s", utf16ToUtf8String(path).c_str()); - jstring javaPath = env->NewString((jchar*) path.c_str(), (jsize) path.length()); - env->CallVoidMethod(watcherCallback.get(), watcherReportOverflowMethod, javaPath); - env->DeleteLocalRef(javaPath); - getJavaExceptionAndPrintStacktrace(env); -} - -void AbstractServer::reportFailure(JNIEnv* env, const exception& exception) { - reportFailure(env, exception.what()); -} - -void AbstractServer::reportFailure(JNIEnv* env, const char* message) { - u16string utf16Message = utf8ToUtf16String(message); - jstring javaMessage = env->NewString((jchar*) utf16Message.c_str(), (jsize) utf16Message.length()); - jmethodID constructor = env->GetMethodID(nativePlatformJniConstants->nativeExceptionClass.get(), "", "(Ljava/lang/String;)V"); - jobject javaException = env->NewObject(nativePlatformJniConstants->nativeExceptionClass.get(), constructor, javaMessage); - env->CallVoidMethod(watcherCallback.get(), watcherReportFailureMethod, javaException); - env->DeleteLocalRef(javaMessage); - env->DeleteLocalRef(javaException); - getJavaExceptionAndPrintStacktrace(env); -} - -AbstractServer* getServer(JNIEnv* env, jobject javaServer) { - AbstractServer* server = (AbstractServer*) env->GetDirectBufferAddress(javaServer); - if (server == NULL) { - throw FileWatcherException("Closed already"); - } - return server; -} - -jobject rethrowAsJavaException(JNIEnv* env, const exception& e) { - logToJava(LogLevel::SEVERE, "Caught exception: %s", e.what()); - return rethrowAsJavaException(env, e, nativePlatformJniConstants->nativeExceptionClass.get()); -} - -jobject rethrowAsJavaException(JNIEnv* env, const exception& e, jclass exceptionClass) { - jint ret = env->ThrowNew(exceptionClass, e.what()); - if (ret != 0) { - cerr << "JNI ThrowNew returned %d when rethrowing native exception: " << ret << endl; - } - return NULL; -} - -jobject wrapServer(JNIEnv* env, AbstractServer* server) { - return env->NewDirectByteBuffer(server, sizeof(server)); -} - -void AbstractServer::executeRunLoop(JNIEnv* env) { - try { - runLoop(); - } catch (const exception& ex) { - rethrowAsJavaException(env, ex); - } - unique_lock terminationLock(terminationMutex); - terminated = true; - terminationVariable.notify_all(); -} - -bool AbstractServer::awaitTermination(long timeoutInMillis) { - unique_lock terminationLock(terminationMutex); - if (terminated) { - return true; - } - auto status = terminationVariable.wait_for(terminationLock, chrono::milliseconds(timeoutInMillis)); - bool success = status != cv_status::timeout; - return success; -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_AbstractNativeFileEventFunctions_00024NativeFileWatcher_initializeRunLoop0(JNIEnv* env, jobject, jobject javaServer) { - try { - AbstractServer* server = getServer(env, javaServer); - server->initializeRunLoop(); - } catch (const exception& e) { - rethrowAsJavaException(env, e); - } -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_AbstractNativeFileEventFunctions_00024NativeFileWatcher_executeRunLoop0(JNIEnv* env, jobject, jobject javaServer) { - try { - AbstractServer* server = getServer(env, javaServer); - server->executeRunLoop(env); - } catch (const exception& e) { - rethrowAsJavaException(env, e); - } -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_AbstractNativeFileEventFunctions_00024NativeFileWatcher_startWatching0(JNIEnv* env, jobject, jobject javaServer, jobjectArray javaPaths) { - try { - AbstractServer* server = getServer(env, javaServer); - vector paths; - javaToUtf16StringArray(env, javaPaths, paths); - server->registerPaths(paths); - } catch (const JavaExceptionThrownException&) { - // Ignore, the Java exception has already been thrown. - } catch (const exception& e) { - rethrowAsJavaException(env, e); - } -} - -JNIEXPORT jboolean JNICALL -Java_net_rubygrapefruit_platform_internal_jni_AbstractNativeFileEventFunctions_00024NativeFileWatcher_stopWatching0(JNIEnv* env, jobject, jobject javaServer, jobjectArray javaPaths) { - try { - AbstractServer* server = getServer(env, javaServer); - vector paths; - javaToUtf16StringArray(env, javaPaths, paths); - return server->unregisterPaths(paths); - } catch (const exception& e) { - rethrowAsJavaException(env, e); - return false; - } -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_AbstractNativeFileEventFunctions_00024NativeFileWatcher_shutdown0(JNIEnv* env, jobject, jobject javaServer) { - try { - AbstractServer* server = getServer(env, javaServer); - server->shutdownRunLoop(); - } catch (const exception& e) { - rethrowAsJavaException(env, e); - } -} - -JNIEXPORT jboolean JNICALL -Java_net_rubygrapefruit_platform_internal_jni_AbstractNativeFileEventFunctions_00024NativeFileWatcher_awaitTermination0(JNIEnv* env, jobject, jobject javaServer, jlong timeoutInMillis) { - try { - AbstractServer* server = getServer(env, javaServer); - bool successful = server->awaitTermination((long) timeoutInMillis); - if (successful) { - delete server; - } - return successful; - } catch (const exception& e) { - rethrowAsJavaException(env, e); - return false; - } -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_AbstractNativeFileEventFunctions_invalidateLogLevelCache0(JNIEnv* env, jobject) { - try { - logging->invalidateLogLevelCache(); - } catch (const exception& e) { - rethrowAsJavaException(env, e); - } -} - -NativePlatformJniConstants::NativePlatformJniConstants(JavaVM* jvm) - : JniSupport(jvm) - , nativeExceptionClass(getThreadEnv(), "net/rubygrapefruit/platform/NativeException") { -} diff --git a/file-events/src/file-events/cpp/jni_support.cpp b/file-events/src/file-events/cpp/jni_support.cpp deleted file mode 100644 index 02e5cb05..00000000 --- a/file-events/src/file-events/cpp/jni_support.cpp +++ /dev/null @@ -1,141 +0,0 @@ -#include -#include -#include -#include - -#include "jni_support.h" - -using namespace std; - -JavaVM* getJavaVm(JNIEnv* env) { - JavaVM* jvm; - int jvmStatus = env->GetJavaVM(&jvm); - if (jvmStatus != 0) { - throw runtime_error(string("Could not get jvm instance: ") + to_string(jvmStatus)); - } - return jvm; -} - -JavaExceptionThrownException::JavaExceptionThrownException() - : runtime_error("Java exception thrown from native code") { -} - -JniSupport::JniSupport(JavaVM* jvm) - : jvm(jvm) { -} - -JniSupport::JniSupport(JNIEnv* env) - : jvm(getJavaVm(env)) { -} - -JNIEnv* JniSupport::getThreadEnv() { - JNIEnv* env; - jint ret = jvm->GetEnv((void**) &env, JNI_VERSION_1_6); - if (ret != JNI_OK) { - throw runtime_error(string("Failed to get JNI env for current thread: ") + to_string(ret)); - } - return env; -} - -jthrowable JniSupport::getJavaExceptionAndPrintStacktrace(JNIEnv* env) { - jthrowable exception = env->ExceptionOccurred(); - if (exception != nullptr) { - env->ExceptionDescribe(); - env->ExceptionClear(); - } - return exception; -} - -void JniSupport::rethrowJavaException(JNIEnv* env) { - jthrowable exception = getJavaExceptionAndPrintStacktrace(env); - if (exception != nullptr) { - env->ExceptionDescribe(); - env->ExceptionClear(); - - jclass exceptionClass = env->GetObjectClass(exception); - jmethodID getClassName = env->GetMethodID(baseJniConstants->classClass.get(), "getName", "()Ljava/lang/String;"); - jstring javaExceptionType = (jstring) env->CallObjectMethod(exceptionClass, getClassName); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - throw runtime_error("Couldn't get exception type"); - } - string exceptionType = javaToUtf8String(env, javaExceptionType); - env->DeleteLocalRef(javaExceptionType); - - jmethodID getMessage = env->GetMethodID(exceptionClass, "getMessage", "()Ljava/lang/String;"); - jstring javaMessage = (jstring) env->CallObjectMethod(exception, getMessage); - if (env->ExceptionCheck()) { - env->ExceptionDescribe(); - throw runtime_error("Couldn't get exception message"); - } - env->DeleteLocalRef(exceptionClass); - - string message = javaMessage == nullptr - ? "Caught " + exceptionType - : "Caught " + exceptionType + " with message: " + javaToUtf8String(env, javaMessage); - if (javaMessage != nullptr) { - env->DeleteLocalRef(javaMessage); - } - - env->DeleteLocalRef(exception); - - throw runtime_error(message); - } -} - -void JniSupport::throwNativeExceptionWhenJavaExceptionOccurred(JNIEnv* env) { - jthrowable exception = env->ExceptionOccurred(); - if (exception != nullptr) { - throw JavaExceptionThrownException(); - } -} - -BaseJniConstants::BaseJniConstants(JavaVM* jvm) - : JniSupport(jvm) - , classClass(getThreadEnv(), "java/lang/Class") { -} - -string javaToUtf8String(JNIEnv* env, jstring javaString) { - return utf16ToUtf8String(javaToUtf16String(env, javaString)); -} - -u16string javaToUtf16String(JNIEnv* env, jstring javaString) { - jsize length = env->GetStringLength(javaString); - const jchar* javaChars = env->GetStringCritical(javaString, nullptr); - if (javaChars == NULL) { - throw runtime_error("Could not get Java string character"); - } - u16string path((char16_t*) javaChars, length); - env->ReleaseStringCritical(javaString, javaChars); - return path; -} - -void javaToUtf16StringArray(JNIEnv* env, jobjectArray javaStrings, vector& strings) { - int count = env->GetArrayLength(javaStrings); - strings.reserve(count); - for (int i = 0; i < count; i++) { - jstring javaString = reinterpret_cast(env->GetObjectArrayElement(javaStrings, i)); - u16string string = javaToUtf16String(env, javaString); - env->DeleteLocalRef(javaString); - strings.push_back(string); - } -} - -#if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -#endif - -u16string utf8ToUtf16String(const char* string) { - wstring_convert>, char16_t> conv16; - return conv16.from_bytes(string); -} - -string utf16ToUtf8String(const u16string& string) { - wstring_convert>, char16_t> conv16; - return conv16.to_bytes(string); -} - -#if defined(__GNUC__) || defined(__clang__) -#pragma GCC diagnostic pop -#endif diff --git a/file-events/src/file-events/cpp/linux_fsnotifier.cpp b/file-events/src/file-events/cpp/linux_fsnotifier.cpp deleted file mode 100644 index 27f9f4da..00000000 --- a/file-events/src/file-events/cpp/linux_fsnotifier.cpp +++ /dev/null @@ -1,411 +0,0 @@ -#ifdef __linux__ - -#include -#include -#include -#include -#include -#include - -#include "linux_fsnotifier.h" - -#define EVENT_BUFFER_SIZE (16 * 1024) - -#define EVENT_MASK (IN_CREATE | IN_DELETE | IN_DELETE_SELF | IN_EXCL_UNLINK | IN_MODIFY | IN_MOVE_SELF | IN_MOVED_FROM | IN_MOVED_TO | IN_ONLYDIR) - -InotifyInstanceLimitTooLowException::InotifyInstanceLimitTooLowException() - : InsufficientResourcesFileWatcherException("Inotify instance limit too low") { -} - -InotifyWatchesLimitTooLowException::InotifyWatchesLimitTooLowException() - : InsufficientResourcesFileWatcherException("Inotify watches limit too low") { -} - -WatchPoint::WatchPoint(const u16string& path, shared_ptr inotify, int watchDescriptor, ino_t inode) - : status(WatchPointStatus::LISTENING) - , watchDescriptor(watchDescriptor) - , inotify(inotify) - , path(path) - , inode(inode) { -} - -CancelResult WatchPoint::cancel() { - if (status == WatchPointStatus::CANCELLED) { - return CancelResult::ALREADY_CANCELLED; - } - status = WatchPointStatus::CANCELLED; - if (inotify_rm_watch(inotify->fd, watchDescriptor) != 0) { - switch (errno) { - case EINVAL: - logToJava(LogLevel::INFO, "Couldn't stop watching %s (probably because the directory was removed)", utf16ToUtf8String(path).c_str()); - return CancelResult::NOT_CANCELLED; - break; - default: - throw FileWatcherException("Couldn't stop watching", path, errno); - } - } - return CancelResult::CANCELLED; -} - -Inotify::Inotify() - : fd(inotify_init1(IN_CLOEXEC | IN_NONBLOCK)) { - if (fd == -1) { - if (errno == EMFILE) { - throw InotifyInstanceLimitTooLowException(); - } - throw FileWatcherException("Couldn't register inotify handle", errno); - } -} - -Inotify::~Inotify() { - close(fd); -} - -ShutdownEvent::ShutdownEvent() - : fd(eventfd(0, 0)) { - if (fd == -1) { - throw FileWatcherException("Couldn't register event source", errno); - } -} -ShutdownEvent::~ShutdownEvent() { - close(fd); -} - -void ShutdownEvent::trigger() const { - const uint64_t increment = 1; - write(fd, &increment, sizeof(increment)); -} - -void ShutdownEvent::consume() const { - uint64_t counter; - ssize_t bytesRead = read(fd, &counter, sizeof(counter)); - if (bytesRead == -1) { - throw FileWatcherException("Couldn't read from termination event notifier", errno); - } -} - -Server::Server(JNIEnv* env, jobject watcherCallback) - : AbstractServer(env, watcherCallback) - , inotify(new Inotify()) { - buffer.reserve(EVENT_BUFFER_SIZE); - jclass listClass = env->FindClass("java/util/List"); - this->listAddMethod = env->GetMethodID(listClass, "add", "(Ljava/lang/Object;)Z"); -} - -void Server::initializeRunLoop() { -} - -void Server::shutdownRunLoop() { - shutdownEvent.trigger(); -} - -void Server::runLoop() { - int forever = numeric_limits::max(); - - while (!shouldTerminate) { - processQueues(forever); - } - - // No need to clean up watch points, they will be cancelled - // and closed when the Inotify destructs -} - -void Server::processQueues(int timeout) { - struct pollfd fds[2]; - fds[0].fd = shutdownEvent.fd; - fds[1].fd = inotify->fd; - fds[0].events = POLLIN; - fds[1].events = POLLIN; - - int ret = poll(fds, 2, timeout); - if (ret == -1) { - throw FileWatcherException("Couldn't poll for events", errno); - } - - if (IS_SET(fds[0].revents, POLLIN)) { - shutdownEvent.consume(); - // Ignore counter, we only care about the notification itself - shouldTerminate = true; - return; - } - - if (IS_SET(fds[1].revents, POLLIN)) { - try { - handleEvents(); - } catch (const exception& ex) { - reportFailure(getThreadEnv(), ex); - } - } -} - -void Server::handleEvents() { - unsigned int available; - ioctl(inotify->fd, FIONREAD, &available); - - while (available > 0) { - ssize_t bytesRead = read(inotify->fd, &buffer[0], buffer.capacity()); - - switch (bytesRead) { - case -1: - if (errno == EAGAIN) { - // For a non-blocking read, we receive EAGAIN here if there is nothing to read. - // This may happen when the inotify is already closed. - return; - } else { - throw FileWatcherException("Couldn't read from inotify", errno); - } - break; - case 0: - throw FileWatcherException("EOF reading from inotify", errno); - break; - default: - // Handle events - unique_lock lock(mutationMutex); - JNIEnv* env = getThreadEnv(); - logToJava(LogLevel::FINE, "Processing %d bytes worth of events", bytesRead); - int index = 0; - int count = 0; - while (index < bytesRead) { - const struct inotify_event* event = (struct inotify_event*) &buffer[index]; - handleEvent(env, event); - index += sizeof(struct inotify_event) + event->len; - count++; - } - logToJava(LogLevel::FINE, "Processed %d events", count); - break; - } - available -= bytesRead; - } -} - -void Server::handleEvent(JNIEnv* env, const inotify_event* event) { - uint32_t mask = event->mask; - const char* eventName = (event->len == 0) - ? "" - : event->name; - logToJava(LogLevel::FINE, "Event mask: 0x%x for %s (wd = %d, cookie = 0x%x, len = %d)", mask, eventName, event->wd, event->cookie, event->len); - if (IS_SET(mask, IN_UNMOUNT)) { - return; - } - - // Overflow received, handle gracefully - if (IS_SET(mask, IN_Q_OVERFLOW)) { - for (auto it : watchPoints) { - auto path = it.first; - reportOverflow(env, path); - } - return; - } - - auto iWatchRoot = watchRoots.find(event->wd); - if (iWatchRoot == watchRoots.end()) { - auto iRecentlyUnregisteredWatchPoint = recentlyUnregisteredWatchRoots.find(event->wd); - if (iRecentlyUnregisteredWatchPoint == recentlyUnregisteredWatchRoots.end()) { - logToJava(LogLevel::INFO, "Received event for unknown watch descriptor %d", event->wd); - } else { - // We've removed this via unregisterPath() not long ago - auto& path = iRecentlyUnregisteredWatchPoint->second; - if (IS_SET(mask, IN_IGNORED)) { - logToJava(LogLevel::FINE, "Finished watching recently unregistered watch point '%s' (wd = %d)", - utf16ToUtf8String(path).c_str(), event->wd); - recentlyUnregisteredWatchRoots.erase(iRecentlyUnregisteredWatchPoint); - } else { - logToJava(LogLevel::FINE, "Ignoring incoming events for recently removed watch descriptor for '%s' (wd = %d)", - utf16ToUtf8String(path).c_str(), event->wd); - } - } - return; - } - - auto path = iWatchRoot->second; - auto& watchPoint = watchPoints.at(path); - - if (IS_SET(mask, IN_IGNORED)) { - // Finished with watch point - logToJava(LogLevel::FINE, "Finished watching still registered '%s' (wd = %d)", - utf16ToUtf8String(path).c_str(), event->wd); - watchRoots.erase(event->wd); - watchPoints.erase(path); - return; - } - - if (watchPoint.status != WatchPointStatus::LISTENING) { - logToJava(LogLevel::FINE, "Ignoring incoming events for %s as watch-point is not listening (status = %d)", - utf16ToUtf8String(path).c_str(), watchPoint.status); - return; - } - - if (shouldTerminate) { - logToJava(LogLevel::FINE, "Ignoring incoming events for %s because server is terminating (status = %d)", - utf16ToUtf8String(path).c_str(), watchPoint.status); - return; - } - - ChangeType type; - const u16string name = utf8ToUtf16String(eventName); - - if (!name.empty()) { - path.append(u"/"); - path.append(name); - } - - if (IS_SET(mask, IN_CREATE | IN_MOVED_TO)) { - type = ChangeType::CREATED; - } else if (IS_SET(mask, IN_DELETE | IN_DELETE_SELF | IN_MOVED_FROM)) { - type = ChangeType::REMOVED; - } else if (IS_SET(mask, IN_MODIFY)) { - type = ChangeType::MODIFIED; - } else { - logToJava(LogLevel::WARNING, "Unknown event 0x%x for %s", mask, utf16ToUtf8String(path).c_str()); - reportUnknownEvent(env, path); - return; - } - - reportChangeEvent(env, type, path); -} - -void Server::registerPaths(const vector& paths) { - unique_lock lock(mutationMutex); - for (auto& path : paths) { - registerPath(path); - } -} - -bool Server::unregisterPaths(const vector& paths) { - unique_lock lock(mutationMutex); - bool success = true; - for (auto& path : paths) { - success &= unregisterPath(path); - } - return success; -} - -void Server::registerPath(const u16string& path) { - auto it = watchPoints.find(path); - if (it != watchPoints.end()) { - throw FileWatcherException("Already watching path", path); - } - string pathNarrow = utf16ToUtf8String(path); - struct stat st; - if (lstat(pathNarrow.c_str(), &st) != 0) { - throw FileWatcherException("Couldn't add watch, stat failed", path, errno); - } - - int watchDescriptor = inotify_add_watch(inotify->fd, pathNarrow.c_str(), EVENT_MASK); - if (watchDescriptor == -1) { - if (errno == ENOSPC) { - rethrowAsJavaException(getThreadEnv(), InotifyWatchesLimitTooLowException(), linuxJniConstants->inotifyWatchesLimitTooLowExceptionClass.get()); - throw JavaExceptionThrownException(); - } - throw FileWatcherException("Couldn't add watch, inotify_add_watch failed", path, errno); - } - if (watchRoots.find(watchDescriptor) != watchRoots.end()) { - throw FileWatcherException("Already watching path", path); - } - - watchPoints.emplace(piecewise_construct, - forward_as_tuple(path), - forward_as_tuple(path, inotify, watchDescriptor, st.st_ino)); - watchRoots[watchDescriptor] = path; -} - -bool Server::unregisterPath(const u16string& path) { - auto it = watchPoints.find(path); - if (it == watchPoints.end()) { - logToJava(LogLevel::INFO, "Path is not watched: %s", utf16ToUtf8String(path).c_str()); - return false; - } - auto& watchPoint = it->second; - int wd = watchPoint.watchDescriptor; - CancelResult ret = watchPoint.cancel(); - if (ret == CancelResult::ALREADY_CANCELLED) { - return false; - } - recentlyUnregisteredWatchRoots.emplace(wd, path); - watchRoots.erase(wd); - // We use the path instead erase(it) here because on Alpine Linux we've seen crashes happen here - // when inside a Docker container a host-mapped directory is watched. There is no good theory as - // of this writing why the problem occurs, but not using the iterator here fixes it. - watchPoints.erase(path); - return ret == CancelResult::CANCELLED; -} - -void Server::stopWatchingMovedPaths(jobjectArray absolutePathsToCheck, jobject droppedPaths) { - JNIEnv* env = getThreadEnv(); - int count = env->GetArrayLength(absolutePathsToCheck); - for (int i = 0; i < count; i++) { - jstring jPathToCheck = reinterpret_cast(env->GetObjectArrayElement(absolutePathsToCheck, i)); - auto pathToCheck = javaToUtf16String(env, jPathToCheck); - - auto it = watchPoints.find(pathToCheck); - if (it == watchPoints.end()) { - addToList(env, droppedPaths, jPathToCheck); - env->DeleteLocalRef(jPathToCheck); - continue; - } - auto& watchPoint = it->second; - if (watchPoint.status != WatchPointStatus::LISTENING) { - addToList(env, droppedPaths, jPathToCheck); - env->DeleteLocalRef(jPathToCheck); - continue; - } - - string pathNarrow = utf16ToUtf8String(watchPoint.path); - struct stat st; - if (lstat(pathNarrow.c_str(), &st) == 0 && st.st_ino == watchPoint.inode) { - env->DeleteLocalRef(jPathToCheck); - continue; - } - - addToList(env, droppedPaths, jPathToCheck); - env->DeleteLocalRef(jPathToCheck); - - watchPoint.cancel(); - } -} - -void Server::addToList(JNIEnv* env, jobject jList, jstring jString) { - env->CallBooleanMethod(jList, listAddMethod, jString); - throwNativeExceptionWhenJavaExceptionOccurred(env); -} - -JNIEXPORT jobject JNICALL -Java_net_rubygrapefruit_platform_internal_jni_LinuxFileEventFunctions_startWatcher0(JNIEnv* env, jclass, jobject javaCallback) { - try { - return wrapServer(env, new Server(env, javaCallback)); - } catch (const InotifyInstanceLimitTooLowException& e) { - rethrowAsJavaException(env, e, linuxJniConstants->inotifyInstanceLimitTooLowExceptionClass.get()); - return NULL; - } -} - -JNIEXPORT jboolean JNICALL -Java_net_rubygrapefruit_platform_internal_jni_LinuxFileEventFunctions_isGlibc0(JNIEnv*, jclass) { - void* libcLibrary = dlopen("libc.so.6", RTLD_LAZY); - if (!libcLibrary) { - return false; - } - void* libcVerCheck = dlsym(libcLibrary, "gnu_get_libc_version"); - jboolean isValid = libcVerCheck != NULL; - dlclose(libcLibrary); - return isValid; -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_LinuxFileEventFunctions_00024LinuxFileWatcher_stopWatchingMovedPaths0(JNIEnv* env, jobject, jobject javaServer, jobjectArray jAbsolutePathsToCheck, jobject jDroppedPaths) { - try { - Server* server = (Server*) getServer(env, javaServer); - server->stopWatchingMovedPaths(jAbsolutePathsToCheck, jDroppedPaths); - } catch (const JavaExceptionThrownException&) { - // Ignore, the Java exception has already been thrown. - } catch (const exception& e) { - rethrowAsJavaException(env, e); - } -} - -LinuxJniConstants::LinuxJniConstants(JavaVM* jvm) - : JniSupport(jvm) - , inotifyWatchesLimitTooLowExceptionClass(getThreadEnv(), "net/rubygrapefruit/platform/internal/jni/InotifyWatchesLimitTooLowException") - , inotifyInstanceLimitTooLowExceptionClass(getThreadEnv(), "net/rubygrapefruit/platform/internal/jni/InotifyInstanceLimitTooLowException") { -} -#endif diff --git a/file-events/src/file-events/cpp/logging.cpp b/file-events/src/file-events/cpp/logging.cpp deleted file mode 100644 index 6b81caf6..00000000 --- a/file-events/src/file-events/cpp/logging.cpp +++ /dev/null @@ -1,44 +0,0 @@ -#include - -#include "logging.h" - -Logging::Logging(JavaVM* jvm) - : JniSupport(jvm) - , clsLogger(getThreadEnv(), "net/rubygrapefruit/platform/internal/jni/NativeLogger") - , logMethod(getThreadEnv()->GetStaticMethodID(clsLogger.get(), "log", "(ILjava/lang/String;)V")) - , getLevelMethod(getThreadEnv()->GetStaticMethodID(clsLogger.get(), "getLogLevel", "()I")) { -} - -void Logging::invalidateLogLevelCache() { - lastLevelCheck = chrono::steady_clock::time_point(); -} - -bool Logging::enabled(LogLevel level) { - auto current = chrono::steady_clock::now(); - auto elapsed = chrono::duration_cast(current - lastLevelCheck).count(); - if (elapsed > LOG_LEVEL_CHECK_INTERVAL_IN_MS) { - JNIEnv* env = getThreadEnv(); - minimumLogLevel = env->CallStaticIntMethod(clsLogger.get(), getLevelMethod); - rethrowJavaException(env); - lastLevelCheck = current; - } - return minimumLogLevel <= static_cast(level); -} - -void Logging::send(LogLevel level, const char* fmt, ...) { - char buffer[1024]; - va_list args; - va_start(args, fmt); - vsnprintf(buffer, sizeof(buffer), fmt, args); - va_end(args); - - JNIEnv* env = getThreadEnv(); - if (env == NULL) { - cerr << buffer << endl; - } else { - jstring logString = env->NewStringUTF(buffer); - env->CallStaticVoidMethod(clsLogger.get(), logMethod, level, logString); - env->DeleteLocalRef(logString); - rethrowJavaException(env); - } -} diff --git a/file-events/src/file-events/cpp/services.cpp b/file-events/src/file-events/cpp/services.cpp deleted file mode 100644 index a5c92efc..00000000 --- a/file-events/src/file-events/cpp/services.cpp +++ /dev/null @@ -1,31 +0,0 @@ -#include "logging.h" -#include "generic_fsnotifier.h" -#include "linux_fsnotifier.h" - -BaseJniConstants* baseJniConstants; -NativePlatformJniConstants* nativePlatformJniConstants; -#ifdef __linux__ -LinuxJniConstants* linuxJniConstants; -#endif -Logging* logging; - -JNIEXPORT jint JNICALL -JNI_OnLoad(JavaVM* jvm, void*) { - baseJniConstants = new BaseJniConstants(jvm); - nativePlatformJniConstants = new NativePlatformJniConstants(jvm); - logging = new Logging(jvm); -#ifdef __linux__ - linuxJniConstants = new LinuxJniConstants(jvm); -#endif - return JNI_VERSION_1_6; -} - -JNIEXPORT void JNICALL -JNI_OnUnload(JavaVM*, void*) { -#ifdef __linux__ - delete linuxJniConstants; -#endif - delete logging; - delete nativePlatformJniConstants; - delete baseJniConstants; -} diff --git a/file-events/src/file-events/cpp/win_fsnotifier.cpp b/file-events/src/file-events/cpp/win_fsnotifier.cpp deleted file mode 100644 index 92d662c0..00000000 --- a/file-events/src/file-events/cpp/win_fsnotifier.cpp +++ /dev/null @@ -1,494 +0,0 @@ -#ifdef _WIN32 - -#include "win_fsnotifier.h" -#include "command.h" - -#include -#include - -using namespace std; - -string wideToUtf8String(const wstring& string) { - wstring_convert>, wchar_t> conv; - return conv.to_bytes(string); -} - -#define wideToUtf16String(string) (u16string((string).begin(), (string).end())) - -bool isAbsoluteLocalPath(const wstring& path) { - if (path.length() < 3) { - return false; - } - return ((L'a' <= path[0] && path[0] <= L'z') || (L'A' <= path[0] && path[0] <= L'Z')) - && path[1] == L':' - && path[2] == L'\\'; -} - -bool isAbsoluteUncPath(const wstring& path) { - if (path.length() < 3) { - return false; - } - return path[0] == L'\\' && path[1] == L'\\'; -} - -bool isLongPath(const wstring& path) { - return path.length() >= 4 && path.substr(0, 4) == L"\\\\?\\"; -} - -bool isUncLongPath(const wstring& path) { - return path.length() >= 8 && path.substr(0, 8) == L"\\\\?\\UNC\\"; -} - -// TODO How can this be done nicer, wihtout both unnecessary copy and in-place mutation? -void convertToLongPathIfNeeded(wstring& path) { - // Technically, this should be MAX_PATH (i.e. 260), except some Win32 API related - // to working with directory paths are actually limited to 240. It is just - // safer/simpler to cover both cases in one code path. - if (path.length() <= 240) { - return; - } - - // It is already a long path, nothing to do here - if (isLongPath(path)) { - return; - } - - if (isAbsoluteLocalPath(path)) { - // Format: C:\... -> \\?\C:\... - path.insert(0, L"\\\\?\\"); - } else if (isAbsoluteUncPath(path)) { - // In this case, we need to skip the first 2 characters: - // Format: \\server\share\... -> \\?\UNC\server\share\... - path.erase(0, 2); - path.insert(0, L"\\\\?\\UNC\\"); - } else { - // It is some sort of unknown format, don't mess with it - } -} - -// Allocate maximum path length -#define PATH_BUFFER_SIZE 32768 - -bool resolveFinalPath(HANDLE handle, wstring& path) { - vector buffer; - buffer.reserve(PATH_BUFFER_SIZE); - DWORD pathLength = GetFinalPathNameByHandleW( - handle, - &buffer[0], - PATH_BUFFER_SIZE, - FILE_NAME_OPENED); - if (pathLength == 0 || pathLength > PATH_BUFFER_SIZE) { - logToJava(LogLevel::WARNING, "Couldn't get final path for handle 0x%x, error code: %d", handle, GetLastError()); - return false; - } - path.clear(); - path.insert(0, &buffer[0], pathLength); - return true; -} - -// -// WatchPoint -// - -WatchPoint::WatchPoint(Server* server, size_t eventBufferSize, const wstring& path) - : registeredPath(path) - , status(WatchPointStatus::NOT_LISTENING) - , server(server) { - wstring longPath = path; - convertToLongPathIfNeeded(longPath); - HANDLE directoryHandle = CreateFileW( - longPath.c_str(), // pointer to the file name - FILE_LIST_DIRECTORY, // access (read/write) mode - CREATE_SHARE, // share mode - NULL, // security descriptor - OPEN_EXISTING, // how to create - CREATE_FLAGS, // file attributes - NULL // file with attributes to copy - ); - if (directoryHandle == INVALID_HANDLE_VALUE) { - throw FileWatcherException("Couldn't add watch", wideToUtf16String(path), GetLastError()); - } - this->directoryHandle = directoryHandle; - bool directoryHandleIsAccessible = resolveFinalPath(directoryHandle, registeredFinalPath); - if (!directoryHandleIsAccessible) { - throw FileWatcherException("Couldn't resolve final path of", wideToUtf16String(path), GetLastError()); - } - this->eventBuffer.reserve(eventBufferSize); - ZeroMemory(&this->overlapped, sizeof(OVERLAPPED)); - this->overlapped.hEvent = this; - switch (listen()) { - case ListenResult::SUCCESS: - break; - case ListenResult::DELETED: - throw FileWatcherException("Couldn't add watch, path is not a directory", wideToUtf16String(path)); - } -} - -bool WatchPoint::cancel() { - if (status == WatchPointStatus::LISTENING) { - logToJava(LogLevel::FINE, "Cancelling %s", wideToUtf8String(registeredPath).c_str()); - bool cancelled = (bool) CancelIoEx(directoryHandle, &overlapped); - if (cancelled) { - status = WatchPointStatus::CANCELLED; - } else { - DWORD cancelError = GetLastError(); - close(); - if (cancelError == ERROR_NOT_FOUND) { - // Do nothing, looks like this is a typical scenario - logToJava(LogLevel::FINE, "Watch point already finished %s", wideToUtf8String(registeredPath).c_str()); - } else { - throw FileWatcherException("Couldn't cancel watch point", wideToUtf16String(registeredPath), cancelError); - } - } - return cancelled; - } - return false; -} - -WatchPoint::~WatchPoint() { - try { - cancel(); - SleepEx(0, true); - close(); - } catch (const exception& ex) { - logToJava(LogLevel::WARNING, "Couldn't cancel watch point %s: %s", wideToUtf8String(registeredPath).c_str(), ex.what()); - } -} - -static void CALLBACK handleEventCallback(DWORD errorCode, DWORD bytesTransferred, LPOVERLAPPED overlapped) { - WatchPoint* watchPoint = (WatchPoint*) overlapped->hEvent; - watchPoint->handleEventsInBuffer(errorCode, bytesTransferred); -} - -bool WatchPoint::isValidDirectory() { - DWORD attrib = GetFileAttributesW(registeredPath.c_str()); - - return (attrib != INVALID_FILE_ATTRIBUTES) - && ((attrib & FILE_ATTRIBUTE_DIRECTORY) != 0); -} - -ListenResult WatchPoint::listen() { - BOOL success = ReadDirectoryChangesExW( - directoryHandle, // handle to directory - &eventBuffer[0], // read results buffer - (DWORD) eventBuffer.capacity(), // length of buffer - TRUE, // include children - EVENT_MASK, // filter conditions - NULL, // bytes returned - &overlapped, // overlapped buffer - &handleEventCallback, // completion routine - ReadDirectoryNotifyExtendedInformation); - if (success) { - status = WatchPointStatus::LISTENING; - return ListenResult::SUCCESS; - } else { - DWORD listenError = GetLastError(); - close(); - if (listenError == ERROR_ACCESS_DENIED && !isValidDirectory()) { - return ListenResult::DELETED; - } else { - throw FileWatcherException("Couldn't add watch", wideToUtf16String(registeredPath), listenError); - } - } -} - -void WatchPoint::close() { - if (status != WatchPointStatus::FINISHED) { - try { - BOOL ret = CloseHandle(directoryHandle); - if (!ret) { - logToJava(LogLevel::SEVERE, "Couldn't close handle %p for '%ls': %d", directoryHandle, wideToUtf8String(registeredPath).c_str(), GetLastError()); - } - } catch (const exception& ex) { - // Apparently with debugging enabled CloseHandle() can also throw, see: - // https://docs.microsoft.com/en-us/windows/win32/api/handleapi/nf-handleapi-closehandle#return-value - logToJava(LogLevel::SEVERE, "Couldn't close handle %p for '%ls': %s", directoryHandle, wideToUtf8String(registeredPath).c_str(), ex.what()); - } - status = WatchPointStatus::FINISHED; - } -} - -void WatchPoint::handleEventsInBuffer(DWORD errorCode, DWORD bytesTransferred) { - if (errorCode == ERROR_OPERATION_ABORTED) { - logToJava(LogLevel::FINE, "Finished watching '%s', status = %d", wideToUtf8String(registeredPath).c_str(), status); - close(); - return; - } - - if (status != WatchPointStatus::LISTENING) { - logToJava(LogLevel::FINE, "Ignoring incoming events for %s as watch-point is not listening (%d bytes, errorCode = %d, status = %d)", - wideToUtf8String(registeredPath).c_str(), bytesTransferred, errorCode, status); - return; - } - status = WatchPointStatus::NOT_LISTENING; - server->handleEvents(this, errorCode, eventBuffer, bytesTransferred); -} - -// -// Server -// - -void Server::handleEvents(WatchPoint* watchPoint, DWORD errorCode, const vector& eventBuffer, DWORD bytesTransferred) { - JNIEnv* env = getThreadEnv(); - - try { - if (errorCode != ERROR_SUCCESS) { - if (errorCode == ERROR_ACCESS_DENIED && !watchPoint->isValidDirectory()) { - reportWatchPointDeleted(watchPoint); - return; - } else { - throw FileWatcherException("Error received when handling events", wideToUtf16String(watchPoint->registeredPath), errorCode); - } - } - - wstring currentFinalPath; - bool watchedHandleIsAccessible = resolveFinalPath(watchPoint->directoryHandle, currentFinalPath); - if (!watchedHandleIsAccessible || currentFinalPath != watchPoint->registeredFinalPath) { - // The handle has become invalid or missing, or the directory has been relocated, consider this as if the the watch point was deleted - reportWatchPointDeleted(watchPoint); - return; - } - - const wstring& path = watchPoint->registeredPath; - if (shouldTerminate) { - logToJava(LogLevel::FINE, "Ignoring incoming events for %s because server is terminating (%d bytes, status = %d)", - wideToUtf8String(path).c_str(), bytesTransferred, watchPoint->status); - return; - } - - if (bytesTransferred == 0) { - // This is what the documentation has to say about a zero-length dataset: - // - // If the number of bytes transferred is zero, the eventBuffer was either too large - // for the system to allocate or too small to provide detailed information on - // all the changes that occurred in the directory or subtree. In this case, - // you should compute the changes by enumerating the directory or subtree. - // - // (See https://docs.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw) - // - // We'll handle this as a simple overflow and report it as such. - reportOverflow(env, wideToUtf16String(path)); - } else { - int index = 0; - for (;;) { - FILE_NOTIFY_EXTENDED_INFORMATION* current = (FILE_NOTIFY_EXTENDED_INFORMATION*) &eventBuffer[index]; - handleEvent(env, path, current); - if (current->NextEntryOffset == 0) { - break; - } - index += current->NextEntryOffset; - } - } - - switch (watchPoint->listen()) { - case ListenResult::SUCCESS: - break; - case ListenResult::DELETED: - logToJava(LogLevel::FINE, "Watched directory removed for %s", wideToUtf8String(path).c_str()); - reportChangeEvent(env, ChangeType::REMOVED, wideToUtf16String(path)); - break; - } - } catch (const exception& ex) { - reportFailure(env, ex); - } -} - -void Server::handleEvent(JNIEnv* env, const wstring& watchedPathW, FILE_NOTIFY_EXTENDED_INFORMATION* info) { - wstring changedPathW = wstring(info->FileName, 0, info->FileNameLength / sizeof(wchar_t)); - if (!changedPathW.empty()) { - changedPathW.insert(0, 1, L'\\'); - } - changedPathW.insert(0, watchedPathW); - - logToJava(LogLevel::FINE, "Change detected: 0x%x '%s'", info->Action, wideToUtf8String(changedPathW).c_str()); - - ChangeType type; - if (info->Action == FILE_ACTION_ADDED || info->Action == FILE_ACTION_RENAMED_NEW_NAME) { - type = ChangeType::CREATED; - } else if (info->Action == FILE_ACTION_REMOVED || info->Action == FILE_ACTION_RENAMED_OLD_NAME) { - type = ChangeType::REMOVED; - } else if (info->Action == FILE_ACTION_MODIFIED) { - if (info->FileAttributes & FILE_ATTRIBUTE_DIRECTORY) { - // Ignore MODIFIED events on directories - logToJava(LogLevel::FINE, "Ignored MODIFIED event on directory", nullptr); - return; - } - type = ChangeType::MODIFIED; - } else { - logToJava(LogLevel::WARNING, "Unknown event 0x%x for %s", info->Action, wideToUtf8String(changedPathW).c_str()); - reportUnknownEvent(env, wideToUtf16String(changedPathW)); - return; - } - - reportChangeEvent(env, type, wideToUtf16String(changedPathW)); -} - -void Server::reportWatchPointDeleted(WatchPoint* watchPoint) { - reportChangeEvent(getThreadEnv(), ChangeType::REMOVED, wideToUtf16String(watchPoint->registeredPath)); - watchPoint->close(); -} - -Server::Server(JNIEnv* env, size_t eventBufferSize, long commandTimeoutInMillis, jobject watcherCallback) - : AbstractServer(env, watcherCallback) - , eventBufferSize(eventBufferSize) - , commandTimeoutInMillis(commandTimeoutInMillis) { - jclass listClass = env->FindClass("java/util/List"); - this->listAddMethod = env->GetMethodID(listClass, "add", "(Ljava/lang/Object;)Z"); -} - -void Server::initializeRunLoop() { - // For some reason GetCurrentThread() returns a thread that doesn't accept APCs - // so we need to use OpenThread() instead. - threadHandle = OpenThread( - THREAD_ALL_ACCESS, // dwDesiredAccess - false, // bInheritHandle - GetCurrentThreadId() // dwThreadId - ); - if (threadHandle == NULL) { - throw FileWatcherException("Couldn't open current thread", GetLastError()); - } -} - -void Server::shutdownRunLoop() { - executeOnRunLoop([this]() { - shouldTerminate = true; - return true; - }); -} - -void Server::runLoop() { - while (!shouldTerminate) { - SleepEx(INFINITE, true); - } - - // We have received termination, cancel all watchers - logToJava(LogLevel::FINE, "Finished with run loop, now cancelling remaining watch points", NULL); - for (auto& it : watchPoints) { - auto& watchPoint = it.second; - if (watchPoint.status == WatchPointStatus::LISTENING) { - try { - watchPoint.cancel(); - } catch (const exception& ex) { - logToJava(LogLevel::SEVERE, "%s", ex.what()); - } - } - } - - logToJava(LogLevel::FINE, "Waiting for any pending watch points to abort completely", NULL); - SleepEx(0, true); - - // Warn about any unfinished watchpoints - for (auto& it : watchPoints) { - auto& watchPoint = it.second; - switch (watchPoint.status) { - case WatchPointStatus::NOT_LISTENING: - case WatchPointStatus::FINISHED: - break; - default: - logToJava(LogLevel::WARNING, "Watch point %s did not finish before termination timeout (status = %d)", - wideToUtf8String(watchPoint.registeredPath).c_str(), watchPoint.status); - break; - } - } - - CloseHandle(threadHandle); -} - -static void CALLBACK executeOnRunLoopCallback(_In_ ULONG_PTR info) { - Command* command = (Command*) info; - command->executeInsideRunLoop(); -} - -bool Server::executeOnRunLoop(function function) { - Command command(function); - return command.execute(commandTimeoutInMillis, [this](Command* command) { - DWORD ret = QueueUserAPC(executeOnRunLoopCallback, threadHandle, (ULONG_PTR) command); - if (ret == 0) { - throw FileWatcherException("Received error while queuing APC", GetLastError()); - } - }); -} - -void Server::registerPaths(const vector& paths) { - executeOnRunLoop([this, paths]() { - for (auto& path : paths) { - registerPath(path); - } - return true; - }); -} - -bool Server::unregisterPaths(const vector& paths) { - return executeOnRunLoop([this, paths]() { - bool success = true; - for (auto& path : paths) { - success &= unregisterPath(path); - } - return success; - }); -} - -void Server::registerPath(const u16string& path) { - wstring registeredPath(path.begin(), path.end()); - auto it = watchPoints.find(registeredPath); - if (it != watchPoints.end()) { - if (it->second.status == WatchPointStatus::FINISHED) { - watchPoints.erase(it); - } else { - throw FileWatcherException("Already watching path", path); - } - } - watchPoints.emplace(piecewise_construct, - forward_as_tuple(registeredPath), - forward_as_tuple(this, eventBufferSize, registeredPath)); -} - -bool Server::unregisterPath(const u16string& path) { - wstring registeredPath(path.begin(), path.end()); - if (watchPoints.erase(registeredPath) == 0) { - logToJava(LogLevel::INFO, "Path is not watched: %s", wideToUtf8String(registeredPath).c_str()); - return false; - } - return true; -} - -void Server::stopWatchingMovedPaths(jobject droppedPaths) { - JNIEnv* env = getThreadEnv(); - for (auto& it : watchPoints) { - auto& watchPoint = it.second; - if (watchPoint.status == WatchPointStatus::FINISHED) { - continue; - } - wstring currentFinalPath; - bool watchedHandleIsAccessible = resolveFinalPath(watchPoint.directoryHandle, currentFinalPath); - if (!watchedHandleIsAccessible || watchPoint.registeredFinalPath != currentFinalPath) { - jstring javaPath = env->NewString((jchar*) wideToUtf16String(watchPoint.registeredPath).c_str(), (jsize) watchPoint.registeredPath.length()); - env->CallBooleanMethod(droppedPaths, listAddMethod, javaPath); - env->DeleteLocalRef(javaPath); - getJavaExceptionAndPrintStacktrace(env); - - watchPoint.cancel(); - } - } -} - -// -// JNI calls -// - -JNIEXPORT jobject JNICALL -Java_net_rubygrapefruit_platform_internal_jni_WindowsFileEventFunctions_startWatcher0(JNIEnv* env, jclass target, jint eventBufferSize, jlong commandTimeoutInMillis, jobject javaCallback) { - return wrapServer(env, new Server(env, eventBufferSize, (long) commandTimeoutInMillis, javaCallback)); -} - -JNIEXPORT void JNICALL -Java_net_rubygrapefruit_platform_internal_jni_WindowsFileEventFunctions_00024WindowsFileWatcher_stopWatchingMovedPaths0(JNIEnv* env, jobject, jobject javaServer, jobject jDroppedPaths) { - try { - Server* server = (Server*) getServer(env, javaServer); - server->stopWatchingMovedPaths(jDroppedPaths); - } catch (const exception& e) { - rethrowAsJavaException(env, e); - } -} - -#endif diff --git a/file-events/src/file-events/headers/apple_fsnotifier.h b/file-events/src/file-events/headers/apple_fsnotifier.h deleted file mode 100644 index 93cfe270..00000000 --- a/file-events/src/file-events/headers/apple_fsnotifier.h +++ /dev/null @@ -1,122 +0,0 @@ -#pragma once - -#if defined(__APPLE__) - -#include -#include -#include -#include -#include - -#include -#include - -#include "generic_fsnotifier.h" -#include "net_rubygrapefruit_platform_internal_jni_OsxFileEventFunctions.h" - -using namespace std; - -template -class BlockingQueue { -private: - std::mutex mtx; - std::condition_variable cv; - std::queue queue; - -public: - // Enqueue an item into the queue and notify one waiting thread - void enqueue(const T& item) { - { - std::unique_lock lock(mtx); - queue.push(item); - } - cv.notify_one(); - } - - // Dequeue an item from the queue. Blocks if the queue is empty until an item is available. - T dequeue() { - std::unique_lock lock(mtx); - // Block until the queue isn't empty - cv.wait(lock, [this] { return !queue.empty(); }); - T item = queue.front(); - queue.pop(); - return item; - } - - size_t size() { - return queue.size(); - } -}; - -class Server; - -static void handleEventsCallback( - ConstFSEventStreamRef streamRef, - void* clientCallBackInfo, - size_t numEvents, - void* eventPaths, - const FSEventStreamEventFlags eventFlags[], - const FSEventStreamEventId*); - -class WatchPoint { -public: - WatchPoint(Server* server, dispatch_queue_t dispatchQueue, const u16string& path, long latencyInMillis); - ~WatchPoint(); - -private: - FSEventStreamRef watcherStream; -}; - -struct FileEvent { - std::string eventPath; - FSEventStreamEventFlags eventFlags; - FSEventStreamEventId eventId; -}; - -struct ErrorEvent { - std::string message; -}; - -struct PoisonPill { }; - -using QueueItem = std::variant; - -class Server : public AbstractServer { -public: - Server(JNIEnv* env, jobject watcherCallback, long latencyInMillis); - virtual ~Server(); - - virtual void registerPaths(const vector& paths) override; - virtual bool unregisterPaths(const vector& paths) override; - -protected: - void initializeRunLoop() override; - void runLoop() override; - - void shutdownRunLoop() override; - -private: - void handleEvent(JNIEnv* env, const char* path, FSEventStreamEventFlags flags, FSEventStreamEventId eventId); - void handleEvents( - size_t numEvents, - char** eventPaths, - const FSEventStreamEventFlags eventFlags[], - const FSEventStreamEventId eventIds[]); - - friend void handleEventsCallback( - ConstFSEventStreamRef stream, - void* clientCallBackInfo, - size_t numEvents, - void* eventPaths, - const FSEventStreamEventFlags eventFlags[], - const FSEventStreamEventId eventIds[]); - - const long latencyInMillis; - recursive_mutex mutationMutex; - unordered_map watchPoints; - - const dispatch_queue_t dispatchQueue; - BlockingQueue eventQueue; -}; - -#endif diff --git a/file-events/src/file-events/headers/command.h b/file-events/src/file-events/headers/command.h deleted file mode 100644 index fe3c5abb..00000000 --- a/file-events/src/file-events/headers/command.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -#include "exception.h" - -using namespace std; - -class Command { -public: - Command(function work) - : work(work) { - } - - bool execute(long timeout, function scheduleWithRunLoop) { - unique_lock lock(executionMutex); - scheduleWithRunLoop(this); - auto status = executed.wait_for(lock, chrono::milliseconds(timeout)); - if (status == cv_status::timeout) { - throw FileWatcherException("Execution timed out"); - } else if (failure) { - rethrow_exception(failure); - } else { - return result; - } - } - - void executeInsideRunLoop() { - try { - result = work(); - } catch (const exception&) { - failure = current_exception(); - } - unique_lock lock(executionMutex); - executed.notify_all(); - } - -private: - function work; - mutex executionMutex; - condition_variable executed; - bool result; - exception_ptr failure; -}; diff --git a/file-events/src/file-events/headers/exception.h b/file-events/src/file-events/headers/exception.h deleted file mode 100644 index 79392815..00000000 --- a/file-events/src/file-events/headers/exception.h +++ /dev/null @@ -1,54 +0,0 @@ -#pragma once - -#include -#include -#include - -#include "jni_support.h" - -using namespace std; - -inline string createMessage(const string& message, const u16string& path) { - stringstream ss; - ss << message; - ss << ": "; - ss << utf16ToUtf8String(path); - return ss.str(); -} - -inline string createMessage(const string& message, int errorCode) { - stringstream ss; - ss << message; - ss << ", error = "; - ss << errorCode; - return ss.str(); -} - -inline string createMessage(const string& message, const u16string& path, int errorCode) { - stringstream ss; - ss << message; - ss << ", error = "; - ss << errorCode; - ss << ": "; - ss << utf16ToUtf8String(path); - return ss.str(); -} - -struct FileWatcherException : public runtime_error { -public: - FileWatcherException(const string& message, const u16string& path, int errorCode) - : runtime_error(createMessage(message, path, errorCode)) { - } - - FileWatcherException(const string& message, const u16string& path) - : runtime_error(createMessage(message, path)) { - } - - FileWatcherException(const string& message, int errorCode) - : runtime_error(createMessage(message, errorCode)) { - } - - FileWatcherException(const string& message) - : runtime_error(message) { - } -}; diff --git a/file-events/src/file-events/headers/generic_fsnotifier.h b/file-events/src/file-events/headers/generic_fsnotifier.h deleted file mode 100644 index 90293068..00000000 --- a/file-events/src/file-events/headers/generic_fsnotifier.h +++ /dev/null @@ -1,102 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "exception.h" -#include "jni_support.h" -#include "logging.h" -#include "net_rubygrapefruit_platform_internal_jni_AbstractNativeFileEventFunctions.h" -#include "net_rubygrapefruit_platform_internal_jni_AbstractNativeFileEventFunctions_NativeFileWatcher.h" - -using namespace std; - -// Corresponds to values of FileWatchEvent.ChangeType -enum class ChangeType { - CREATED, - REMOVED, - MODIFIED, - INVALIDATED -}; - -#define IS_SET(flags, mask) (((flags) & (mask)) != 0) - -struct InsufficientResourcesFileWatcherException : public FileWatcherException { -public: - InsufficientResourcesFileWatcherException(const string& message); -}; - -class AbstractServer; - -AbstractServer* getServer(JNIEnv* env, jobject javaServer); - -class AbstractServer : public JniSupport { -public: - AbstractServer(JNIEnv* env, jobject watcherCallback); - virtual ~AbstractServer(); - - virtual void initializeRunLoop() = 0; - void executeRunLoop(JNIEnv* env); - - /** - * Registers new watch point with the server for the given paths. - */ - virtual void registerPaths(const vector& paths) = 0; - - /** - * Unregisters watch points with the server for the given paths. - */ - virtual bool unregisterPaths(const vector& paths) = 0; - - /** - * Shuts the server down. - */ - virtual void shutdownRunLoop() = 0; - - /** - * Waits for the given timeout for the server to finsih terminating. - */ - bool awaitTermination(long timeoutInMillis); - -protected: - virtual void runLoop() = 0; - - void reportChangeEvent(JNIEnv* env, ChangeType type, const u16string& path); - void reportUnknownEvent(JNIEnv* env, const u16string& path); - void reportOverflow(JNIEnv* env, const u16string& path); - void reportFailure(JNIEnv* env, const char* message); - void reportFailure(JNIEnv* env, const exception& ex); - -private: - mutex terminationMutex; - condition_variable terminationVariable; - bool terminated = false; - - JniGlobalRef watcherCallback; - jmethodID watcherReportChangeEventMethod; - jmethodID watcherReportUnknownEventMethod; - jmethodID watcherReportOverflowMethod; - jmethodID watcherReportFailureMethod; -}; - -class NativePlatformJniConstants : public JniSupport { -public: - NativePlatformJniConstants(JavaVM* jvm); - - const JClass nativeExceptionClass; -}; - -extern NativePlatformJniConstants* nativePlatformJniConstants; - -jobject wrapServer(JNIEnv* env, AbstractServer* server); - -jobject rethrowAsJavaException(JNIEnv* env, const exception& e); -jobject rethrowAsJavaException(JNIEnv* env, const exception& e, jclass exceptionClass); diff --git a/file-events/src/file-events/headers/jni_support.h b/file-events/src/file-events/headers/jni_support.h deleted file mode 100644 index 0d3dc17d..00000000 --- a/file-events/src/file-events/headers/jni_support.h +++ /dev/null @@ -1,111 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -using namespace std; - -// Utility wrapper to adapt locale-bound facets for wstring convert -// Exposes the protected destructor as public -// See https://en.cppreference.com/w/cpp/locale/codecvt -template -struct deletable_facet : Facet { - template - deletable_facet(Args&&... args) - : Facet(forward(args)...) { - } - ~deletable_facet() { - } -}; - -template -class JniGlobalRef; - -// Throwing a Java exception from native code does not change the program flow. -// So it may be necessary to throw a native exception as well which then can be catched in the outmost level just before returning to Java. -// The idea here is that the catch clause for this exception is always empty. -struct JavaExceptionThrownException : public runtime_error { -public: - JavaExceptionThrownException(); -}; - -/** - * Support for using JNI in a multi-threaded environment. - */ -class JniSupport { -public: - JniSupport(JavaVM* jvm); - JniSupport(JNIEnv* env); - - /** - * Check for a Java exception and log it. - */ - static jthrowable getJavaExceptionAndPrintStacktrace(JNIEnv* env); - - /** - * Check for a Java exception and rethrow as a native exception. - */ - static void rethrowJavaException(JNIEnv* env); - - /** - * Check for a Java exception and throw native JavaExceptionThrownException. - */ - static void throwNativeExceptionWhenJavaExceptionOccurred(JNIEnv* env); - -protected: - const JniGlobalRef& findClass(const char* className); - JNIEnv* getThreadEnv(); - -protected: - JavaVM* jvm; -}; - -template -class JniGlobalRef : public JniSupport { -public: - JniGlobalRef(JNIEnv* env, T object) - : JniSupport(env) - , ref(reinterpret_cast(env->NewGlobalRef(object))) { - if (ref == nullptr) { - throw runtime_error("Failed to create global JNI reference"); - } - } - ~JniGlobalRef() { - getThreadEnv()->DeleteGlobalRef(ref); - } - - T get() const { - return ref; - } - -private: - const T ref; -}; - -class JClass : public JniGlobalRef { -public: - JClass(JNIEnv* env, const char* className) - : JniGlobalRef(env, env->FindClass(className)) { - } -}; - -class BaseJniConstants : public JniSupport { -public: - BaseJniConstants(JavaVM* jvm); - - const JClass classClass; -}; - -extern BaseJniConstants* baseJniConstants; - -extern string javaToUtf8String(JNIEnv* env, jstring javaString); - -extern u16string javaToUtf16String(JNIEnv* env, jstring javaString); - -extern void javaToUtf16StringArray(JNIEnv* env, jobjectArray javaStrings, vector& strings); - -extern u16string utf8ToUtf16String(const char* string); - -extern string utf16ToUtf8String(const u16string& string); diff --git a/file-events/src/file-events/headers/linux_fsnotifier.h b/file-events/src/file-events/headers/linux_fsnotifier.h deleted file mode 100644 index bbc64643..00000000 --- a/file-events/src/file-events/headers/linux_fsnotifier.h +++ /dev/null @@ -1,137 +0,0 @@ -#pragma once - -#ifdef __linux__ - -#include -#include -#include -#include -#include - -#include "generic_fsnotifier.h" -#include "net_rubygrapefruit_platform_internal_jni_LinuxFileEventFunctions.h" -#include "net_rubygrapefruit_platform_internal_jni_LinuxFileEventFunctions_LinuxFileWatcher.h" - -using namespace std; - -struct InotifyInstanceLimitTooLowException : public InsufficientResourcesFileWatcherException { -public: - InotifyInstanceLimitTooLowException(); -}; - -struct InotifyWatchesLimitTooLowException : public InsufficientResourcesFileWatcherException { -public: - InotifyWatchesLimitTooLowException(); -}; - -class Server; - -struct Inotify { - Inotify(); - ~Inotify(); - - const int fd; -}; - -struct ShutdownEvent { - ShutdownEvent(); - ~ShutdownEvent(); - - void trigger() const; - void consume() const; - - const int fd; -}; - -enum class WatchPointStatus { - /** - * The watch point is listening, expect events to arrive. - */ - LISTENING, - - /** - * The watch point has been cancelled, expect IN_IGNORED event. - */ - CANCELLED -}; - -enum class CancelResult { - /** - * The watch point was successfully cancelled. - */ - CANCELLED, - - /** - * The watch point was not cancelled (probably because it was removed). - */ - NOT_CANCELLED, - - /** - * The watch poing has already been cancelled earlier. - */ - ALREADY_CANCELLED -}; - -class WatchPoint { -public: - WatchPoint(const u16string& path, const shared_ptr inotify, int watchDescriptor, ino_t inode); - - CancelResult cancel(); - -private: - WatchPointStatus status; - const int watchDescriptor; - const shared_ptr inotify; - const u16string path; - const ino_t inode; - - friend class Server; -}; - -class Server : public AbstractServer { -public: - Server(JNIEnv* env, jobject watcherCallback); - - // List absolutePathsToCheck, List droppedPaths - void stopWatchingMovedPaths(jobjectArray absolutePathsToCheck, jobject droppedPaths); - - virtual void registerPaths(const vector& paths) override; - virtual bool unregisterPaths(const vector& paths) override; - -protected: - void initializeRunLoop() override; - void runLoop() override; - void shutdownRunLoop() override; - -private: - void processQueues(int timeout); - void handleEvents(); - void handleEvent(JNIEnv* env, const inotify_event* event); - - void registerPath(const u16string& path); - bool unregisterPath(const u16string& path); - - void addToList(JNIEnv* env, jobject jList, jstring jString); - - recursive_mutex mutationMutex; - unordered_map watchPoints; - unordered_map watchRoots; - unordered_map recentlyUnregisteredWatchRoots; - const shared_ptr inotify; - const ShutdownEvent shutdownEvent; - bool shouldTerminate = false; - vector buffer; - jmethodID listAddMethod; -}; - -class LinuxJniConstants : public JniSupport { -public: - LinuxJniConstants(JavaVM* jvm); - - const JClass inotifyWatchesLimitTooLowExceptionClass; - const JClass inotifyInstanceLimitTooLowExceptionClass; -}; - -extern LinuxJniConstants* linuxJniConstants; - -#endif diff --git a/file-events/src/file-events/headers/logging.h b/file-events/src/file-events/headers/logging.h deleted file mode 100644 index e2efd576..00000000 --- a/file-events/src/file-events/headers/logging.h +++ /dev/null @@ -1,40 +0,0 @@ -#pragma once - -#include -#include - -#include "jni_support.h" - -#define LOG_LEVEL_CHECK_INTERVAL_IN_MS 1000 - -enum class LogLevel : int { - ALL, - FINEST, - FINER, - FINE, - CONFIG, - INFO, - WARNING, - SEVERE, - OFF -}; - -class Logging : public JniSupport { -public: - Logging(JavaVM* jvm); - - void invalidateLogLevelCache(); - bool enabled(LogLevel level); - void send(LogLevel level, const char* fmt, ...); - -private: - int minimumLogLevel; - const JClass clsLogger; - const jmethodID logMethod; - const jmethodID getLevelMethod; - chrono::time_point lastLevelCheck; -}; - -extern Logging* logging; - -#define logToJava(level, message, ...) (logging->enabled(level) ? logging->send(level, message, __VA_ARGS__) : ((void) NULL)) diff --git a/file-events/src/file-events/headers/win_fsnotifier.h b/file-events/src/file-events/headers/win_fsnotifier.h deleted file mode 100644 index a314978c..00000000 --- a/file-events/src/file-events/headers/win_fsnotifier.h +++ /dev/null @@ -1,163 +0,0 @@ -#pragma once - -#ifdef _WIN32 - -#include -#include -#include -#include -#include -#include -#include - -// Needs to stay below otherwise byte symbol gets confused with std::byte -#include "generic_fsnotifier.h" -#include "net_rubygrapefruit_platform_internal_jni_WindowsFileEventFunctions.h" -#include "net_rubygrapefruit_platform_internal_jni_WindowsFileEventFunctions_WindowsFileWatcher.h" - -using namespace std; - -#define CREATE_SHARE (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE) -#define CREATE_FLAGS (FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED) - -#define EVENT_MASK (FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE) - -class Server; -class WatchPoint; - -enum class ListenResult { - /** - * Listening succeeded. - */ - SUCCESS, - /** - * Target directory has been removed. - */ - DELETED -}; - -enum class WatchPointStatus { - /** - * The watch point has been constructed, but not currently listening. - */ - NOT_LISTENING, - - /** - * The watch point is listening, expect events to arrive. - */ - LISTENING, - - /** - * The watch point has been cancelled, expect ERROR_OPERATION_ABORTED event. - */ - CANCELLED, - - /** - * The watch point has been cancelled, the ERROR_OPERATION_ABORTED event arrived; or starting the listener caused an error. - */ - FINISHED -}; - -/** - * Represents a watched directory hierarchy. - * - * @note When the hierarchy is moved, Windows does not send any events, and thus Java application won't - * be notified of the change. To avoid reporting incorrect changes, we instead report the watched directory - * being removed on the first event received after the move. To detect moved watched directories without - * having to wait for an event to happen inside the moved directory, the Java application can call - * stopWatchingMovedPaths(). - */ -class WatchPoint { -public: - WatchPoint(Server* server, size_t eventBufferSize, const wstring& path); - ~WatchPoint(); - - ListenResult listen(); - bool cancel(); - -private: - bool isValidDirectory(); - void close(); - - Server* server; - friend class Server; - - /** - * The path this watch point is registered with (same as the key of Server::watchPoints). - * It is the path passed from the Java side, and it has not been canonicalized or finalized. - * - * For SUBST drives this holds the substed location (e.g. `G:\watched`). - * - * @see registeredFinalPath - */ - const wstring registeredPath; - - /** - * The HANDLE of the directory being watched. - */ - HANDLE directoryHandle; - - /** - * The final path of the watched directory at registration, according to GetFinalPathNameByHandleW. - * - * This is a canonicalized path that is similar to Java's File.getCanonicalizedFile() in that it - * resoves symlinks. Unlike the Java method, GetFinalPathNameByHandleW also resolves SUBST drives - * to the locations they point to. - */ - wstring registeredFinalPath; - - /** - * OVERLAPPED structure used with ReadDirectoryChangesExW. - */ - OVERLAPPED overlapped; - - /** - * Event buffer used with ReadDirectoryChangesExW. - */ - vector eventBuffer; - - /** - * Whether the watch point is watching, has been cancelled or fully closed. - */ - WatchPointStatus status; - - void handleEventsInBuffer(DWORD errorCode, DWORD bytesTransferred); - friend static void CALLBACK handleEventCallback(DWORD errorCode, DWORD bytesTransferred, LPOVERLAPPED overlapped); -}; - -class Server : public AbstractServer { -public: - Server(JNIEnv* env, size_t eventBufferSize, long commandTimeoutInMillis, jobject watcherCallback); - - // List droppedPaths - void stopWatchingMovedPaths(jobject droppedPaths); - - void handleEvents(WatchPoint* watchPoint, DWORD errorCode, const vector& eventBuffer, DWORD bytesTransferred); - bool executeOnRunLoop(function command); - - virtual void registerPaths(const vector& paths) override; - virtual bool unregisterPaths(const vector& paths) override; - -protected: - void initializeRunLoop() override; - void runLoop() override; - void shutdownRunLoop() override; - -private: - void handleEvent(JNIEnv* env, const wstring& watchedPath, FILE_NOTIFY_EXTENDED_INFORMATION* info); - - void registerPath(const u16string& path); - bool unregisterPath(const u16string& path); - - void reportWatchPointDeleted(WatchPoint* watchPoint); - - HANDLE threadHandle; - const size_t eventBufferSize; - const long commandTimeoutInMillis; - unordered_map watchPoints; - bool shouldTerminate = false; - friend void CALLBACK executeOnRunLoopCallback(_In_ ULONG_PTR info); - jmethodID listAddMethod; -}; - -#endif diff --git a/file-events/src/main/java/net/rubygrapefruit/platform/file/FileEvents.java b/file-events/src/main/java/net/rubygrapefruit/platform/file/FileEvents.java deleted file mode 100644 index d8a3ccdf..00000000 --- a/file-events/src/main/java/net/rubygrapefruit/platform/file/FileEvents.java +++ /dev/null @@ -1,110 +0,0 @@ -package net.rubygrapefruit.platform.file; - -import net.rubygrapefruit.platform.NativeException; -import net.rubygrapefruit.platform.NativeIntegration; -import net.rubygrapefruit.platform.NativeIntegrationUnavailableException; -import net.rubygrapefruit.platform.ThreadSafe; -import net.rubygrapefruit.platform.internal.NativeLibraryLoader; -import net.rubygrapefruit.platform.internal.NativeLibraryLocator; -import net.rubygrapefruit.platform.internal.Platform; -import net.rubygrapefruit.platform.internal.jni.AbstractNativeFileEventFunctions; -import net.rubygrapefruit.platform.internal.jni.FileEventsVersion; -import net.rubygrapefruit.platform.internal.jni.LinuxFileEventFunctions; -import net.rubygrapefruit.platform.internal.jni.OsxFileEventFunctions; -import net.rubygrapefruit.platform.internal.jni.WindowsFileEventFunctions; - -import java.io.File; -import java.util.HashMap; -import java.util.Map; - -@ThreadSafe -public class FileEvents { - private static NativeLibraryLoader loader; - private static final Map, Object> integrations = new HashMap, Object>(); - - private FileEvents() { - } - - @ThreadSafe - static public void init(File extractDir) throws NativeIntegrationUnavailableException, NativeException { - synchronized (FileEvents.class) { - if (loader == null) { - Platform platform = Platform.current(); - try { - loader = new NativeLibraryLoader(platform, new NativeLibraryLocator(extractDir, FileEventsVersion.VERSION)); - loader.load(determineLibraryName(platform), platform.getLibraryVariants()); - String nativeVersion = AbstractNativeFileEventFunctions.getVersion(); - if (!nativeVersion.equals(FileEventsVersion.VERSION)) { - throw new NativeException(String.format( - "Unexpected native file events library version loaded. Expected %s, was %s.", - nativeVersion, - FileEventsVersion.VERSION - )); - } - } catch (NativeException e) { - throw e; - } catch (Throwable t) { - throw new NativeException("Failed to initialise native integration.", t); - } - } - } - } - - /** - * Locates a native integration of the given type. - * - * @return The native integration. Never returns null. - * @throws NativeIntegrationUnavailableException When the given native integration is not available on the current - * machine. - * @throws NativeException On failure to load the native integration. - */ - @ThreadSafe - public static T get(Class type) - throws NativeIntegrationUnavailableException, NativeException { - init(null); - synchronized (FileEvents.class) { - Platform platform = Platform.current(); - Object instance = integrations.get(type); - if (instance == null) { - try { - instance = getEventFunctions(type, platform); - } catch (NativeException e) { - throw e; - } catch (Throwable t) { - throw new NativeException(String.format("Failed to load native integration %s.", type.getSimpleName()), t); - } - integrations.put(type, instance); - } - return type.cast(instance); - } - } - - private static T getEventFunctions(Class type, Platform platform) { - if (platform.isWindows() && type.equals(WindowsFileEventFunctions.class)) { - return type.cast(new WindowsFileEventFunctions()); - } - if (platform.isLinux() && type.equals(LinuxFileEventFunctions.class)) { - return type.cast(new LinuxFileEventFunctions()); - } - if (platform.isMacOs() && type.equals(OsxFileEventFunctions.class)) { - return type.cast(new OsxFileEventFunctions()); - } - throw new NativeIntegrationUnavailableException(String.format( - "Native integration %s is not supported for %s.", - type.getSimpleName(), platform.toString()) - ); - } - - private static String determineLibraryName(Platform platform) { - if (platform.isLinux()) { - return "libnative-platform-file-events.so"; - } - if (platform.isMacOs()) { - return "libnative-platform-file-events.dylib"; - } - if (platform.isWindows()) { - return "native-platform-file-events.dll"; - } - throw new NativeIntegrationUnavailableException(String.format("Native file events integration is not available for %s.", platform)); - } -} diff --git a/file-events/src/main/java/net/rubygrapefruit/platform/file/FileWatchEvent.java b/file-events/src/main/java/net/rubygrapefruit/platform/file/FileWatchEvent.java deleted file mode 100644 index 7d4caa44..00000000 --- a/file-events/src/main/java/net/rubygrapefruit/platform/file/FileWatchEvent.java +++ /dev/null @@ -1,55 +0,0 @@ -package net.rubygrapefruit.platform.file; - -import javax.annotation.Nullable; - -public interface FileWatchEvent { - - void handleEvent(Handler handler); - - interface Handler { - void handleChangeEvent(ChangeType type, String absolutePath); - - void handleUnknownEvent(String absolutePath); - - void handleOverflow(OverflowType type, @Nullable String absolutePath); - - void handleFailure(Throwable failure); - - void handleTerminated(); - } - - enum ChangeType { - /** - * An item with the given path has been created. - */ - CREATED, - - /** - * An item with the given path has been removed. - */ - REMOVED, - - /** - * An item with the given path has been modified. - */ - MODIFIED, - - /** - * Some undisclosed changes happened under the given path, - * all information about descendants must be discarded. - */ - INVALIDATED - } - - enum OverflowType { - /** - * The overflow happened in the operating system's routines. - */ - OPERATING_SYSTEM, - - /** - * The overflow happened because the Java event queue has filled up. - */ - EVENT_QUEUE - } -} diff --git a/file-events/src/main/java/net/rubygrapefruit/platform/file/FileWatcher.java b/file-events/src/main/java/net/rubygrapefruit/platform/file/FileWatcher.java deleted file mode 100644 index 2d64120f..00000000 --- a/file-events/src/main/java/net/rubygrapefruit/platform/file/FileWatcher.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.rubygrapefruit.platform.file; - -import net.rubygrapefruit.platform.internal.jni.InsufficientResourcesForWatchingException; - -import javax.annotation.CheckReturnValue; -import javax.annotation.concurrent.NotThreadSafe; -import java.io.File; -import java.util.Collection; -import java.util.concurrent.TimeUnit; - -/** - * A handle for watching file system locations. - */ -@NotThreadSafe -public interface FileWatcher { - void initialize(long startTimeout, TimeUnit startTimeoutUnit) throws InterruptedException; - - void startWatching(Collection paths) throws InsufficientResourcesForWatchingException; - - @CheckReturnValue - boolean stopWatching(Collection paths); - - /** - * Initiates an orderly shutdown and release of any native resources. - * No more events will arrive after this method returns. - */ - void shutdown(); - - /** - * Blocks until the termination is complete after a {@link #shutdown()} - * request, or the timeout occurs, or the current thread is interrupted, - * whichever happens first. - * - * @param timeout the maximum time to wait - * @param unit the time unit of the timeout argument - * @return {@code true} if this watcher terminated and - * {@code false} if the timeout elapsed before termination - * @throws InterruptedException if interrupted while waiting - */ - @CheckReturnValue - boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException; -} diff --git a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/AbstractFileEventFunctions.java b/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/AbstractFileEventFunctions.java deleted file mode 100644 index ecf04e53..00000000 --- a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/AbstractFileEventFunctions.java +++ /dev/null @@ -1,336 +0,0 @@ -package net.rubygrapefruit.platform.internal.jni; - -import net.rubygrapefruit.platform.NativeException; -import net.rubygrapefruit.platform.NativeIntegration; -import net.rubygrapefruit.platform.file.FileWatchEvent; -import net.rubygrapefruit.platform.file.FileWatchEvent.OverflowType; -import net.rubygrapefruit.platform.file.FileWatcher; - -import javax.annotation.Nullable; -import java.io.File; -import java.util.Collection; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -import static java.util.concurrent.TimeUnit.SECONDS; - -public abstract class AbstractFileEventFunctions implements NativeIntegration { - public abstract static class AbstractWatcherBuilder { - public static final long DEFAULT_START_TIMEOUT_IN_SECONDS = 5; - - private final BlockingQueue eventQueue; - - public AbstractWatcherBuilder(BlockingQueue eventQueue) { - this.eventQueue = eventQueue; - } - - /** - * Start the file watcher. - * - * @throws FileWatcherTimeoutException if the watcher did not start up - * in {@value DEFAULT_START_TIMEOUT_IN_SECONDS} seconds. - * @throws InterruptedException if the current thread has been interrupted. - * - * @see FileWatcher#startWatching(Collection) - */ - public T start() throws InterruptedException { - return start(DEFAULT_START_TIMEOUT_IN_SECONDS, SECONDS); - } - - /** - * Start the file watcher with the given timeout. - * - * @throws FileWatcherTimeoutException if the watcher did not start up in - * the given timeout. - * @throws InterruptedException if the current thread has been interrupted. - * - * @see FileWatcher#startWatching(Collection) - */ - public T start(long startTimeout, TimeUnit startTimeoutUnit) throws InterruptedException, InsufficientResourcesForWatchingException { - NativeFileWatcherCallback callback = new NativeFileWatcherCallback(eventQueue); - T watcher = createWatcher(callback); - watcher.initialize(startTimeout, startTimeoutUnit); - return watcher; - } - - protected abstract T createWatcher(NativeFileWatcherCallback callback); - } - - /** - * Configures a new watcher using a builder. - * Call {@link AbstractWatcherBuilder#start()} to actually start the {@link FileWatcher}. - * - * The queue must have a total capacity of at least 2 elements. - * The caller should only consume events from the queue, and never add any of their own. - */ - public abstract AbstractWatcherBuilder newWatcher(BlockingQueue queue); - - protected static class NativeFileWatcherCallback { - - private final BlockingQueue eventQueue; - - public NativeFileWatcherCallback(BlockingQueue eventQueue) { - this.eventQueue = eventQueue; - } - - // Called from the native side - @SuppressWarnings("unused") - public void reportChangeEvent(int typeIndex, String path) { - FileWatchEvent.ChangeType type = FileWatchEvent.ChangeType.values()[typeIndex]; - queueEvent(new ChangeEvent(type, path), false); - } - - // Called from the native side - @SuppressWarnings("unused") - public void reportUnknownEvent(String path) { - queueEvent(new UnknownEvent(path), false); - } - - // Called from the native side - @SuppressWarnings("unused") - public void reportOverflow(@Nullable String path) { - signalOverflow(OverflowType.OPERATING_SYSTEM, path); - } - - // Called from the native side - @SuppressWarnings("unused") - public void reportFailure(Throwable ex) { - queueEvent(new FailureEvent(ex), true); - } - - // Called from the native side - @SuppressWarnings("unused") - public void reportTermination() { - queueEvent(TerminationEvent.INSTANCE, true); - } - - private void queueEvent(FileWatchEvent event, boolean deliverOnOverflow) { - if (!eventQueue.offer(event)) { - NativeLogger.LOGGER.info("Event queue overflow, dropping all events"); - signalOverflow(OverflowType.EVENT_QUEUE, null); - if (deliverOnOverflow) { - forceQueueEvent(event); - } - } - } - - private void signalOverflow(OverflowType type, @Nullable String path) { - eventQueue.clear(); - forceQueueEvent(new OverflowEvent(type, path)); - } - - /** - * Queue event to a queue that we expect has enough capacity to accept the event. - * We expect there is enough space because we just cleared the queue, and thus - * it should have enough space. - * - * This can fail if the queue is extremely small (has 0 capacity, or has a capacity of - * 1 and we are trying to queue an error event here right after an overflow event). - * The queue can also be full if some other thread is adding events to it. - * Both a queue with a less than two element capacity and pushing events from user code - * are forbidden. If they occur the best we can do is log the situation. - */ - private void forceQueueEvent(FileWatchEvent event) { - boolean eventPublished = eventQueue.offer(event); - if (!eventPublished) { - NativeLogger.LOGGER.severe("Couldn't queue event: " + event); - } - } - } - - protected static abstract class AbstractFileWatcher implements FileWatcher { - private final CountDownLatch runLoopInitialized = new CountDownLatch(1); - private final Thread processorThread; - private boolean shutdown; - - public AbstractFileWatcher(final NativeFileWatcherCallback callback) { - this.processorThread = new Thread("File watcher server") { - @Override - public void run() { - initializeRunLoop(); - runLoopInitialized.countDown(); - try { - executeRunLoop(); - if (!shutdown) { - callback.reportFailure(new FileWatcherException("File watcher server did exit without being shutdown")); - } - } catch (Throwable e) { - callback.reportFailure(e); - } finally { - callback.reportTermination(); - } - } - }; - this.processorThread.setDaemon(true); - } - - @Override - public void initialize(long startTimeout, TimeUnit startTimeoutUnit) throws InterruptedException { - this.processorThread.start(); - boolean started = runLoopInitialized.await(startTimeout, startTimeoutUnit); - if (!started) { - // Note: we don't close here because we have no idea what state the native backend is in - throw new FileWatcherTimeoutException("Starting the watcher timed out"); - } - } - - @Override - public void startWatching(Collection paths) { - ensureOpen(); - doStartWatching(paths); - } - - @Override - public boolean stopWatching(Collection paths) { - ensureOpen(); - return doStopWatching(paths); - } - - @Override - public void shutdown() { - ensureOpen(); - shutdown = true; - doShutdown(); - } - - @Override - public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { - long timeoutInMillis = unit.toMillis(timeout); - long startTime = System.currentTimeMillis(); - boolean successful = awaitTermination(timeoutInMillis); - if (successful) { - long endTime = System.currentTimeMillis(); - long remainingTimeout = timeoutInMillis - (endTime - startTime); - if (remainingTimeout > 0) { - processorThread.join(remainingTimeout); - } - return !processorThread.isAlive(); - } else { - return false; - } - } - - protected abstract void initializeRunLoop(); - - protected abstract void executeRunLoop(); - - protected abstract void doStartWatching(Collection paths); - - protected abstract boolean doStopWatching(Collection paths); - - protected abstract void doShutdown(); - - protected abstract boolean awaitTermination(long timeoutInMillis); - - private void ensureOpen() { - if (shutdown) { - throw new IllegalStateException("Watcher already closed"); - } - } - } - - private static class ChangeEvent implements FileWatchEvent { - private final ChangeType type; - private final String path; - - public ChangeEvent(ChangeType type, String path) { - this.type = type; - this.path = path; - } - - @Override - public void handleEvent(Handler handler) { - handler.handleChangeEvent(type, path); - } - - @Override - public String toString() { - return type + " " + path; - } - } - - private static class OverflowEvent implements FileWatchEvent { - private final OverflowType type; - private final String path; - - public OverflowEvent(OverflowType type, @Nullable String path) { - this.type = type; - this.path = path; - } - - @Override - public void handleEvent(Handler handler) { - handler.handleOverflow(type, path); - } - - @Override - public String toString() { - return "OVERFLOW (" + type + ") at " + path; - } - } - - private static class UnknownEvent implements FileWatchEvent { - private final String path; - - public UnknownEvent(String path) { - this.path = path; - } - - @Override - public void handleEvent(Handler handler) { - handler.handleUnknownEvent(path); - } - - @Override - public String toString() { - return "UNKNOWN " + path; - } - } - - private static class FailureEvent implements FileWatchEvent { - private final Throwable failure; - - public FailureEvent(Throwable failure) { - this.failure = failure; - } - - @Override - public void handleEvent(Handler handler) { - handler.handleFailure(failure); - } - - @Override - public String toString() { - return "FAILURE " + failure.getMessage(); - } - } - - private static class TerminationEvent implements FileWatchEvent { - public final static TerminationEvent INSTANCE = new TerminationEvent(); - - private TerminationEvent() {} - - @Override - public void handleEvent(Handler handler) { - handler.handleTerminated(); - } - - @Override - public String toString() { - return "TERMINATE"; - } - } - - public static class FileWatcherException extends NativeException { - public FileWatcherException(String message) { - super(message); - } - } - - public static class FileWatcherTimeoutException extends FileWatcherException { - public FileWatcherTimeoutException(String message) { - super(message); - } - } -} diff --git a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/AbstractNativeFileEventFunctions.java b/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/AbstractNativeFileEventFunctions.java deleted file mode 100644 index 8b162336..00000000 --- a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/AbstractNativeFileEventFunctions.java +++ /dev/null @@ -1,84 +0,0 @@ -package net.rubygrapefruit.platform.internal.jni; - -import net.rubygrapefruit.platform.file.FileWatcher; - -import java.io.File; -import java.util.Collection; - -public abstract class AbstractNativeFileEventFunctions extends AbstractFileEventFunctions { - public static String getVersion() { - return getVersion0(); - } - - private static native String getVersion0(); - - /** - * Forces the native backend to drop the cached JUL log level and thus - * re-query it the next time it tries to log something to the Java side. - */ - public void invalidateLogLevelCache() { - invalidateLogLevelCache0(); - } - - private native void invalidateLogLevelCache0(); - - protected static abstract class NativeFileWatcher extends AbstractFileWatcher { - protected final Object server; - - public NativeFileWatcher(Object server, NativeFileWatcherCallback callback) { - super(callback); - this.server = server; - } - - @Override - protected void initializeRunLoop() { - initializeRunLoop0(server); - } - - private native void initializeRunLoop0(Object server); - - @Override - protected void executeRunLoop() { - executeRunLoop0(server); - } - - private native void executeRunLoop0(Object server); - - @Override - protected void doStartWatching(Collection paths) { - startWatching0(server, toAbsolutePaths(paths)); - } - - private native void startWatching0(Object server, String[] absolutePaths); - - @Override - protected boolean doStopWatching(Collection paths) { - return stopWatching0(server, toAbsolutePaths(paths)); - } - - private native boolean stopWatching0(Object server, String[] absolutePaths); - - protected static String[] toAbsolutePaths(Collection files) { - String[] paths = new String[files.size()]; - int index = 0; - for (File file : files) { - paths[index++] = file.getAbsolutePath(); - } - return paths; - } - - @Override - protected void doShutdown() { - shutdown0(server); - } - - private native void shutdown0(Object server); - - @Override - protected boolean awaitTermination(long timeoutInMillis) { - return awaitTermination0(server, timeoutInMillis); - } - - private native boolean awaitTermination0(Object server, long timeoutInMillis); - } -} diff --git a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/InotifyInstanceLimitTooLowException.java b/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/InotifyInstanceLimitTooLowException.java deleted file mode 100644 index 929fd4a3..00000000 --- a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/InotifyInstanceLimitTooLowException.java +++ /dev/null @@ -1,11 +0,0 @@ -package net.rubygrapefruit.platform.internal.jni; - -/** - * A {@link InotifyInstanceLimitTooLowException} is thrown by {@link AbstractFileEventFunctions.AbstractWatcherBuilder#start()} - * when the inotify instance count is too low. - */ -public class InotifyInstanceLimitTooLowException extends InsufficientResourcesForWatchingException { - public InotifyInstanceLimitTooLowException(String message) { - super(message); - } -} diff --git a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/InotifyWatchesLimitTooLowException.java b/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/InotifyWatchesLimitTooLowException.java deleted file mode 100644 index 0309d2c0..00000000 --- a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/InotifyWatchesLimitTooLowException.java +++ /dev/null @@ -1,14 +0,0 @@ -package net.rubygrapefruit.platform.internal.jni; - -import java.util.Collection; - -/** - * A {@link InotifyInstanceLimitTooLowException} is thrown by {@link net.rubygrapefruit.platform.file.FileWatcher#startWatching(Collection)} - * when the inotify watches count is too low. - */ -@SuppressWarnings("unused") // Thrown from the native side -public class InotifyWatchesLimitTooLowException extends InsufficientResourcesForWatchingException { - public InotifyWatchesLimitTooLowException(String message) { - super(message); - } -} diff --git a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/LinuxFileEventFunctions.java b/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/LinuxFileEventFunctions.java deleted file mode 100644 index 030bd375..00000000 --- a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/LinuxFileEventFunctions.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2012 Adam Murdoch - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.rubygrapefruit.platform.internal.jni; - -import net.rubygrapefruit.platform.NativeIntegrationUnavailableException; -import net.rubygrapefruit.platform.file.FileWatchEvent; -import net.rubygrapefruit.platform.file.FileWatcher; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.BlockingQueue; - -/** - * File watcher for Linux. Reports changes to the watched paths and their immediate children. - * Changes to deeper descendants are not reported. - * - *

Remarks:

- * - *
    - *
  • Events arrive from a single background thread unique to the {@link FileWatcher}. - * Calling methods from the {@link FileWatcher} inside the callback method is undefined - * behavior and can lead to a deadlock.
  • - *
- */ -public class LinuxFileEventFunctions extends AbstractNativeFileEventFunctions { - - public LinuxFileEventFunctions() { - // We have seen some weird behavior on Alpine Linux that uses musl with Gradle that lead to crashes - // As a band-aid we currently don't support file events on Linux with a non-glibc libc. - // See also https://github.com/gradle/gradle/issues/17099 - if (!isGlibc0()) { - throw new NativeIntegrationUnavailableException("File events on Linux are only supported with glibc"); - } - } - - private static native boolean isGlibc0(); - - @Override - public WatcherBuilder newWatcher(BlockingQueue eventQueue) { - return new WatcherBuilder(eventQueue); - } - - public static class LinuxFileWatcher extends AbstractNativeFileEventFunctions.NativeFileWatcher { - public LinuxFileWatcher(Object server, NativeFileWatcherCallback callback) { - super(server, callback); - } - - /** - * Stops watching any directories that have been moved to a different path since registration, - * and returns the list of the registered paths that have been dropped. - */ - public List stopWatchingMovedPaths(Collection pathsToCheck) { - String[] absolutePathsToCheck = toAbsolutePaths(pathsToCheck); - List droppedPathStrings = new ArrayList(); - stopWatchingMovedPaths0(server, absolutePathsToCheck, droppedPathStrings); - List droppedPaths = new ArrayList(droppedPathStrings.size()); - for (String droppedPath : droppedPathStrings) { - droppedPaths.add(new File(droppedPath)); - } - return droppedPaths; - } - - private native void stopWatchingMovedPaths0(Object server, String[] absolutePathsToCheck, List droppedPaths); - } - - public static class WatcherBuilder extends AbstractWatcherBuilder { - WatcherBuilder(BlockingQueue eventQueue) { - super(eventQueue); - } - - @Override - protected LinuxFileWatcher createWatcher(NativeFileWatcherCallback callback) { - Object server = startWatcher0(callback); - return new LinuxFileWatcher(server, callback); - } - } - - private static native Object startWatcher0(NativeFileWatcherCallback callback); -} diff --git a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/NativeLogger.java b/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/NativeLogger.java deleted file mode 100644 index d80b9654..00000000 --- a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/NativeLogger.java +++ /dev/null @@ -1,58 +0,0 @@ -package net.rubygrapefruit.platform.internal.jni; - -import java.util.logging.Level; -import java.util.logging.Logger; - -// Used from native -@SuppressWarnings("unused") -public class NativeLogger { - static final Logger LOGGER = Logger.getLogger(NativeLogger.class.getName()); - - enum LogLevel { - ALL(Level.ALL), - FINEST(Level.FINEST), - FINER(Level.FINER), - FINE(Level.FINE), - CONFIG(Level.CONFIG), - INFO(Level.INFO), - WARNING(Level.WARNING), - SEVERE(Level.SEVERE), - OFF(Level.OFF); - - private final Level delegate; - - LogLevel(Level delegate) { - this.delegate = delegate; - } - - Level getLevel() { - return delegate; - } - } - - public static void log(int level, String message) { - LOGGER.log(LogLevel.values()[level].getLevel(), message); - } - - public static int getLogLevel() { - Logger effectiveLogger = LOGGER; - Level effectiveLevel; - while (true) { - effectiveLevel = effectiveLogger.getLevel(); - if (effectiveLevel != null) { - break; - } - effectiveLogger = effectiveLogger.getParent(); - if (effectiveLogger == null) { - throw new AssertionError("Effective log level is not set"); - } - } - - for (LogLevel logLevel : LogLevel.values()) { - if (logLevel.getLevel().equals(effectiveLevel)) { - return logLevel.ordinal(); - } - } - throw new AssertionError("Unknown effective log level found: " + effectiveLevel); - } -} diff --git a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/OsxFileEventFunctions.java b/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/OsxFileEventFunctions.java deleted file mode 100644 index b3451196..00000000 --- a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/OsxFileEventFunctions.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2012 Adam Murdoch - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.rubygrapefruit.platform.internal.jni; - -import net.rubygrapefruit.platform.file.FileWatchEvent; -import net.rubygrapefruit.platform.file.FileWatcher; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; - -/** - * File watcher for macOS. Reports changes to the watched paths and any of their descendants. - * - *

Remarks:

- * - *
    - *
  • Changes are reported as non-canonical paths. This means: - *
      - *
    • When watching a path with a different case, the reported case will match - * the one used in starting the watcher.
    • - *
    • Symlinks are not canonicalized, and changes are reported against the watched path.
    • - *
    - *
  • - * - *
  • Events arrive from a single background thread unique to the {@link FileWatcher}. - * Calling methods from the {@link FileWatcher} inside the callback method is undefined - * behavior and can lead to a deadlock.
  • - *
- */ -public class OsxFileEventFunctions extends AbstractNativeFileEventFunctions { - private static final long DEFAULT_LATENCY_IN_MS = 0; - - @Override - public WatcherBuilder newWatcher(BlockingQueue eventQueue) { - return new WatcherBuilder(eventQueue); - } - - public static class OsxFileWatcher extends AbstractNativeFileEventFunctions.NativeFileWatcher { - public OsxFileWatcher(Object server, NativeFileWatcherCallback callback) { - super(server, callback); - } - } - - public static class WatcherBuilder extends AbstractWatcherBuilder { - private long latencyInMillis = DEFAULT_LATENCY_IN_MS; - - WatcherBuilder(BlockingQueue eventQueue) { - super(eventQueue); - } - - /** - * Set the latency for handling events. - * The default is {@value DEFAULT_LATENCY_IN_MS} ms. - * - * @param latency coalesce events for the given amount of time, {@code 0} meaning no coalescing. - * @param unit the time unit for {@code latency}. - */ - public WatcherBuilder withLatency(long latency, TimeUnit unit) { - latencyInMillis = unit.toMillis(latency); - return this; - } - - @Override - protected OsxFileWatcher createWatcher(NativeFileWatcherCallback callback) { - Object server = startWatcher0(latencyInMillis, callback); - return new OsxFileWatcher(server, callback); - } - } - - private static native Object startWatcher0(long latencyInMillis, NativeFileWatcherCallback callback); -} diff --git a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/WindowsFileEventFunctions.java b/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/WindowsFileEventFunctions.java deleted file mode 100644 index 3123e8d4..00000000 --- a/file-events/src/main/java/net/rubygrapefruit/platform/internal/jni/WindowsFileEventFunctions.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2012 Adam Murdoch - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.rubygrapefruit.platform.internal.jni; - -import net.rubygrapefruit.platform.file.FileWatchEvent; -import net.rubygrapefruit.platform.file.FileWatcher; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; - -/** - * File watcher for Windows. Reports changes to the watched paths and any of their descendants. - * - *

Remarks:

- * - *
    - *
  • Changes are reported as canonical paths. When watching a path with a - * different case, the canonical one is used to report changes.
  • - * - *
  • When reporting - * {@link net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType#REMOVED REMOVED} - * events, Windows sometimes also reports a - * {@link net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType#MODIFIED MODIFIED} - * event for the same file. This can happen when deleting a file or renaming it.
  • - * - *
  • Events arrive from a single background thread unique to the {@link FileWatcher}. - * Calling methods from the {@link FileWatcher} inside the callback method is undefined - * behavior and can lead to a deadlock.
  • - *
- */ -public class WindowsFileEventFunctions extends AbstractNativeFileEventFunctions { - - public static final int DEFAULT_BUFFER_SIZE = 64 * 1024; - public static final int DEFAULT_COMMAND_TIMEOUT_IN_SECONDS = 5; - - @Override - public WatcherBuilder newWatcher(BlockingQueue eventQueue) { - return new WatcherBuilder(eventQueue); - } - - public static class WindowsFileWatcher extends AbstractNativeFileEventFunctions.NativeFileWatcher { - public WindowsFileWatcher(Object server, NativeFileWatcherCallback callback) { - super(server, callback); - } - - /** - * Stops watching any directory hierarchies that have been moved to a different path since registration, - * and returns the list of the registered paths that have been dropped. - */ - public List stopWatchingMovedPaths() { - List droppedPathStrings = new ArrayList(); - stopWatchingMovedPaths0(server, droppedPathStrings); - List droppedPaths = new ArrayList(droppedPathStrings.size()); - for (String droppedPath : droppedPathStrings) { - droppedPaths.add(new File(droppedPath)); - } - return droppedPaths; - } - - private native void stopWatchingMovedPaths0(Object server, List droppedPaths); - } - - public static class WatcherBuilder extends AbstractWatcherBuilder { - private int bufferSize = DEFAULT_BUFFER_SIZE; - private long commandTimeoutInMillis = TimeUnit.SECONDS.toMillis(DEFAULT_COMMAND_TIMEOUT_IN_SECONDS); - - private WatcherBuilder(BlockingQueue eventQueue) { - super(eventQueue); - } - - /** - * Set the buffer size used to collect events. - * Default value is {@value DEFAULT_BUFFER_SIZE} bytes. - */ - public WatcherBuilder withBufferSize(int bufferSize) { - this.bufferSize = bufferSize; - return this; - } - - /** - * Sets the timeout for commands to get scheduled on the run loop. - * - * Commands are {@link FileWatcher#startWatching(Collection)}, - * {@link FileWatcher#stopWatching(Collection)} and {@link FileWatcher#shutdown()}, - * The Windows file watcher relies on scheduling the execution of these commands - * on the background thread. - * - * Defaults to {@value DEFAULT_COMMAND_TIMEOUT_IN_SECONDS} seconds. - */ - public WatcherBuilder withCommandTimeout(int timeoutValue, TimeUnit timeoutUnit) { - this.commandTimeoutInMillis = timeoutUnit.toMillis(timeoutValue); - return this; - } - - @Override - protected WindowsFileWatcher createWatcher(NativeFileWatcherCallback callback) { - Object server = startWatcher0(bufferSize, commandTimeoutInMillis, callback); - return new WindowsFileWatcher(server, callback); - } - } - - private static native Object startWatcher0(int bufferSize, long commandTimeoutInMillis, NativeFileWatcherCallback callback); -} diff --git a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/AbstractFileEventFunctionsTest.groovy b/file-events/src/test/groovy/net/rubygrapefruit/platform/file/AbstractFileEventFunctionsTest.groovy deleted file mode 100644 index ec309db0..00000000 --- a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/AbstractFileEventFunctionsTest.groovy +++ /dev/null @@ -1,657 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.rubygrapefruit.platform.file - -import groovy.transform.Memoized -import net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType -import net.rubygrapefruit.platform.file.FileWatchEvent.OverflowType -import net.rubygrapefruit.platform.internal.Platform -import net.rubygrapefruit.platform.internal.jni.AbstractFileEventFunctions -import net.rubygrapefruit.platform.internal.jni.LinuxFileEventFunctions -import net.rubygrapefruit.platform.internal.jni.NativeLogger -import net.rubygrapefruit.platform.internal.jni.OsxFileEventFunctions -import net.rubygrapefruit.platform.internal.jni.WindowsFileEventFunctions -import net.rubygrapefruit.platform.testfixture.JniChecksEnabled -import net.rubygrapefruit.platform.testfixture.JulLogging -import org.junit.Assume -import org.junit.Rule -import org.junit.experimental.categories.Category -import org.junit.rules.TemporaryFolder -import org.junit.rules.TestName -import org.spockframework.util.Assert -import org.spockframework.util.Nullable -import spock.lang.Specification -import spock.lang.Timeout - -import java.util.concurrent.BlockingQueue -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit -import java.util.function.BooleanSupplier -import java.util.function.Predicate -import java.util.logging.Level -import java.util.logging.Logger -import java.util.regex.Pattern - -import static java.util.concurrent.TimeUnit.SECONDS -import static java.util.logging.Level.CONFIG -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.EventStatus.EXPECTED -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.EventStatus.UNEXPECTED - -@Timeout(value = 10, unit = SECONDS) -@Category(JniChecksEnabled) -abstract class AbstractFileEventFunctionsTest extends Specification { - - public static final Logger LOGGER = Logger.getLogger(AbstractFileEventFunctionsTest.name) - - @Rule - TemporaryFolder tmpDir = new TemporaryFolder(tmpDir()) - @Rule - TestName testName - @Rule - JulLogging logging = new JulLogging(NativeLogger, CONFIG) - - def eventQueue = newEventQueue() - File testDir - File rootDir - FileWatcher watcher - List uncaughtFailureOnThread - - private Map expectedLogMessages - - // We could do this with @Delegate, but Groovy doesn't let us :( - protected FileWatcherFixture watcherFixture - - private static File tmpDir() { - File tmpFile = new File(System.getProperty("test.directory")).absoluteFile - if (!tmpFile.directory) { - assert tmpFile.mkdirs() - } - return tmpFile - } - - def setup() { - uncaughtFailureOnThread = [] - expectedLogMessages = [:] - - def isJniTest = Boolean.getBoolean("testJni") - def isAmazonLinux = System.getProperty("agentName", "unknown").contains("Amazon") - Assume.assumeFalse("testJni doesn't seem to work on Amazon Linux", isJniTest && isAmazonLinux) - - watcherFixture = FileWatcherFixture.of(Platform.current()) - LOGGER.info(">>> Running '${testName.methodName}'") - testDir = tmpDir.newFolder(testName.methodName).canonicalFile - rootDir = new File(testDir, "root") - assert rootDir.mkdirs() - uncaughtFailureOnThread = [] - expectedLogMessages = [:] - } - - def cleanup() { - def isJniTest = Boolean.getBoolean("testJni") - def isAmazonLinux = System.getProperty("agentName", "unknown").contains("Amazon") - if (isJniTest && isAmazonLinux) { - return - } - - shutdownWatcher() - LOGGER.info("<<< Finished '${testName.methodName}'") - - uncaughtFailureOnThread.each { - it.printStackTrace() - } - // Avoid power assertion printing exceptions again - Assert.that(uncaughtFailureOnThread.empty, "There were uncaught exceptions, see stacktraces above") - - // Check if the logs (INFO and above) match our expectations - if (expectedLogMessages != null) { - Map unexpectedLogMessages = logging.messages - .findAll { message, level -> level.intValue() >= Level.INFO.intValue() } - def remainingExpectedLogMessages = new LinkedHashMap(expectedLogMessages) - unexpectedLogMessages.removeAll { message, level -> - remainingExpectedLogMessages.removeAll { expectedMessage, expectedLevel -> - expectedMessage.matcher(message).matches() && expectedLevel == level - } - } - Assert.that( - unexpectedLogMessages.isEmpty() && remainingExpectedLogMessages.isEmpty(), - createLogMessageFailure(unexpectedLogMessages, remainingExpectedLogMessages) - ) - } - } - - private static String createLogMessageFailure(Map unexpectedLogMessages, LinkedHashMap remainingExpectedLogMessages) { - String failure = "Log messages differ from expected:\n" - unexpectedLogMessages.each { message, level -> - failure += " - UNEXPECTED $level $message\n" - } - remainingExpectedLogMessages.each { message, level -> - failure += " - MISSING $level $message\n" - } - return failure - } - - void ignoreLogMessages() { - expectedLogMessages = null - } - - void expectLogMessage(Level level, String message) { - expectLogMessage(level, Pattern.compile(Pattern.quote(message))) - } - - void expectLogMessage(Level level, Pattern pattern) { - expectedLogMessages.put(pattern, level) - } - - enum FileWatcherFixture { - MAC_OS(){ - private static final int LATENCY_IN_MILLIS = 0 - - @Memoized - @Override - OsxFileEventFunctions getService() { - FileEvents.get(OsxFileEventFunctions) - } - - @Override - FileWatcher startNewWatcherInternal(BlockingQueue eventQueue, boolean preventOverflow) { - // Avoid setup operations to be reported - waitForChangeEventLatency() - service.newWatcher(eventQueue) - .withLatency(LATENCY_IN_MILLIS, TimeUnit.MILLISECONDS) - .start() - } - - @Override - void waitForChangeEventLatency() { - TimeUnit.MILLISECONDS.sleep(LATENCY_IN_MILLIS + 50) - } - }, - LINUX(){ - @Memoized - @Override - LinuxFileEventFunctions getService() { - FileEvents.get(LinuxFileEventFunctions) - } - - @Override - FileWatcher startNewWatcherInternal(BlockingQueue eventQueue, boolean preventOverflow) { - // Avoid setup operations to be reported - waitForChangeEventLatency() - service.newWatcher(eventQueue) - .start() - } - - @Override - void waitForChangeEventLatency() { - TimeUnit.MILLISECONDS.sleep(50) - } - }, - WINDOWS(){ - @Memoized - @Override - WindowsFileEventFunctions getService() { - FileEvents.get(WindowsFileEventFunctions) - } - - @Override - FileWatcher startNewWatcherInternal(BlockingQueue eventQueue, boolean preventOverflow) { - int bufferSizeInKb - if (preventOverflow) { - bufferSizeInKb = 16384 - AbstractFileEventFunctionsTest.LOGGER.info("Using $bufferSizeInKb kByte buffer to prevent overflow events"); - } else { - bufferSizeInKb = 16 - } - service.newWatcher(eventQueue) - .withBufferSize(bufferSizeInKb * 1024) - .start() - } - - @Override - void waitForChangeEventLatency() { - Thread.sleep(50) - } - }, - UNSUPPORTED() { - @Override - AbstractFileEventFunctions getService() { - throw new UnsupportedOperationException() - } - - @Override - FileWatcher startNewWatcherInternal(BlockingQueue eventQueue, boolean preventOverflow) { - throw new UnsupportedOperationException() - } - - @Override - void waitForChangeEventLatency() { - throw new UnsupportedOperationException() - } - } - - static FileWatcherFixture of(Platform platform) { - if (platform.macOs) { - return MAC_OS - } else if (platform.linux) { - return LINUX - } else if (platform.windows) { - return WINDOWS - } else { - return UNSUPPORTED - } - } - - abstract AbstractFileEventFunctions getService() - - abstract FileWatcher startNewWatcherInternal(BlockingQueue eventQueue, boolean preventOverflow) - - FileWatcher startNewWatcher(BlockingQueue eventQueue) { - startNewWatcherInternal(eventQueue, false) - } - - /** - * Create a watcher that has a larger buffer to avoid overflow events happening during stress tests. - * Overflow events are okay when we have lots of chagnes, but they make it impossible to test - * other behavior we care about in stress tests. - */ - FileWatcher startNewWatcherWithOverflowPrevention(BlockingQueue eventQueue) { - startNewWatcherInternal(eventQueue, true) - } - - abstract void waitForChangeEventLatency() - } - - protected static BlockingQueue newEventQueue() { - new LinkedBlockingQueue() - } - - private enum EventStatus { - EXPECTED, UNEXPECTED - } - - private interface ExpectedEvent { - boolean matches(FileWatchEvent event) - boolean isOptional() - } - - private class ExpectedChange implements ExpectedEvent { - private final ChangeType type - private final File file - final boolean optional - - ExpectedChange(ChangeType type, File file, boolean optional) { - this.type = type - this.file = file - this.optional = optional - } - - @Override - boolean matches(FileWatchEvent event) { - def matcher = new MatcherHandler() { - @Override - void handleChangeEvent(ChangeType type, String absolutePath) { - matched = ExpectedChange.this.type == type && ExpectedChange.this.file.absolutePath == absolutePath - } - } - event.handleEvent(matcher) - return matcher.matched - } - - @Override - String toString() { - return "${optional ? "optional " : ""}$type ${file == null ? null : shorten(file)}" - } - } - - private class ExpectedFailure implements ExpectedEvent { - private final Pattern message - private final Class type - - ExpectedFailure(Class type, Pattern message) { - this.type = type - this.message = message - } - - @Override - boolean matches(FileWatchEvent event) { - def matcher = new MatcherHandler() { - @Override - void handleFailure(Throwable failure) { - matched = type.isInstance(failure) && message.matcher(failure.message).matches() - } - } - event.handleEvent(matcher) - return matcher.matched - } - - @Override - boolean isOptional() { - false - } - - @Override - String toString() { - return "FAILURE /${message.pattern()}/" - } - } - - private class ExpectedTermination implements ExpectedEvent { - @Override - boolean matches(FileWatchEvent event) { - def matcher = new MatcherHandler() { - @Override - void handleTerminated() { - matched = true - } - } - event.handleEvent(matcher) - return matcher.matched - } - - @Override - boolean isOptional() { - return false - } - - @Override - String toString() { - return "TERMINATE" - } - } - - protected AbstractFileEventFunctions getService() { - watcherFixture.service - } - - protected void waitForChangeEventLatency() { - watcherFixture.waitForChangeEventLatency() - } - - protected void startWatcher(BlockingQueue eventQueue = this.eventQueue, File... roots) { - watcher = startNewWatcher(eventQueue, roots) - } - - protected FileWatcher startNewWatcher(BlockingQueue eventQueue = this.eventQueue, File... roots) { - def watcher = startNewWatcher(eventQueue) - watcher.startWatching(roots as List) - return watcher - } - - protected FileWatcher startNewWatcher(BlockingQueue eventQueue) { - watcherFixture.startNewWatcher(eventQueue) - } - - protected void startWatching(File... roots) { - watcher.startWatching(roots as List) - } - - protected boolean stopWatching(File... roots) { - return watcher.stopWatching(roots as List) - } - - protected void shutdownWatcher() { - def copyWatcher = watcher - watcher = null - if (copyWatcher != null) { - shutdownWatcher(copyWatcher) - } - } - - protected static void shutdownWatcher(FileWatcher watcher) { - watcher.shutdown() - assert watcher.awaitTermination(5, SECONDS) - } - - protected static T byPlatform(Map platforms) { - T match = platforms.find { platform, _ -> platform.matches(Platform.current()) }?.value - Assert.that(match != null, "No match for platform ${Platform.current()}") - return match - } - - private void ensureNoMoreEvents(BlockingQueue eventQueue = this.eventQueue) { - def receivedEvents = new ArrayList() - eventQueue.drainTo(receivedEvents) - Assert.that( - receivedEvents.empty, - createEventFailure(receivedEvents.collectEntries { event -> [event, UNEXPECTED]}, []) - ) - } - - protected void expectNoEvents(BlockingQueue eventQueue = this.eventQueue) { - // Let's make sure there are no events occurring, - // and we don't just miss them because of timing - waitForChangeEventLatency() - ensureNoMoreEvents(eventQueue) - } - - protected void expectEvents(BlockingQueue eventQueue = this.eventQueue, int timeoutValue = 1, TimeUnit timeoutUnit = SECONDS, ExpectedEvent... events) { - expectEvents(eventQueue, timeoutValue, timeoutUnit, events as List) - } - - protected void expectEvents(BlockingQueue eventQueue = this.eventQueue, int timeoutValue = 1, TimeUnit timeoutUnit = SECONDS, List expectedEvents) { - expectedEvents.each { expectedEvent -> - LOGGER.info("> Expecting $expectedEvent") - } - def remainingExpectedEvents = new ArrayList(expectedEvents) - Map receivedEvents = [:] - expectEvents( - eventQueue, - timeoutValue, - timeoutUnit, - { !remainingExpectedEvents.empty }, - { event -> - if (event == null) { - return false - } - LOGGER.info("> Received $event") - def expectedEventIndex = remainingExpectedEvents.findIndexOf { expectedEvent -> - expectedEvent.matches(event) - } - EventStatus status - if (expectedEventIndex == -1) { - status = UNEXPECTED - } else { - remainingExpectedEvents.remove(expectedEventIndex) - status = EXPECTED - } - receivedEvents.put(event, status) - return true - }) - Assert.that( - remainingExpectedEvents.every { it.optional } && receivedEvents.values().every { it == EXPECTED }, - createEventFailure(receivedEvents, remainingExpectedEvents) - ) - ensureNoMoreEvents(eventQueue) - } - - private String createEventFailure(Map receivedEvents, List remainingExpectedEvents) { - String failure = "Events received differ from expected:\n" - receivedEvents.each { event, status -> - failure += " - ${status == EXPECTED ? "RECEIVED " : "UNEXPECTED"} ${format(event)}\n" - } - remainingExpectedEvents.each { event -> - failure += " - MISSING $event\n" - } - return failure - } - - protected void expectEvents( - BlockingQueue eventQueue = this.eventQueue, - int timeoutValue = 1, - TimeUnit timeoutUnit = SECONDS, - BooleanSupplier shouldContinue = { true }, - Predicate eventHandler - ) { - long start = System.currentTimeMillis() - long end = start + timeoutUnit.toMillis(timeoutValue) - while (shouldContinue.asBoolean) { - def current = System.currentTimeMillis() - long timeout = end - current - def event = eventQueue.poll(timeout, TimeUnit.MILLISECONDS) - if (!eventHandler.test(event)) { - break - } - } - } - - protected String format(FileWatchEvent event) { - String shortened = null - event.handleEvent(new FileWatchEvent.Handler() { - @Override - void handleChangeEvent(ChangeType type, String absolutePath) { - shortened = type.name() + " " + shorten(absolutePath) - } - - @Override - void handleUnknownEvent(@Nullable String absolutePath) { - shortened = "UNKNOWN ${shorten(absolutePath)}" - } - - @Override - void handleOverflow(OverflowType type, @Nullable String absolutePath) { - shortened = "OVERFLOW ${shorten(absolutePath)} ($type)" - } - - @Override - void handleFailure(Throwable failure) { - shortened = "FAILURE $failure" - } - - @Override - void handleTerminated() { - shortened = "TERMINATE" - } - }) - assert shortened != null - return shortened - } - - protected String shorten(File file) { - shorten(file.absolutePath) - } - - protected String shorten(@Nullable String path) { - if (path == null) { - return "null" - } - def prefix = testDir.absolutePath - return path.startsWith(prefix + File.separator) - ? "..." + path.substring(prefix.length()) - : path - } - - protected ExpectedEvent change(ChangeType type, File file) { - new ExpectedChange(type, file, false) - } - - protected ExpectedEvent optionalChange(ChangeType type, File file) { - return new ExpectedChange(type, file, true) - } - - protected ExpectedEvent failure(Class type = Exception, String message) { - failure(type, Pattern.quote(message)) - } - - protected ExpectedEvent failure(Class type = Exception, Pattern message) { - return new ExpectedFailure(type, message) - } - - protected ExpectedEvent termination() { - return new ExpectedTermination() - } - - protected void createNewFile(File file) { - LOGGER.info("> Creating ${shorten(file)}") - file.createNewFile() - LOGGER.info("< Created ${shorten(file)}") - } - - private static class MatcherHandler implements FileWatchEvent.Handler { - boolean matched - - @Override - void handleChangeEvent(ChangeType type, String absolutePath) {} - - @Override - void handleOverflow(OverflowType type, @Nullable String absolutePath) {} - - @Override - void handleUnknownEvent(@Nullable String absolutePath) {} - - @Override - void handleFailure(Throwable failure) {} - - @Override - void handleTerminated() {} - } - - protected static class TestHandler implements FileWatchEvent.Handler { - @Override - void handleChangeEvent(ChangeType type, String absolutePath) { - throw new IllegalStateException(String.format("Received unexpected change with %s / %s", type, absolutePath)) - } - - @Override - void handleOverflow(OverflowType type, @Nullable String absolutePath) { - throw new IllegalStateException(String.format("Received unexpected %s overflow at %s", type, absolutePath)) - } - - @Override - void handleUnknownEvent(@Nullable String absolutePath) { - throw new IllegalStateException(String.format("Received unexpected unknown event at %s", absolutePath)) - } - - @Override - void handleFailure(Throwable failure) { - throw new IllegalStateException(String.format("Received unexpected failure", failure)) - } - - @Override - void handleTerminated() { - throw new IllegalStateException("Received unexpected termination") - } - } - - protected static enum PlatformType { - LINUX() { - @Override - boolean matches(Platform platform) { - return platform.linux - } - }, - MAC_OS() { - @Override - boolean matches(Platform platform) { - return platform.macOs - } - }, - WINDOWS() { - @Override - boolean matches(Platform platform) { - return platform.windows - } - }, - OTHERWISE() { - @Override - boolean matches(Platform platform) { - return true - } - }; - - abstract boolean matches(Platform platform) - } -} diff --git a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/BasicFileEventFunctionsTest.groovy b/file-events/src/test/groovy/net/rubygrapefruit/platform/file/BasicFileEventFunctionsTest.groovy deleted file mode 100644 index 1ce23c87..00000000 --- a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/BasicFileEventFunctionsTest.groovy +++ /dev/null @@ -1,800 +0,0 @@ -/* - * Copyright 2012 Adam Murdoch - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package net.rubygrapefruit.platform.file - -import net.rubygrapefruit.platform.NativeException -import net.rubygrapefruit.platform.internal.Platform -import net.rubygrapefruit.platform.internal.jni.AbstractFileEventFunctions -import net.rubygrapefruit.platform.internal.jni.NativeLogger -import org.junit.Assume -import spock.lang.IgnoreIf -import spock.lang.Requires -import spock.lang.Unroll - -import java.util.concurrent.BlockingQueue -import java.util.concurrent.TimeUnit -import java.util.logging.Level -import java.util.logging.Logger -import java.util.regex.Pattern - -import static java.util.concurrent.TimeUnit.SECONDS -import static java.util.logging.Level.INFO -import static java.util.logging.Level.SEVERE -import static java.util.logging.Level.WARNING -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.PlatformType.OTHERWISE -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.PlatformType.WINDOWS -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.CREATED -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.MODIFIED -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.REMOVED - -@Unroll -@Requires({ Platform.current().macOs || Platform.current().linux || Platform.current().windows }) -class BasicFileEventFunctionsTest extends AbstractFileEventFunctionsTest { - def "can start and shutdown watcher without watching any paths"() { - when: - def watcher = startNewWatcher() - - then: - noExceptionThrown() - - when: - shutdownWatcher(watcher) - - then: - expectEvents termination() - } - - def "can start and shutdown watcher on a directory without receiving any events"() { - when: - def watcher = startNewWatcher(rootDir) - - then: - noExceptionThrown() - - when: - shutdownWatcher(watcher) - - then: - expectEvents termination() - } - - def "can detect file created"() { - given: - def createdFile = new File(rootDir, "created.txt") - startWatcher(rootDir) - - when: - createNewFile(createdFile) - - then: - expectEvents change(CREATED, createdFile) - } - - @IgnoreIf({ Platform.current().linux }) - def "can detect file created in subdirectory"() { - given: - def subDir = new File(rootDir, "sub-dir") - subDir.mkdirs() - def createdFile = new File(subDir, "created.txt") - startWatcher(rootDir) - - when: - createNewFile(createdFile) - - then: - expectEvents change(CREATED, createdFile) - } - - def "can detect directory created"() { - given: - def createdDir = new File(rootDir, "created") - startWatcher(rootDir) - - when: - assert createdDir.mkdirs() - - then: - expectEvents change(CREATED, createdDir) - } - - @IgnoreIf({ Platform.current().linux }) - def "can detect directory created in subdirectory"() { - given: - def subDir = new File(rootDir, "sub-dir") - subDir.mkdirs() - def createdDir = new File(subDir, "created") - startWatcher(rootDir) - - when: - assert createdDir.mkdirs() - - then: - expectEvents change(CREATED, createdDir) - } - - def "can detect file removed"() { - given: - def removedFile = new File(rootDir, "removed.txt") - createNewFile(removedFile) - startWatcher(rootDir) - - when: - removedFile.delete() - - then: - // Windows reports the file as modified before removing it - expectEvents byPlatform( - (WINDOWS): [change(MODIFIED, removedFile), change(REMOVED, removedFile)], - (OTHERWISE): [change(REMOVED, removedFile)] - ) - } - - @IgnoreIf({ Platform.current().linux }) - def "can detect file removed in subdirectory"() { - given: - def subDir = new File(rootDir, "sub-dir") - subDir.mkdirs() - def removedFile = new File(subDir, "removed.txt") - createNewFile(removedFile) - startWatcher(rootDir) - - when: - removedFile.delete() - - then: - // Windows reports the file as modified before removing it - expectEvents byPlatform( - (WINDOWS): [change(MODIFIED, removedFile), change(REMOVED, removedFile)], - (OTHERWISE): [change(REMOVED, removedFile)] - ) - } - - def "can detect directory removed"() { - given: - def removedDir = new File(rootDir, "removed") - assert removedDir.mkdirs() - startWatcher(rootDir) - - when: - removedDir.deleteDir() - - then: - expectEvents change(REMOVED, removedDir) - } - - @IgnoreIf({ Platform.current().linux }) - def "can detect directory removed in subdirectory"() { - given: - def subDir = new File(rootDir, "sub-dir") - subDir.mkdirs() - def removedDir = new File(subDir, "removed") - assert removedDir.mkdirs() - startWatcher(rootDir) - - when: - removedDir.deleteDir() - - then: - expectEvents change(REMOVED, removedDir) - } - - @IgnoreIf({ Platform.current().linux }) - def "can detect hierarchy removed"() { - given: - def removedDir = new File(rootDir, "removed") - assert removedDir.mkdirs() - def removedSubDir = new File(removedDir, "sub-dir") - assert removedSubDir.mkdirs() - def removedSubSubDir = new File(removedSubDir, "sub-sub-dir") - assert removedSubSubDir.mkdirs() - def removedFile = new File(removedSubSubDir, "file.txt") - createNewFile(removedFile) - startWatcher(rootDir) - - when: - removedDir.deleteDir() - - then: - expectEvents byPlatform( - (WINDOWS): [change(MODIFIED, removedFile), change(REMOVED, removedFile), change(REMOVED, removedSubSubDir), change(REMOVED, removedSubDir), change(REMOVED, removedDir)], - (OTHERWISE): [change(REMOVED, removedFile), change(REMOVED, removedSubSubDir), change(REMOVED, removedSubDir), change(REMOVED, removedDir)] - ) - } - - def "can detect file modified"() { - given: - def modifiedFile = new File(rootDir, "modified.txt") - createNewFile(modifiedFile) - startWatcher(rootDir) - - when: - modifiedFile << "change" - - then: - expectEvents change(MODIFIED, modifiedFile) - } - - @IgnoreIf({ Platform.current().linux }) - def "can detect file modified in subdirectory"() { - given: - def subDir = new File(rootDir, "sub-dir") - subDir.mkdirs() - def modifiedFile = new File(subDir, "modified.txt") - createNewFile(modifiedFile) - startWatcher(rootDir) - - when: - modifiedFile << "change" - - then: - expectEvents change(MODIFIED, modifiedFile) - } - - @Requires({ Platform.current().macOs }) - def "can detect file metadata modified"() { - given: - def modifiedFile = new File(rootDir, "modified.txt") - createNewFile(modifiedFile) - startWatcher(rootDir) - - when: - modifiedFile.setReadable(false) - - then: - expectEvents change(MODIFIED, modifiedFile) - - when: - modifiedFile.setReadable(true) - - then: - expectEvents change(MODIFIED, modifiedFile) - } - - @Requires({ Platform.current().macOs }) - def "changing metadata immediately after creation is reported as modified"() { - given: - def createdFile = new File(rootDir, "file.txt") - startWatcher(rootDir) - - when: - createNewFile(createdFile) - createdFile.setReadable(false) - - then: - expectEvents change(MODIFIED, createdFile) - } - - @Requires({ Platform.current().macOs }) - def "changing metadata doesn't mask content change"() { - given: - def modifiedFile = new File(rootDir, "modified.txt") - modifiedFile.createNewFile() - startWatcher(rootDir) - - when: - modifiedFile.setReadable(false) - modifiedFile << "change" - - then: - expectEvents change(MODIFIED, modifiedFile) - } - - @Requires({ Platform.current().macOs }) - def "changing metadata doesn't mask removal"() { - given: - def removedFile = new File(rootDir, "removed.txt") - createNewFile(removedFile) - startWatcher(rootDir) - - when: - removedFile.setReadable(false) - assert removedFile.delete() - - then: - expectEvents change(REMOVED, removedFile) - } - - def "can detect file renamed"() { - given: - def sourceFile = new File(rootDir, "source.txt") - def targetFile = new File(rootDir, "target.txt") - createNewFile(sourceFile) - startWatcher(rootDir) - - when: - sourceFile.renameTo(targetFile) - - then: - expectEvents change(REMOVED, sourceFile), change(CREATED, targetFile) - } - - @IgnoreIf({ Platform.current().linux }) - def "can detect file renamed in subdirectory"() { - given: - def subDir = new File(rootDir, "sub-dir") - subDir.mkdirs() - def sourceFile = new File(subDir, "source.txt") - def targetFile = new File(subDir, "target.txt") - createNewFile(sourceFile) - startWatcher(rootDir) - - when: - sourceFile.renameTo(targetFile) - - then: - expectEvents change(REMOVED, sourceFile), change(CREATED, targetFile) - } - - def "can detect file moved out"() { - given: - def outsideDir = new File(testDir, "outside") - assert outsideDir.mkdirs() - def sourceFileInside = new File(rootDir, "source-inside.txt") - def targetFileOutside = new File(outsideDir, "target-outside.txt") - createNewFile(sourceFileInside) - startWatcher(rootDir) - - when: - sourceFileInside.renameTo(targetFileOutside) - - then: - expectEvents change(REMOVED, sourceFileInside) - } - - def "can detect file moved in"() { - given: - def outsideDir = new File(testDir, "outside") - assert outsideDir.mkdirs() - def sourceFileOutside = new File(outsideDir, "source-outside.txt") - def targetFileInside = new File(rootDir, "target-inside.txt") - createNewFile(sourceFileOutside) - startWatcher(rootDir) - - when: - sourceFileOutside.renameTo(targetFileInside) - - then: - expectEvents change(CREATED, targetFileInside) - } - - def "can receive multiple events from the same directory"() { - given: - def firstFile = new File(rootDir, "first.txt") - def secondFile = new File(rootDir, "second.txt") - startWatcher(rootDir) - - when: - createNewFile(firstFile) - - then: - expectEvents change(CREATED, firstFile) - - when: - waitForChangeEventLatency() - createNewFile(secondFile) - - then: - expectEvents change(CREATED, secondFile) - } - - def "does not receive events from unwatched directory"() { - given: - def watchedFile = new File(rootDir, "watched.txt") - def unwatchedDir = new File(testDir, "unwatched") - assert unwatchedDir.mkdirs() - def unwatchedFile = new File(unwatchedDir, "unwatched.txt") - startWatcher(rootDir) - - when: - createNewFile(unwatchedFile) - createNewFile(watchedFile) - // Let's make sure there are no events for the unwatched file, - // and we don't just miss them because of timing - waitForChangeEventLatency() - - then: - expectEvents change(CREATED, watchedFile) - } - - // Apparently on macOS we can watch non-existent directories - // TODO Should we fail for this? - @IgnoreIf({ Platform.current().macOs }) - def "fails when watching non-existent directory"() { - given: - def missingDirectory = new File(rootDir, "missing") - - when: - startWatcher(missingDirectory) - - then: - def ex = thrown NativeException - ex.message ==~ /Couldn't add watch.*: ${Pattern.quote(missingDirectory.absolutePath)}/ - - expectLogMessage(SEVERE, Pattern.compile("Caught exception: Couldn't add watch.*: ${Pattern.quote(missingDirectory.absolutePath)}")) - } - - // Apparently on macOS we can watch files - // TODO Should we fail for this? - @IgnoreIf({ Platform.current().macOs }) - def "fails when watching file"() { - given: - def file = new File(rootDir, "file.txt") - assert file.createNewFile() - - when: - startWatcher(file) - - then: - def ex = thrown NativeException - ex.message ==~ /Couldn't add watch.*: ${Pattern.quote(file.absolutePath)}/ - - expectLogMessage(SEVERE, Pattern.compile("Caught exception: Couldn't add watch.*: ${Pattern.quote(file.absolutePath)}")) - } - - def "fails when watching directory twice"() { - given: - startWatcher(rootDir) - - when: - startWatching(rootDir) - - then: - def ex = thrown NativeException - ex.message == "Already watching path: ${rootDir.absolutePath}" - - expectLogMessage(SEVERE, "Caught exception: Already watching path: ${rootDir.absolutePath}") - } - - def "can un-watch path that was not watched"() { - given: - startWatcher() - - expect: - !stopWatching(rootDir) - - expectLogMessage(INFO, "Path is not watched: ${rootDir.absolutePath}") - } - - def "can un-watch watched directory twice"() { - given: - startWatcher(rootDir) - - expect: - stopWatching(rootDir) - !stopWatching(rootDir) - - expectLogMessage(INFO, "Path is not watched: ${rootDir.absolutePath}") - } - - def "does not receive events after directory is unwatched"() { - given: - def file = new File(rootDir, "first.txt") - startWatcher(rootDir) - - expect: - stopWatching(rootDir) - - when: - createNewFile(file) - - then: - expectNoEvents() - } - - def "can receive multiple events from multiple watched directories"() { - given: - def firstWatchedDir = new File(testDir, "first") - assert firstWatchedDir.mkdirs() - def firstFileInFirstWatchedDir = new File(firstWatchedDir, "first-watched.txt") - def secondWatchedDir = new File(testDir, "second") - assert secondWatchedDir.mkdirs() - def secondFileInSecondWatchedDir = new File(secondWatchedDir, "sibling-watched.txt") - startWatcher(firstWatchedDir, secondWatchedDir) - - when: - createNewFile(firstFileInFirstWatchedDir) - - then: - expectEvents change(CREATED, firstFileInFirstWatchedDir) - - when: - createNewFile(secondFileInSecondWatchedDir) - - then: - expectEvents change(CREATED, secondFileInSecondWatchedDir) - } - - @Requires({ !Platform.current().linux }) - def "can receive events from directory with different casing"() { - given: - def lowercaseDir = new File(rootDir, "watch-this") - def uppercaseDir = new File(rootDir, "WATCH-THIS") - def fileInLowercaseDir = new File(lowercaseDir, "lowercase.txt") - def fileInUppercaseDir = new File(uppercaseDir, "UPPERCASE.TXT") - uppercaseDir.mkdirs() - - def reportedDir = Platform.current().macOs - ? uppercaseDir - : lowercaseDir - - startWatcher(lowercaseDir) - - when: - createNewFile(fileInLowercaseDir) - - then: - expectEvents change(CREATED, new File(reportedDir, fileInLowercaseDir.name)) - - when: - createNewFile(fileInUppercaseDir) - - then: - expectEvents change(CREATED, new File(reportedDir, fileInUppercaseDir.name)) - } - - def "fails when stopped multiple times"() { - given: - def watcher = startNewWatcher() - shutdownWatcher(watcher) - - when: - watcher.shutdown() - - then: - def ex = thrown IllegalStateException - ex.message == "Watcher already closed" - } - - def "can be used multiple times"() { - given: - def firstFile = new File(rootDir, "first.txt") - def secondFile = new File(rootDir, "second.txt") - startWatcher(rootDir) - - when: - createNewFile(firstFile) - - then: - expectEvents change(CREATED, firstFile) - - when: - shutdownWatcher() - - then: - expectEvents termination() - - when: - startWatcher(rootDir) - createNewFile(secondFile) - - then: - expectEvents change(CREATED, secondFile) - } - - def "can start multiple watchers"() { - given: - def firstRoot = new File(rootDir, "first") - firstRoot.mkdirs() - def secondRoot = new File(rootDir, "second") - secondRoot.mkdirs() - def firstFile = new File(firstRoot, "file.txt") - def secondFile = new File(secondRoot, "file.txt") - def firstQueue = newEventQueue() - def secondQueue = newEventQueue() - - LOGGER.info("> Starting first watcher") - def firstWatcher = startNewWatcher(firstQueue) - firstWatcher.startWatching([firstRoot]) - LOGGER.info("> Starting second watcher") - def secondWatcher = startNewWatcher(secondQueue) - secondWatcher.startWatching([secondRoot]) - LOGGER.info("> Watchers started") - - when: - createNewFile(firstFile) - - then: - expectEvents firstQueue, change(CREATED, firstFile) - - when: - createNewFile(secondFile) - - then: - expectEvents secondQueue, change(CREATED, secondFile) - - cleanup: - shutdownWatcher(firstWatcher) - shutdownWatcher(secondWatcher) - } - - @Requires({ !Platform.current().linux }) - def "can receive event about a non-direct descendant change"() { - given: - def subDir = new File(rootDir, "sub-dir") - subDir.mkdirs() - def fileInSubDir = new File(subDir, "watched-descendant.txt") - startWatcher(rootDir) - - when: - createNewFile(fileInSubDir) - - then: - expectEvents change(CREATED, fileInSubDir) - } - - @Requires({ Platform.current().linux }) - def "does not receive event about a non-direct descendant change"() { - given: - def subDir = new File(rootDir, "sub-dir") - subDir.mkdirs() - def fileInSubDir = new File(subDir, "unwatched-descendant.txt") - - when: - createNewFile(fileInSubDir) - - then: - expectNoEvents() - } - - def "can watch directory with long path"() { - given: - def subDir = new File(rootDir, "long-path") - 4.times { - subDir = new File(subDir, "X" * 200) - } - subDir.mkdirs() - def fileInSubDir = new File(subDir, "watched-descendant.txt") - startWatcher(subDir) - - when: - createNewFile(fileInSubDir) - - then: - expectEvents change(CREATED, fileInSubDir) - } - - def "can watch directory with #type characters"() { - Assume.assumeTrue(supported as boolean) - - given: - def subDir = new File(rootDir, path) - subDir.mkdirs() - def fileInSubDir = new File(subDir, path) - startWatcher(subDir) - - when: - createNewFile(fileInSubDir) - - then: - expectEvents change(CREATED, fileInSubDir) - - where: - type | path | supported - "ASCII-only" | "directory" | true - "Chinese" | "输入文件" | true - "four-byte UTF8" | "𠜎𠜱𠝹𠱓" | true - "Hungarian" | "Dezső" | true - "space" | "test directory" | true - "zwnj" | "test\u200cdirectory" | true - "newline" | "test\ndirectory" | Platform.current().macOs - "URL-quoted" | "test%#2.txt" | !Platform.current().windows - } - - def "can set log level by #action"() { - given: - def nativeLogger = Logger.getLogger(NativeLogger.name) - def originalLevel = nativeLogger.level - def fileChanged = new File(rootDir, "changed.txt") - fileChanged.createNewFile() - - when: - logging.clear() - nativeLogger.level = Level.FINEST - ensureLogLevelInvalidated(service) - startWatcher(rootDir) - fileChanged << "changed" - waitForChangeEventLatency() - - then: - logging.messages.values().any { it == Level.FINE } - - when: - shutdownWatcher() - logging.clear() - nativeLogger.level = WARNING - ensureLogLevelInvalidated(service) - startWatcher() - fileChanged << "changed again" - waitForChangeEventLatency() - - then: - !logging.messages.values().any { it == Level.FINE } - - cleanup: - nativeLogger.level = originalLevel - - where: - action | ensureLogLevelInvalidated - "invalidating the log level cache" | { AbstractFileEventFunctions service -> service.invalidateLogLevelCache() } - "waiting for log level cache to time out" | { Thread.sleep(1500) } - } - - def "handles queue not able to take any events"() { - given: - def notAcceptingQueue = Stub(BlockingQueue) { - _ * offer(_ as FileWatchEvent, _ as long, _ as TimeUnit) >> false - _ * offer(_ as FileWatchEvent) >> false - } - def fileCreated = new File(rootDir, "changed.txt") - def watcher = startNewWatcher(notAcceptingQueue, rootDir) - - when: - fileCreated.createNewFile() - waitForChangeEventLatency() - - then: - expectLogMessage(INFO, "Event queue overflow, dropping all events") - expectLogMessage(SEVERE, "Couldn't queue event: OVERFLOW (EVENT_QUEUE) at null") - - when: - shutdownWatcher(watcher) - - then: - expectLogMessage(INFO, "Event queue overflow, dropping all events") - expectLogMessage(SEVERE, "Couldn't queue event: OVERFLOW (EVENT_QUEUE) at null") - expectLogMessage(SEVERE, "Couldn't queue event: TERMINATE") - } - - def "can handle watcher start timing out"() { - when: - service.newWatcher(eventQueue).start(0, SECONDS) - - then: - def ex = thrown AbstractFileEventFunctions.FileWatcherTimeoutException - ex.message == "Starting the watcher timed out" - } - - def "can detect events in directory removed then re-added"() { - given: - def watchedDir = new File(rootDir, "watched") - assert watchedDir.mkdirs() - def createdFile = new File(watchedDir, "created.txt") - startWatcher(watchedDir) - - def directoryRemoved = watchedDir.delete() - def directoryRecreated = watchedDir.mkdirs() - // On Windows we don't always manage to remove the watched directory, it's unreliable - if (!Platform.current().windows) { - assert directoryRemoved - assert directoryRecreated - } - waitForChangeEventLatency() - - // Restart watching freshly recreated directory on platforms that auto-unregister on deletion - if (!Platform.current().macOs) { - watcher.startWatching([watchedDir]) - } - // Ignore events received during setup - waitForChangeEventLatency() - eventQueue.clear() - - when: - createdFile.createNewFile() - - then: - expectEvents change(CREATED, createdFile) - } -} diff --git a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/FileEventFunctionsOverflowTest.groovy b/file-events/src/test/groovy/net/rubygrapefruit/platform/file/FileEventFunctionsOverflowTest.groovy deleted file mode 100644 index edff9f11..00000000 --- a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/FileEventFunctionsOverflowTest.groovy +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.rubygrapefruit.platform.file - -import net.rubygrapefruit.platform.internal.Platform -import org.spockframework.util.Nullable -import spock.lang.Ignore -import spock.lang.Requires - -import java.util.concurrent.ArrayBlockingQueue -import java.util.concurrent.BlockingQueue -import java.util.concurrent.CountDownLatch -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit - -import static java.util.concurrent.TimeUnit.SECONDS -import static java.util.logging.Level.INFO -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.CREATED - -@Requires({ Platform.current().macOs || Platform.current().linux || Platform.current().windows }) -class FileEventFunctionsOverflowTest extends AbstractFileEventFunctionsTest { - - @Ignore("Flaky") - def "delivers more events after overflow event"() { - given: - // We don't want to fail when overflow is logged - ignoreLogMessages() - - def afterOverflowFile = new File(rootDir, "after-overflow.txt") - - def numberOfParallelWriters = 10 - def executorService = Executors.newFixedThreadPool(numberOfParallelWriters) - def readyLatch = new CountDownLatch(numberOfParallelWriters) - def finishedLatch = new CountDownLatch(numberOfParallelWriters) - def startModifyingLatch = new CountDownLatch(1) - numberOfParallelWriters.times { index -> - executorService.submit({ -> - def fileToChange = new File(rootDir, "file-${index}") - readyLatch.countDown() - startModifyingLatch.await() - 2000.times { - fileToChange.delete() - fileToChange.createNewFile() - } - finishedLatch.countDown() - }) - } - - when: - startWatcher(rootDir) - readyLatch.await() - startModifyingLatch.countDown() - finishedLatch.await(5, SECONDS) - waitForChangeEventLatency() - - then: - expectOverflow(5, SECONDS) - - when: - LOGGER.info("> Making change after overflow") - afterOverflowFile.createNewFile() - - then: - expectEvents change(CREATED, afterOverflowFile) - - cleanup: - executorService.shutdown() - } - - def "handles Java-side event queue overflowing"() { - given: - def singleElementQueue = new ArrayBlockingQueue(1) - def firstFile = new File(rootDir, "first.txt") - def secondFile = new File(rootDir, "second.txt") - - startWatcher(singleElementQueue, rootDir) - - when: - createNewFile(firstFile) - waitForChangeEventLatency() - - then: - singleElementQueue.peek().handleEvent(new AbstractFileEventFunctionsTest.TestHandler() { - @Override - void handleChangeEvent(FileWatchEvent.ChangeType type, String absolutePath) { - assert type == CREATED - assert absolutePath == firstFile.absolutePath - } - }) - - when: - createNewFile(secondFile) - waitForChangeEventLatency() - - then: - singleElementQueue.poll().handleEvent(new AbstractFileEventFunctionsTest.TestHandler() { - @Override - void handleOverflow(FileWatchEvent.OverflowType type, @Nullable String absolutePath) { - // Good - } - }) - - then: - singleElementQueue.empty - - expectLogMessage(INFO, "Event queue overflow, dropping all events") - } - - private boolean expectOverflow(BlockingQueue eventQueue = this.eventQueue, int timeoutValue, TimeUnit timeoutUnit) { - boolean overflow = false - expectEvents(eventQueue, timeoutValue, timeoutUnit, { -> true }, { event -> - if (event == null) { - return false - } else { - event.handleEvent(new AbstractFileEventFunctionsTest.TestHandler() { - @Override - void handleOverflow(FileWatchEvent.OverflowType type, @Nullable String absolutePath) { - overflow = true - } - }) - } - return true - }) - return overflow - } -} diff --git a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/FileEventFunctionsStressTest.groovy b/file-events/src/test/groovy/net/rubygrapefruit/platform/file/FileEventFunctionsStressTest.groovy deleted file mode 100644 index d3da807c..00000000 --- a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/FileEventFunctionsStressTest.groovy +++ /dev/null @@ -1,393 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.rubygrapefruit.platform.file - -import net.rubygrapefruit.platform.internal.Platform -import spock.lang.Requires -import spock.lang.Timeout -import spock.lang.Unroll - -import java.util.concurrent.BlockingQueue -import java.util.concurrent.CountDownLatch -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicInteger - -import static java.util.concurrent.TimeUnit.SECONDS -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.PlatformType.LINUX -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.PlatformType.MAC_OS -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.PlatformType.WINDOWS -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.CREATED - -@Requires({ Platform.current().macOs || Platform.current().linux || Platform.current().windows }) -class FileEventFunctionsStressTest extends AbstractFileEventFunctionsTest { - - def "can be started and stopped many times"() { - when: - 100.times { i -> - startWatcher(rootDir) - shutdownWatcher() - } - - then: - noExceptionThrown() - } - - def "can stop and restart watching directory many times"() { - given: - def createdFile = new File(rootDir, "created.txt") - startWatcher(rootDir) - 100.times { - assert stopWatching(rootDir) - startWatching(rootDir) - } - - when: - createNewFile(createdFile) - - then: - expectEvents change(CREATED, createdFile) - } - - def "can stop and restart watching many directories many times"() { - given: - File[] watchedDirs = createDirectoriesToWatch(100) - - startWatcher() - - when: - 100.times { iteration -> - startWatching(watchedDirs) - assert stopWatching(watchedDirs) - } - - then: - noExceptionThrown() - } - - @Unroll - def "can watch #numberOfDirectories directories at the same time with #numberOfChanges changes"() { - given: - def directories = (1..numberOfDirectories).collect { index -> - def dir = new File(rootDir, "dir-${index}") - dir.mkdirs() - return dir - } - startWatcher() - watcher.startWatching(directories) - def expectedEvents = [] - - when: - numberOfChanges.times {index -> - def dir = directories[index % directories.size()] - def file = new File(dir, "file-${index}.txt") - file.createNewFile() - expectedEvents << change(CREATED, file) - } - - then: - expectEvents expectedEvents - - where: - numberOfDirectories = byPlatform( - (MAC_OS): 200, // macOS has a limit around 1000 FSEventStream, so we test with fewer - (LINUX): 10000, - (WINDOWS): 200 // Windows seems to fail weirdly after a few hundred hierarchies - ) - numberOfChanges = 1000 - } - - @Timeout(value = 20, unit = SECONDS) - @Unroll - def "can start and stop watching directory while changes are being made to its contents, round #round"() { - given: - def numberOfParallelWritersPerWatchedDirectory = 10 - def numberOfWatchedDirectories = 10 - - // We don't care about overflows here - ignoreLogMessages() - - expect: - def watchedDirectories = createDirectoriesToWatch(numberOfWatchedDirectories) - - def numberOfThreads = numberOfParallelWritersPerWatchedDirectory * numberOfWatchedDirectories - def inTheMiddleLatch = new CountDownLatch(numberOfThreads) - def changeCount = new AtomicInteger() - def watcher = startNewWatcher() - def onslaught = new OnslaughtExecutor(watchedDirectories.collectMany { watchedDirectory -> - (1..numberOfParallelWritersPerWatchedDirectory).collect { index -> - def fileToChange = new File(watchedDirectory, "file${index}.txt") - fileToChange.createNewFile() - return { -> - 100.times { modifyIndex -> - fileToChange << "Change: $modifyIndex\n" - changeCount.incrementAndGet() - } - inTheMiddleLatch.countDown() - 400.times { modifyIndex -> - fileToChange << "Another change: $modifyIndex\n" - changeCount.incrementAndGet() - } - } as Runnable - } - }) - - onslaught.awaitReady() - watcher.startWatching(watchedDirectories) - onslaught.start() - - inTheMiddleLatch.await() - LOGGER.info("> Shutting down watcher (received ${eventQueue.size()} events of $changeCount changes)") - watcher.shutdown() - LOGGER.info("< Shut down watcher, waiting for termination (received ${eventQueue.size()} events of $changeCount changes)") - assert watcher.awaitTermination(20, SECONDS) - LOGGER.info("< Watcher terminated (received ${eventQueue.size()} events of $changeCount changes)") - - onslaught.terminate(20, SECONDS) - LOGGER.info("< Finished test (received ${eventQueue.size()} events of $changeCount changes)") - - where: - round << (1..20) - } - - @Requires({ Platform.current().linux }) - def "can close watcher for directory hierarchy while it is being deleted"() { - given: - def watchedDirectoryDepth = 10 - - def watchedDir = new File(rootDir, "watchedDir") - assert watchedDir.mkdir() - List watchedDirectories = createHierarchy(watchedDir, watchedDirectoryDepth) - - when: - def watcher = startNewWatcher() - watcher.startWatching(watchedDirectories) - waitForChangeEventLatency() - assert rootDir.deleteDir() - shutdownWatcher(watcher) - - then: - noExceptionThrown() - } - - @Requires({ Platform.current().linux }) - def "can stop watching directory hierarchy while it is being deleted"() { - given: - def watchedDirectoryDepth = 8 - ignoreLogMessages() - - def watchedDir = new File(rootDir, "watchedDir") - assert watchedDir.mkdir() - List watchedDirectories = createHierarchy(watchedDir, watchedDirectoryDepth) - def watcher = startNewWatcher() - def onslaught = new OnslaughtExecutor(watchedDirectories.collect { watchedDirectory -> - return { -> watcher.stopWatching([watchedDirectory]) } as Runnable - }) - - onslaught.awaitReady() - watcher.startWatching(watchedDirectories) - - when: - onslaught.start() - assert rootDir.deleteDir() - onslaught.terminate(5, SECONDS) - - then: - noExceptionThrown() - } - - def "can start and stop watching directories without losing events"() { - given: - def watchedDir = new File(rootDir, "watchedDir") - assert watchedDir.mkdir() - def changedFiles = (1..200).collect { index -> - return new File(watchedDir, "file-${index}.txt") - } - def otherDirs = (1..5).collect { index -> - def dir = new File(rootDir, "dir-$index") - dir.mkdirs() - return dir - } - def watcher = startNewWatcher(watchedDir) - def onslaught = new OnslaughtExecutor( - (otherDirs.collect { otherDir -> - { -> - Thread.sleep((long) (Math.random() * 100 + 100)) - watcher.startWatching(otherDir) - } as Runnable - }) + (changedFiles.collect { changedFile -> - { -> - Thread.sleep((long) (Math.random() * 500)) - LOGGER.fine("> Creating ${changedFile.name}") - changedFile.createNewFile() - } as Runnable - }) - ) - - onslaught.awaitReady() - - when: - onslaught.start() - onslaught.terminate(5, SECONDS) - def expectedEvents = changedFiles.collect { file -> change(CREATED, file) } - - then: - expectEvents(eventQueue, 5, SECONDS, expectedEvents) - - cleanup: - watcher.shutdown() - watcher.awaitTermination(5, SECONDS) - } - - def "can stop watching many directories while they are being deleted"() { - given: - def watchedDirectoryCount = 100 - def files = { File dir -> (1..100).collect { index -> new File(dir, "file-${index}.txt") } } - ignoreLogMessages() - - List watchedDirectories = (1..watchedDirectoryCount).collect { index -> - def dir = new File(rootDir, "dir-$index") - assert dir.mkdirs() - files(dir).each { it.createNewFile() } - return dir - } - - def watcher = startNewWatcher() - - def stopWatchingJobs = watchedDirectories.collect { dir -> - return { - watcher.stopWatching([dir]) - } as Runnable - } - def deleteDirectoriesJobs = watchedDirectories.collect { dir -> - return { - files(dir).each { assert it.delete() } - dir.delete() - } as Runnable - } - def onslaught = new OnslaughtExecutor(stopWatchingJobs + deleteDirectoriesJobs) - - onslaught.awaitReady() - watcher.startWatching(watchedDirectories) - - when: - onslaught.start() - onslaught.terminate(5, SECONDS) - - then: - noExceptionThrown() - } - - @Requires({ !Platform.current().linux }) - def "can stop watching a deep hierarchy when it has been deleted"() { - given: - def watchedDirectoryDepth = 10 - - def watchedDir = new File(rootDir, "watchedDir") - assert watchedDir.mkdir() - createHierarchy(watchedDir, watchedDirectoryDepth) - - when: - def watcher = startNewWatcher() - watcher.startWatching([watchedDir]) - waitForChangeEventLatency() - assert watchedDir.deleteDir() - shutdownWatcher(watcher) - - then: - noExceptionThrown() - } - - @Override - protected FileWatcher startNewWatcher(BlockingQueue eventQueue) { - // Make sure we don't receive overflow events during these tests - return watcherFixture.startNewWatcherWithOverflowPrevention(eventQueue) - } - - private static List createHierarchy(File root, int watchedDirectoryDepth, int branching = 2) { - List allDirs = [] - allDirs.add(root) - List previousRoots = [root] - (watchedDirectoryDepth - 1).times { depth -> - previousRoots = previousRoots.collectMany { previousRoot -> - List dirs = [] - branching.times { index -> - def dir = new File(previousRoot, "dir${index}") - assert dir.mkdir() - dirs << dir - } - return dirs - } - allDirs.addAll(previousRoots) - } - LOGGER.info "> Created ${allDirs.size()} directories" - return allDirs - } - - private List createDirectoriesToWatch(int numberOfWatchedDirectories) { - (1..numberOfWatchedDirectories).collect { - def dir = new File(rootDir, "dir-$it") - assert dir.mkdirs() - return dir - } - } - - private static class OnslaughtExecutor { - private final ExecutorService executorService - private final CountDownLatch readyLatch - private final CountDownLatch startLatch - private final AtomicInteger finishedCounter - private final int numberOfThreads - - OnslaughtExecutor(List jobs) { - this.numberOfThreads = jobs.size() - this.executorService = Executors.newFixedThreadPool(numberOfThreads) - this.readyLatch = new CountDownLatch(numberOfThreads) - this.startLatch = new CountDownLatch(1) - this.finishedCounter = new AtomicInteger() - Collections.shuffle(jobs) - jobs.each { job -> - executorService.submit({ -> - readyLatch.countDown() - startLatch.await() - job.run() - finishedCounter.incrementAndGet() - }) - } - } - - void awaitReady() { - readyLatch.await() - } - - void start() { - awaitReady() - LOGGER.info("> Starting onslaught on $numberOfThreads threads") - startLatch.countDown() - } - - void terminate(long timeout, TimeUnit unit) { - executorService.shutdown() - try { - assert executorService.awaitTermination(timeout, unit) - } finally { - LOGGER.info("> Finished ${finishedCounter} of ${numberOfThreads} jobs") - } - } - } -} diff --git a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/HardLinkFileEventFunctionsTest.groovy b/file-events/src/test/groovy/net/rubygrapefruit/platform/file/HardLinkFileEventFunctionsTest.groovy deleted file mode 100644 index 464d96df..00000000 --- a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/HardLinkFileEventFunctionsTest.groovy +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.rubygrapefruit.platform.file - -import net.rubygrapefruit.platform.internal.Platform -import spock.lang.Requires - -import static java.nio.file.Files.createLink -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.PlatformType.LINUX -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.PlatformType.MAC_OS -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.PlatformType.OTHERWISE -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.PlatformType.WINDOWS -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.CREATED -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.MODIFIED -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.REMOVED - -/** - * None of the operating systems deliver notifications about changing watched content via hard links - * that exist in non-watched locations: - * - *
- *
Linux
- *
"Watching a directory with inotify does not trigger events on hardlinks" - * – link
- * - *
macOS
- *
"Symbolic links work well, the problem is only related with hardlinks. Hardlinks are not possible for folders, - * so FSEvents is a good way to monitor the creation/deletion and move of files inside the given folder. - * In order to monitor file modification, I switched to Kernel Queues, which allows to monitor properly hardlinks." - * – link
- * - *
Windows
- *
"[...] since these changes have no effect on the directory, they are not recognized - * by ReadDirectoryChangesW. The ReadDirectoryChangesW function tells you about changes to - * the directory; if something happens that doesn’t change the directory, then - * ReadDirectoryChangesW will just shrug its shoulders and say, “Hey, not my job.”" - * – link
- *
- */ -@Requires({ Platform.current().macOs || Platform.current().linux || Platform.current().windows }) -class HardLinkFileEventFunctionsTest extends AbstractFileEventFunctionsTest { - - def "can detect hard link created"() { - given: - def target = new File(rootDir, "target") - target.createNewFile() - def link = new File(rootDir, "link") - startWatcher(rootDir) - - when: - createHardLink(link, target) - - then: - // Windows sometimes reports a modification after the creation - // macOS additionally reports a change to the directory containing the original file - expectEvents byPlatform( - (WINDOWS): [change(CREATED, link), optionalChange(MODIFIED, link)], - (MAC_OS): [change(MODIFIED, rootDir), optionalChange(CREATED, link)], - (LINUX): [change(CREATED, link)] - ) - } - - def "does not detect hard link created outside watched hierarchy"() { - given: - def watched = new File(rootDir, "watched") - watched.mkdirs() - def target = new File(watched, "target") - target.createNewFile() - def link = new File(rootDir, "link") - startWatcher(watched) - - when: - createHardLink(link, target) - - then: - // macOS reports a change to the directory containing the original file - expectEvents byPlatform( - (MAC_OS): [change(MODIFIED, watched)], - (OTHERWISE): [] - ) - } - - def "can detect hard link modified"() { - given: - def target = new File(rootDir, "target") - target.createNewFile() - def link = new File(rootDir, "link") - createHardLink(link, target) - startWatcher(rootDir) - - when: - link.text = "modified" - - then: - // On Windows we seem to be getting multiple MODIFIED events - // On Linux we _sometimes_ seem to be getting multiple events - // For macOS 12 we get events for both hardlink locations - expectEvents byPlatform( - (MAC_OS): [change(MODIFIED, link), optionalChange(MODIFIED, target)], - (WINDOWS): [change(MODIFIED, link)] * 2, - (LINUX): [change(MODIFIED, link), optionalChange(MODIFIED, link)] - ) - } - - def "does not detect hard link modified outside watched hierarchy"() { - given: - def watched = new File(rootDir, "watched") - watched.mkdirs() - def target = new File(watched, "target") - target.createNewFile() - def link = new File(rootDir, "link") - createHardLink(link, target) - startWatcher(watched) - - when: - link.text = "modified" - - then: - // For macOS 12 we get an event for the hardlink in the watched directory - if (Platform.current().macOs) { - expectEvents optionalChange(MODIFIED, target) - } else { - expectNoEvents() - } - - - when: - target.text = "modified2" - - then: - // On Windows we seem to be getting multiple MODIFIED events - // On Linux we _sometimes_ seem to be getting multiple events - // For macOS 12 we get events for both the hardlink locations, though the path for link is wrong - expectEvents byPlatform( - (MAC_OS): [change(MODIFIED, target), optionalChange(MODIFIED, new File(watched, "link"))], - (WINDOWS): [change(MODIFIED, target)] * 2, - (LINUX): [change(MODIFIED, target), optionalChange(MODIFIED, target)] - ) - } - - def "can detect hard link removed"() { - given: - def target = new File(rootDir, "target") - target.createNewFile() - def link = new File(rootDir, "link") - createHardLink(link, target) - startWatcher(rootDir) - - when: - link.delete() - - then: - // Windows sometimes reports a modification before the removal - expectEvents byPlatform( - (WINDOWS): [optionalChange(MODIFIED, link), change(REMOVED, link)], - (OTHERWISE): [change(REMOVED, link)] - ) - } - - def "does not detect hard link removed outside watched hierarchy"() { - given: - def watched = new File(rootDir, "watched") - watched.mkdirs() - def target = new File(watched, "target") - target.createNewFile() - def link = new File(rootDir, "link") - createHardLink(link, target) - startWatcher(watched) - - when: - link.delete() - - then: - expectNoEvents() - } - - private void createHardLink(File linked, File target) { - LOGGER.info("> Creating link to ${shorten(target)} in ${shorten(linked)}") - createLink(linked.toPath(), target.toPath()) - LOGGER.info("< Created link to ${shorten(target)} in ${shorten(linked)}") - } -} diff --git a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/MovedDirectoriesFileEventFunctionsTest.groovy b/file-events/src/test/groovy/net/rubygrapefruit/platform/file/MovedDirectoriesFileEventFunctionsTest.groovy deleted file mode 100644 index eb7d1f2e..00000000 --- a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/MovedDirectoriesFileEventFunctionsTest.groovy +++ /dev/null @@ -1,214 +0,0 @@ -/* - * Copyright 2012 Adam Murdoch - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package net.rubygrapefruit.platform.file - -import net.rubygrapefruit.platform.internal.Platform -import spock.lang.Issue -import spock.lang.Requires -import spock.lang.Unroll - -import java.nio.file.AccessDeniedException -import java.nio.file.Files -import java.util.regex.Pattern - -import static java.util.logging.Level.WARNING -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.CREATED -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.INVALIDATED -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.MODIFIED -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.REMOVED - -@Unroll -@Requires({ Platform.current().macOs || Platform.current().linux || Platform.current().windows }) -class MovedDirectoriesFileEventFunctionsTest extends AbstractFileEventFunctionsTest { - - def "can detect #ancestry removed"() { - given: - def parentDir = new File(rootDir, "parent") - def watchedDir = new File(parentDir, "removed") - watchedDir.mkdirs() - def removedFile = new File(watchedDir, "file.txt") - createNewFile(removedFile) - File removedDir = removedDirectory(watchedDir) - startWatcher(watchedDir) - - when: - def directoryRemoved = removedDir.deleteDir() - // On Windows we don't always manage to remove the watched directory, but it's unreliable - if (!Platform.current().windows) { - assert directoryRemoved - } - - def expectedEvents = [] - if (Platform.current().macOs) { - expectedEvents << change(INVALIDATED, watchedDir) - if (ancestry == "watched directory") { - expectedEvents << change(REMOVED, watchedDir) - } - } else if (Platform.current().linux) { - expectedEvents << change(REMOVED, removedFile) << change(REMOVED, watchedDir) - } else if (Platform.current().windows) { - expectedEvents << change(MODIFIED, removedFile) << optionalChange(REMOVED, removedFile) << change(REMOVED, watchedDir) - } - - then: - expectEvents expectedEvents - - where: - ancestry | removedDirectory - "watched directory" | { it } - "parent of watched directory" | { it.parentFile } - "grand-parent of watched directory" | { it.parentFile.parentFile } - } - - @Issue("https://github.com/gradle/native-platform/issues/193") - def "can rename watched directory"() { - given: - def watchedDirectory = new File(rootDir, "watched") - watchedDirectory.mkdirs() - startWatcher(watchedDirectory) - - when: - watchedDirectory.renameTo(new File(rootDir, "newWatched")) - waitForChangeEventLatency() - then: - if (Platform.current().linux) { - expectLogMessage(WARNING, Pattern.compile("Unknown event 0x800 for ${Pattern.quote(watchedDirectory.absolutePath)}")) - } - noExceptionThrown() - } - - @Requires({ Platform.current().windows }) - def "stops watching when path is moved"() { - given: - def watchedDir = new File(rootDir, "watched") - assert watchedDir.mkdirs() - def createdFile = new File(watchedDir, "created.txt") - startWatcher(watchedDir) - - def renamedDir = new File(rootDir, "renamed") - Files.move(watchedDir.toPath(), renamedDir.toPath()) - - when: - def droppedPaths = watcher.stopWatchingMovedPaths() - then: - droppedPaths == [watchedDir] - - when: - assert watchedDir.mkdir() - assert createdFile.createNewFile() - then: - expectNoEvents() - - when: - droppedPaths = watcher.stopWatchingMovedPaths() - then: - droppedPaths == [] - } - - @Requires({ Platform.current().linux }) - def "stops watching when parent of watched directory is moved on Linux"() { - given: - def parentDir = new File(rootDir, "parent") - def watchedDir = new File(parentDir, "watched") - assert watchedDir.mkdirs() - startWatcher(watchedDir) - - def renamedDir = new File(rootDir, "renamed") - Files.move(parentDir.toPath(), renamedDir.toPath()) - - when: - def droppedPaths = watcher.stopWatchingMovedPaths([watchedDir]) - then: - droppedPaths == [watchedDir] - - when: - def createdFile = new File(watchedDir, "created.txt") - assert watchedDir.mkdirs() - assert createdFile.createNewFile() - then: - expectNoEvents() - - when: - droppedPaths = watcher.stopWatchingMovedPaths([watchedDir]) - then: - droppedPaths == [watchedDir] - } - - @Requires({ Platform.current().linux }) - def "reports non-watched directory as moved on Linux"() { - given: - def parentDir = new File(rootDir, "parent") - def watchedDir = new File(parentDir, "watched") - def unwatchedDir = new File(watchedDir, "unwatched-directory") - assert watchedDir.mkdirs() - assert unwatchedDir.mkdirs() - startWatcher(watchedDir) - - when: - def droppedPaths = watcher.stopWatchingMovedPaths([unwatchedDir]) - then: - droppedPaths == [unwatchedDir] - - when: - def createdFile = new File(watchedDir, "created.txt") - assert createdFile.createNewFile() - then: - expectEvents change(CREATED, createdFile) - } - - @Requires({ Platform.current().macOs }) - def "keeps watching when parent of watched directory is moved on macOS"() { - given: - def parentDir = new File(rootDir, "parent") - def watchedDir = new File(parentDir, "watched") - assert watchedDir.mkdirs() - startWatcher(watchedDir) - - def renamedDir = new File(rootDir, "renamed") - - when: - Files.move(parentDir.toPath(), renamedDir.toPath()) - then: - expectEvents change(INVALIDATED, watchedDir) - - when: - assert watchedDir.mkdirs() - then: - expectEvents change(INVALIDATED, watchedDir), change(CREATED, watchedDir) - - when: - def createdFile = new File(watchedDir, "created.txt") - assert createdFile.createNewFile() - then: - expectEvents change(CREATED, createdFile) - } - - @Requires({ Platform.current().windows }) - def "cannot move parent of watched directory on Windows"() { - given: - def parentDir = new File(rootDir, "parent") - def watchedDir = new File(parentDir, "watched") - assert watchedDir.mkdirs() - startWatcher(watchedDir) - - def renamedDir = new File(rootDir, "renamed") - - when: - Files.move(parentDir.toPath(), renamedDir.toPath()) - then: - thrown(AccessDeniedException) - } -} diff --git a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/SymlinkFileEventFunctionsTest.groovy b/file-events/src/test/groovy/net/rubygrapefruit/platform/file/SymlinkFileEventFunctionsTest.groovy deleted file mode 100644 index 9881c188..00000000 --- a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/SymlinkFileEventFunctionsTest.groovy +++ /dev/null @@ -1,203 +0,0 @@ -/* - * Copyright 2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.rubygrapefruit.platform.file - -import net.rubygrapefruit.platform.NativeException -import net.rubygrapefruit.platform.internal.Platform -import spock.lang.Ignore -import spock.lang.Requires -import spock.lang.Unroll - -import static java.nio.file.Files.createSymbolicLink -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.PlatformType.MAC_OS -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.PlatformType.OTHERWISE -import static net.rubygrapefruit.platform.file.AbstractFileEventFunctionsTest.PlatformType.WINDOWS -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.CREATED -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.MODIFIED -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.REMOVED - -@Unroll -@Requires({ Platform.current().macOs || Platform.current().linux || Platform.current().windows }) -class SymlinkFileEventFunctionsTest extends AbstractFileEventFunctionsTest { - - def "can detect symlink to #description created"() { - given: - def target = new File(rootDir, "target") - initialize(target) - def link = new File(rootDir, "link") - startWatcher(rootDir) - - when: - createSymlink(link, target) - - then: - // Windows sometimes reports a modification after the creation - expectEvents byPlatform( - (WINDOWS): [change(CREATED, link), optionalChange(MODIFIED, link)], - (OTHERWISE): [change(CREATED, link)] - ) - - where: - description | initialize - "regular file" | { File file -> assert file.createNewFile() } - "directory" | { File file -> assert file.mkdirs() } - "missing file" | { File file -> } - } - - def "can detect symlink to #description removed"() { - given: - def target = new File(rootDir, "target") - initialize(target) - def link = new File(rootDir, "link") - createSymlink(link, target) - startWatcher(rootDir) - - when: - link.delete() - - then: - // Windows sometimes reports a modification before the removal - expectEvents byPlatform( - (WINDOWS): [optionalChange(MODIFIED, link), change(REMOVED, link)], - (OTHERWISE): [change(REMOVED, link)] - ) - - where: - description | initialize - "regular file" | { File file -> assert file.createNewFile() } - "directory" | { File file -> assert file.mkdirs() } - "missing file" | { File file -> } - } - - def "does not detect symlink target #description created"() { - given: - def contentRoot = new File(rootDir, "contentDir") - contentRoot.mkdirs() - def watchedRoot = new File(rootDir, "watchedDir") - watchedRoot.mkdirs() - def target = new File(contentRoot, "target") - def link = new File(watchedRoot, "link") - createSymlink(link, target) - startWatcher(watchedRoot) - - when: - initialize(target) - - then: - expectNoEvents() - - where: - description | initialize - "regular file" | { File file -> assert file.createNewFile() } - "directory" | { File file -> assert file.mkdirs() } - } - - def "can detect changes in symlinked watched directory"() { - given: - def canonicalFile = new File(rootDir, "modified.txt") - createNewFile(canonicalFile) - def watchedLink = new File(testDir, "linked") - def watchedFile = new File(watchedLink, "modified.txt") - createSymlink(watchedLink, rootDir) - startWatcher(watchedLink) - - when: - watchedFile << "change" - - then: - expectEvents byPlatform( - (MAC_OS): [change(MODIFIED, canonicalFile)], - (OTHERWISE): [change(MODIFIED, watchedFile)] - ) - } - - def "can detect changes if parent of watched directory is a symlink"() { - given: - def canonicalFile = new File(rootDir, "watchedRoot/modified.txt") - canonicalFile.parentFile.mkdirs() - createNewFile(canonicalFile) - def linkedParent = new File(testDir, "parent") - def watchedDir = new File(linkedParent, "watchedRoot") - def watchedFile = new File(watchedDir, "modified.txt") - createSymlink(linkedParent, rootDir) - startWatcher(watchedDir) - - when: - watchedFile << "change" - - then: - expectEvents byPlatform( - (MAC_OS): [change(MODIFIED, canonicalFile)], - (OTHERWISE): [change(MODIFIED, watchedFile)] - ) - } - - @Requires({ Platform.current().macOs || Platform.current().windows }) - def "can watch directory via symlink and directly at the same time"() { - given: - def canonicalDir = new File(rootDir, "watchedDir") - def canonicalFile = new File(canonicalDir, "modified.txt") - canonicalDir.mkdirs() - createNewFile(canonicalFile) - def linkedDir = new File(rootDir, "linked") - def linkedFile = new File(linkedDir, "modified.txt") - createSymlink(linkedDir, canonicalDir) - startWatcher(canonicalDir, linkedDir) - - when: - linkedFile << "change" - - then: - expectEvents byPlatform( - (MAC_OS): [change(MODIFIED, canonicalFile), change(MODIFIED, canonicalFile)], - (OTHERWISE): [change(MODIFIED, linkedFile), change(MODIFIED, canonicalFile)] - ) - } - - @Requires({ Platform.current().linux }) - @Ignore("The behavior doesn't seem consistent across Linux variants") - // Sometimes we get the same watch descriptor back when registering the watch with a different path, - // other times not, but freeing the resulting watchers leads to errors - def "fails when watching same directory both directly and via symlink"() { - given: - def canonicalDir = new File(rootDir, "watchedDir") - canonicalDir.mkdirs() - def linkedDir = new File(rootDir, "linked") - createSymlink(linkedDir, canonicalDir) - - when: - startWatcher(canonicalDir, linkedDir) - - then: - def ex = thrown NativeException - ex.message == "Already watching path: ${linkedDir.absolutePath}" - - when: - startWatcher(linkedDir, canonicalDir) - - then: - ex = thrown NativeException - ex.message == "Already watching path: ${canonicalDir.absolutePath}" - } - - private void createSymlink(File linked, File target) { - LOGGER.info("> Creating link to ${shorten(target)} in ${shorten(linked)}") - createSymbolicLink(linked.toPath(), target.toPath()) - LOGGER.info("< Created link to ${shorten(target)} in ${shorten(linked)}") - } - -} diff --git a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/SyntheticFileEventFunctionsTest.groovy b/file-events/src/test/groovy/net/rubygrapefruit/platform/file/SyntheticFileEventFunctionsTest.groovy deleted file mode 100644 index 016f4b42..00000000 --- a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/SyntheticFileEventFunctionsTest.groovy +++ /dev/null @@ -1,31 +0,0 @@ -package net.rubygrapefruit.platform.file - -import net.rubygrapefruit.platform.internal.jni.TestFileEventFunctions -import spock.lang.Specification - -import java.util.concurrent.LinkedBlockingDeque -import java.util.concurrent.TimeUnit - -class SyntheticFileEventFunctionsTest extends Specification { - def service = new TestFileEventFunctions() - def eventQueue = new LinkedBlockingDeque() - def watcher = service - .newWatcher(eventQueue) - .start() - - def "normal termination produces termination event"() { - when: - watcher.shutdown() - watcher.awaitTermination(1, TimeUnit.SECONDS) - then: - eventQueue*.toString() == ["TERMINATE"] - } - - def "failure in run loop produces failure event followed by termination events"() { - when: - watcher.injectFailureIntoRunLoop() - watcher.awaitTermination(1, TimeUnit.SECONDS) - then: - eventQueue*.toString() == ["FAILURE Error", "TERMINATE"] - } -} diff --git a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/WindowsFileEventFunctionsTest.groovy b/file-events/src/test/groovy/net/rubygrapefruit/platform/file/WindowsFileEventFunctionsTest.groovy deleted file mode 100644 index d4a1d85a..00000000 --- a/file-events/src/test/groovy/net/rubygrapefruit/platform/file/WindowsFileEventFunctionsTest.groovy +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2012 Adam Murdoch - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package net.rubygrapefruit.platform.file - -import net.rubygrapefruit.platform.internal.Platform -import spock.lang.Requires -import spock.lang.Unroll - -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.CREATED -import static net.rubygrapefruit.platform.file.FileWatchEvent.ChangeType.REMOVED - -@Unroll -@Requires({ Platform.current().windows }) -class WindowsFileEventFunctionsTest extends AbstractFileEventFunctionsTest { - - def "stops watching and reports watched directory as removed after it has been moved and an event is received"() { - given: - def watchedDir = new File(rootDir, "watched") - assert watchedDir.mkdirs() - def renamedDir = new File(rootDir, "renamed") - def createdFile = new File(renamedDir, "created.txt") - startWatcher(watchedDir) - - when: - watchedDir.renameTo(renamedDir) - then: - expectNoEvents() - - when: - createdFile.createNewFile() - then: - expectEvents change(REMOVED, watchedDir) - - when: - assert createdFile.delete() - then: - expectNoEvents() - } - - def "reports changes on subst drive"() { - given: - subst("G:", rootDir) - def watchedDir = new File("G:\\watched") - def createdFile = new File(watchedDir, "created.txt") - assert watchedDir.mkdirs() - startWatcher(watchedDir) - - when: - createNewFile(createdFile) - then: - expectEvents change(CREATED, createdFile) - - cleanup: - unsubst("G:") - } - - def "does not drop subst drive as moved"() { - given: - subst("G:", rootDir) - def watchedDir = new File("G:\\watched") - assert watchedDir.mkdirs() - startWatcher(watchedDir) - - when: - def droppedPaths = watcher.stopWatchingMovedPaths() - then: - droppedPaths == [] - - cleanup: - unsubst("G:") - } - - def subst(String substDrive, File substPath) { - ["CMD", "/C", "SUBST", substDrive, substPath.absolutePath].execute() - .waitForProcessOutput(System.out, System.err) - } - - def unsubst(String substDrive) { - ["CMD", "/C", "SUBST", "/D", substDrive].execute() - .waitForProcessOutput(System.out, System.err) - } -} diff --git a/file-events/src/test/groovy/net/rubygrapefruit/platform/internal/jni/TestFileEventFunctions.groovy b/file-events/src/test/groovy/net/rubygrapefruit/platform/internal/jni/TestFileEventFunctions.groovy deleted file mode 100644 index 8e0922d7..00000000 --- a/file-events/src/test/groovy/net/rubygrapefruit/platform/internal/jni/TestFileEventFunctions.groovy +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright 2012 Adam Murdoch - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package net.rubygrapefruit.platform.internal.jni - -import net.rubygrapefruit.platform.file.FileWatchEvent - -import java.util.concurrent.BlockingQueue -import java.util.concurrent.LinkedBlockingQueue - -class TestFileEventFunctions extends AbstractFileEventFunctions { - - @Override - WatcherBuilder newWatcher(BlockingQueue eventQueue) { - new WatcherBuilder(eventQueue) - } - - static class TestFileWatcher extends AbstractFileEventFunctions.AbstractFileWatcher { - enum Command { - FAIL, TERMINATE - } - - private final BlockingQueue commands = new LinkedBlockingQueue() - - TestFileWatcher(AbstractFileEventFunctions.NativeFileWatcherCallback callback) { - super(callback) - } - - @Override - protected void initializeRunLoop() { - } - - @Override - protected void executeRunLoop() { - while (true) { - switch (commands.take()) { - case Command.FAIL: - throw new RuntimeException("Error") - case Command.TERMINATE: - return - } - } - } - - void injectFailureIntoRunLoop() { - commands.put(Command.FAIL) - } - - @Override - protected void doShutdown() { - commands.put(Command.TERMINATE) - } - - @Override - protected boolean awaitTermination(long timeoutInMillis) { - return true; - } - - @Override - protected void doStartWatching(Collection paths) { - throw new UnsupportedOperationException() - } - - @Override - protected boolean doStopWatching(Collection paths) { - throw new UnsupportedOperationException() - } - } - - static class WatcherBuilder extends AbstractFileEventFunctions.AbstractWatcherBuilder { - WatcherBuilder(BlockingQueue eventQueue) { - super(eventQueue) - } - - @Override - protected TestFileWatcher createWatcher(AbstractFileEventFunctions.NativeFileWatcherCallback callback) { - new TestFileWatcher(callback) - } - } -} diff --git a/file-events/src/test/groovy/net/rubygrapefruit/platform/testfixture/JulLogging.java b/file-events/src/test/groovy/net/rubygrapefruit/platform/testfixture/JulLogging.java deleted file mode 100644 index ef749ef7..00000000 --- a/file-events/src/test/groovy/net/rubygrapefruit/platform/testfixture/JulLogging.java +++ /dev/null @@ -1,80 +0,0 @@ -package net.rubygrapefruit.platform.testfixture; - -import org.junit.rules.TestWatcher; -import org.junit.runner.Description; - -import java.io.IOException; -import java.io.InputStream; -import java.io.UncheckedIOException; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.logging.Handler; -import java.util.logging.Level; -import java.util.logging.LogManager; -import java.util.logging.LogRecord; -import java.util.logging.Logger; - -public class JulLogging extends TestWatcher { - - private final Logger logger; - private final Level level; - private RecordingHandler recorder; - private Level oldLevel; - - public JulLogging(Class clazz, Level level) { - this(Logger.getLogger(clazz.getName()), level); - } - - private JulLogging(Logger logger, Level level) { - try { - InputStream input = getClass().getClassLoader().getResource("logging.properties").openStream(); - try { - LogManager.getLogManager().readConfiguration(input); - } finally { - input.close(); - } - } catch (IOException ex) { - throw new UncheckedIOException(ex); - } - this.logger = logger; - this.level = level; - } - - @Override - protected void starting(Description description) { - recorder = new RecordingHandler(); - logger.addHandler(recorder); - - oldLevel = logger.getLevel(); - logger.setLevel(level); - } - - @Override - protected void finished(Description description) { - logger.removeHandler(recorder); - logger.setLevel(oldLevel); - } - - public Map getMessages() { - return recorder.messages; - } - - public void clear() { - recorder.messages.clear(); - } - - private static class RecordingHandler extends Handler { - private final Map messages = new LinkedHashMap(); - - @Override - public void publish(LogRecord record) { - this.messages.put(record.getMessage(), record.getLevel()); - } - - @Override - public void flush() {} - - @Override - public void close() {} - } -} diff --git a/file-events/src/test/resources/logging.properties b/file-events/src/test/resources/logging.properties deleted file mode 100644 index 970fb529..00000000 --- a/file-events/src/test/resources/logging.properties +++ /dev/null @@ -1,5 +0,0 @@ -handlers=java.util.logging.ConsoleHandler -java.util.logging.ConsoleHandler.level=FINEST -java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter -java.util.logging.SimpleFormatter.format=%1$tH:%1$tM:%1$tS.%1$tL %4$-7s %5$s%n -.level=FINEST diff --git a/settings.gradle.kts b/settings.gradle.kts index fcd61a27..56d4a3e4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,6 +7,5 @@ rootProject.name = "native-platform" include("test-app") include("native-platform") -include("file-events") enableFeaturePreview("GROOVY_COMPILATION_AVOIDANCE") diff --git a/test-app/build.gradle b/test-app/build.gradle index 9b997af5..04b3ea57 100644 --- a/test-app/build.gradle +++ b/test-app/build.gradle @@ -11,7 +11,6 @@ if (versions.useRepo) { configurations.all { resolutionStrategy.dependencySubstitution { substitute project(':native-platform') with module("net.rubygrapefruit:native-platform:${version}") - substitute project(':file-events') with module("net.rubygrapefruit:file-events:${version}") } } configurations { @@ -24,7 +23,6 @@ if (versions.useRepo) { dependencies { implementation project(':native-platform') - implementation project(':file-events') implementation 'net.sf.jopt-simple:jopt-simple:4.2' } diff --git a/test-app/src/main/java/net/rubygrapefruit/platform/test/Main.java b/test-app/src/main/java/net/rubygrapefruit/platform/test/Main.java index 5fc38bac..91486005 100755 --- a/test-app/src/main/java/net/rubygrapefruit/platform/test/Main.java +++ b/test-app/src/main/java/net/rubygrapefruit/platform/test/Main.java @@ -24,19 +24,12 @@ import net.rubygrapefruit.platform.Process; import net.rubygrapefruit.platform.SystemInfo; import net.rubygrapefruit.platform.file.DirEntry; -import net.rubygrapefruit.platform.file.FileEvents; import net.rubygrapefruit.platform.file.FileInfo; import net.rubygrapefruit.platform.file.FileSystemInfo; import net.rubygrapefruit.platform.file.FileSystems; -import net.rubygrapefruit.platform.file.FileWatchEvent; -import net.rubygrapefruit.platform.file.FileWatcher; import net.rubygrapefruit.platform.file.Files; import net.rubygrapefruit.platform.file.PosixFileInfo; import net.rubygrapefruit.platform.file.PosixFiles; -import net.rubygrapefruit.platform.internal.Platform; -import net.rubygrapefruit.platform.internal.jni.LinuxFileEventFunctions; -import net.rubygrapefruit.platform.internal.jni.OsxFileEventFunctions; -import net.rubygrapefruit.platform.internal.jni.WindowsFileEventFunctions; import net.rubygrapefruit.platform.memory.Memory; import net.rubygrapefruit.platform.memory.MemoryInfo; import net.rubygrapefruit.platform.memory.WindowsMemory; @@ -52,13 +45,8 @@ import java.io.PrintStream; import java.text.SimpleDateFormat; import java.util.Arrays; -import java.util.Collections; import java.util.Date; import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; public class Main { public static void main(String[] args) throws Exception { @@ -69,7 +57,6 @@ public static void main(String[] args) throws Exception { optionParser.accepts("stat-L", "Display details about the specified file or directory, following symbolic links").withRequiredArg(); optionParser.accepts("ls", "Display contents of the specified directory").withRequiredArg(); optionParser.accepts("ls-L", "Display contents of the specified directory, following symbolic links").withRequiredArg(); - optionParser.accepts("watch", "Watches for changes to the specified file or directory").withRequiredArg(); optionParser.accepts("machine", "Display details about the current machine"); optionParser.accepts("terminal", "Display details about the terminal"); optionParser.accepts("input", "Reads input from the terminal"); @@ -111,11 +98,6 @@ public static void main(String[] args) throws Exception { return; } - if (result.has("watch")) { - watch((String) result.valueOf("watch")); - return; - } - if (result.has("machine")) { machine(); return; @@ -386,90 +368,6 @@ private static void fileSystems() { System.out.println(); } - private static void watch(String path) throws InterruptedException { - final BlockingQueue eventQueue = new ArrayBlockingQueue(16); - Thread processorThread = new Thread(new Runnable() { - @Override - public void run() { - final AtomicBoolean terminated = new AtomicBoolean(false); - while (!terminated.get()) { - FileWatchEvent event; - try { - event = eventQueue.take(); - } catch (InterruptedException e) { - break; - } - event.handleEvent(new FileWatchEvent.Handler() { - @Override - public void handleChangeEvent(FileWatchEvent.ChangeType type, String absolutePath) { - System.out.printf("Change detected: %s / '%s'%n", type, absolutePath); - } - - @Override - public void handleUnknownEvent(String absolutePath) { - System.out.printf("Unknown event happened at %s%n", absolutePath); - } - - @Override - public void handleOverflow(FileWatchEvent.OverflowType type, String absolutePath) { - System.out.printf("Overflow happened (path = %s, type = %s)%n", absolutePath, type); - } - - @Override - public void handleFailure(Throwable failure) { - failure.printStackTrace(); - } - - @Override - public void handleTerminated() { - System.out.printf("Terminated%n"); - terminated.set(true); - } - }); - } - } - }, "File watcher event handler"); - processorThread.start(); - FileWatcher watcher = createWatcher(path, eventQueue); - try { - System.out.println("Waiting - type ctrl-d to exit ..."); - while (true) { - int ch = System.in.read(); - if (ch < 0) { - break; - } - } - } catch (Exception e) { - throw new RuntimeException(e); - } finally { - watcher.shutdown(); - if (!watcher.awaitTermination(5, TimeUnit.SECONDS)) { - System.out.println("Shutting down watcher timed out"); - } - } - } - - private static FileWatcher createWatcher(String path, BlockingQueue eventQueue) throws InterruptedException { - FileWatcher watcher; - if (Platform.current().isMacOs()) { - watcher = FileEvents.get(OsxFileEventFunctions.class) - .newWatcher(eventQueue) - .start(); - } else if (Platform.current().isLinux()) { - watcher = FileEvents.get(LinuxFileEventFunctions.class) - .newWatcher(eventQueue) - .start(); - } else if (Platform.current().isWindows()) { - watcher = FileEvents.get(WindowsFileEventFunctions.class) - .newWatcher(eventQueue) - .start(); - } else { - throw new RuntimeException("Only Windows and macOS are supported for file watching"); - } - watcher.startWatching(Collections.singleton(new File(path))); - return watcher; - } - private static void ls(String path) { ls(path, false); }