How to use the DirectoryWatcher module in an Unreal Engine packaged build

I’m using the IDirectoryWatcher module on my project to monitor changes on a directory and when packaging it, I ran into a number of issues.

Attempt 1:

If I simply add the #include "DirectoryWatcherModule.h" and #include "IDirectoryWatcher.h" to my .cpp files, the game in the editor works fine but I get the following error:

C:\Users[…]\Unreal Projects\MDV\Project4\MDVProject4\Source\MDVProject4\Controller\AMyController.h(6): fatal error C1083: Cannot open include file: ‘IDirectoryWatcher.h’: No such file or directory

Attempt 2:

If I add the module to the .Build.cs file as follows:

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "DirectoryWatcher" });

the game works fine in the editor but shows the following error when packaging:

Missing precompiled manifest for ‘DirectoryWatcher’, ‘C:\Program Files\Epic Games\UE_5.3\Engine\Intermediate\Build\Win64\UnrealGame\Development\DirectoryWatcher\DirectoryWatcher.precompiled’. This module was most likely not flagged for being included in a precompiled build - set ‘PrecompileForTargets = PrecompileTargetsType.Any;’ in DirectoryWatcher.build.cs to override. If part of a plugin, also check if its ‘Type’ is correct.

Note: I tried adding the module with DynamicallyLoadedModuleNames.Add("DirectoryWatcher");, but got the same error.

Attempt 3:

Now things get funny. I can’t add the module and without it, it does not seem to find it. So I took the DirectoryWatcher folder from the engine and copied it into the Plugins folder of my project. The .Build.cs module added in attempt 2 was removed.

I changed the includes to
#include "../../MDVProject4/Plugins/DirectoryWatcher/Public/IDirectoryWatcher.h" and
#include "../../MDVProject4/Plugins/DirectoryWatcher/Public/DirectoryWatcherModule.h"
to make sure the code used is that of my Plugin folder and not the one from the engine.

With this the game works fine in the editor and can package, but I get a black screen with the following error when opening the .exe file:

Assertion failed: Module [File:D:\build++UE5\Sync\Engine\Source\Runtime\Core\Private\Modules\ModuleManager.cpp] [Line: 397] DirectoryWatcher

Looking at the logs, I can see that it points to a specific line on my code that contains:

FDirectoryWatcherModule &DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher"));
IDirectoryWatcher* DirectoryWatcher = DirectoryWatcherModule.Get();

The reason for this is because despite the IDirectoryWatcher and FDirectoryWatcherModule class being used is the one from the Plugin folder, for some reason the Get() method being used is the one from the engine.

I then tried some nasty things that I will not go into detail but the issue persists.


Unreal Engine version 5.3. Packaging mode used Developer.

I’m aware the IDirectoryWatcher module is inside the Developer folder (/Engine/Source/Developer), but I would expect a Developer packaging mode to work with it.

How can I fix this?

1 Like

Same here, have you figured out how to resolve this issue?

It does not say it is editor only feature in the source code or any other places.

If anyone know how to solve this, please help!

1 Like

I actually managed to resolve this issue by making a custom file watching blueprint function library class using windows API.

Here’s code below.
.h file

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Async/Async.h"
#include "MyCustomFileWatchingFUnctionLibrary.generated.h"

DECLARE_DYNAMIC_DELEGATE(FOnFileChangedDelegate);

UCLASS()
class MY_API UMyCustomFileWatchingFunctionLibrary : UBlueprintFunctionLibrary
{
	GENERATED_BODY()

public:
	UFUNCTION(BlueprintCallable, Category = "File Watching")
	static void StartWatching(const FString& DirectoryToWatch, const FOnFileChangedDelegate& OnChangeDelegate);

	UFUNCTION(BlueprintCallable, Category = "File Watching")
	static void StopWatching();
    
private:
	static struct FWindowsFileChangeWatcher* FileChangeWatcher;
};

.cpp file

#include "../Public/MyCustomFileWatchingFunctionLibrary.h"
#include <mutex>
#include "Windows/AllowWindowsPlatformTypes.h"
#include <windows.h>
#include "Windows/HideWindowsPlatformTypes.h"
#include "HAL/Runnable.h"
#include "HAL/RunnableThread.h"

struct FWindowsFileChangeWatcher final : FRunnable
{
    HANDLE DirectoryHandle;
    HANDLE ChangeHandle;
    FString DirectoryToWatch;
    bool bShouldWatch;
    FRunnableThread* WatchThread;
    FOnFileChangedDelegate OnChangeDelegate;

    FWindowsFileChangeWatcher()
        : DirectoryHandle(INVALID_HANDLE_VALUE), ChangeHandle(INVALID_HANDLE_VALUE), bShouldWatch(false), WatchThread(nullptr)
    {
    }

    virtual uint32 Run() override
    {
        while (bShouldWatch)
        {
            switch (WaitForSingleObject(ChangeHandle, INFINITE))
            {
            case WAIT_OBJECT_0:
                UE_LOG(LogTemp, Log, TEXT("Directory change detected."));
                if (OnChangeDelegate.IsBound())
                {
                    AsyncTask(ENamedThreads::GameThread, [this]()
                    {
                        OnChangeDelegate.ExecuteIfBound();
                    });
                }
                if (FindNextChangeNotification(ChangeHandle) == Windows::FALSE)
                {
                    UE_LOG(LogTemp, Error, TEXT("FindNextChangeNotification failed."));
                    bShouldWatch = false;
                }
                break;

            case WAIT_FAILED:
                UE_LOG(LogTemp, Error, TEXT("WaitForSingleObject failed."));
                bShouldWatch = false;
                break;
            }
        }
        return 0;
    }

    void StartWatching()
    {
        bShouldWatch = true;
        WatchThread = FRunnableThread::Create(this, TEXT("FileChangeWatcherThread"));
    }

    void StopWatching()
    {
        bShouldWatch = false;

        if (WatchThread)
        {
            WatchThread->Kill(true);
            delete WatchThread;
            WatchThread = nullptr;
        }

        if (ChangeHandle != INVALID_HANDLE_VALUE)
        {
            FindCloseChangeNotification(ChangeHandle);
            ChangeHandle = INVALID_HANDLE_VALUE;
        }

        if (DirectoryHandle != INVALID_HANDLE_VALUE)
        {
            CloseHandle(DirectoryHandle);
            DirectoryHandle = INVALID_HANDLE_VALUE;
        }
    }
};

FWindowsFileChangeWatcher* UMyCustomFileWatchingFunctionLibrary::FileChangeWatcher = nullptr;

void UMyCustomFileWatchingFunctionLibrary::StartWatching(const FString& DirectoryToWatch, const FOnFileChangedDelegate& OnChangeDelegate)
{
    if (FileChangeWatcher && FileChangeWatcher->bShouldWatch)
    {
        UE_LOG(LogTemp, Warning, TEXT("Already watching a directory."));
        return;
    }

    if (!FileChangeWatcher)
    {
        FileChangeWatcher = new FWindowsFileChangeWatcher();
    }

    FileChangeWatcher->DirectoryToWatch = DirectoryToWatch;
    FileChangeWatcher->OnChangeDelegate = OnChangeDelegate;

    FileChangeWatcher->DirectoryHandle = CreateFile(
        *FileChangeWatcher->DirectoryToWatch,
        FILE_LIST_DIRECTORY,
        FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
        nullptr,
        OPEN_EXISTING,
        FILE_FLAG_BACKUP_SEMANTICS,
    nullptr);

    if (FileChangeWatcher->DirectoryHandle == INVALID_HANDLE_VALUE)
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to get directory handle."));
        return;
    }

    FileChangeWatcher->ChangeHandle = FindFirstChangeNotification(
        *FileChangeWatcher->DirectoryToWatch,
        Windows::FALSE,
        FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME | FILE_NOTIFY_CHANGE_ATTRIBUTES | FILE_NOTIFY_CHANGE_SIZE | FILE_NOTIFY_CHANGE_LAST_WRITE);

    if (FileChangeWatcher->ChangeHandle == INVALID_HANDLE_VALUE)
    {
        UE_LOG(LogTemp, Error, TEXT("Failed to create change notification."));
        CloseHandle(FileChangeWatcher->DirectoryHandle);
        return;
    }

    FileChangeWatcher->StartWatching();
}

void UMyCustomFileWatchingFunctionLibrary::StopWatching()
{
    if (!FileChangeWatcher || !FileChangeWatcher->bShouldWatch)
    {
        return;
    }

    FileChangeWatcher->StopWatching();

    delete FileChangeWatcher;
    FileChangeWatcher = nullptr;
}```
1 Like