Search code examples
c#multithreadingunity-game-enginemonofilesystemwatcher

Thread safe File System Watcher for Unity Editor


I need a thread safe class for File System Watcher to use in Unity Editor, I already know that Threading is not possible out of coroutines, but I didn't know that threading wasn't allowed also in Editor.

So, there is my error:

get_isEditor can only be called from the main thread. Constructors and field initializers will be executed from the loading thread when loading a scene. Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function. 0x0000000140E431ED (Unity) StackWalker::GetCurrentCallstack 0x0000000140E44EE1 (Unity) StackWalker::ShowCallstack 0x00000001405FC603 (Unity) GetStacktrace 0x00000001405F97FE (Unity) DebugStringToFile 0x00000001405F9C5C (Unity) DebugStringToFile 0x000000014035F7B3 (Unity) ThreadAndSerializationSafeCheckReportError 0x0000000140E7B988 (Unity) Application_Get_Custom_PropIsEditor 0x0000000015AC46AA (Mono JIT Code) (wrapper managed-to-native) UnityEngine.Application:get_isEditor () 0x0000000015AC42FE (Mono JIT Code) [Helpers.cs:585] Lerp2API.DebugHandler.Debug:Log (object) 0x0000000015AC41C2 (Mono JIT Code) [Helpers.cs:578] Lerp2API.DebugHandler.Debug:Log (string) 0x0000000015AC40F7 (Mono JIT Code) [LerpedEditorCore.cs:101] Lerp2APIEditor.LerpedEditorCore:Recompile (object,System.IO.FileSystemEventArgs) 0x0000000015AC3F2D (Mono JIT Code) (wrapper runtime-invoke) :runtime_invoke_void__this___object_object (object,intptr,intptr,intptr) 0x00007FFB400A519B (mono) [mini.c:4937] mono_jit_runtime_invoke 0x00007FFB3FFF84FD (mono) [object.c:2623] mono_runtime_invoke 0x00007FFB3FFFE8F7 (mono) [object.c:3827] mono_runtime_invoke_array 0x00007FFB3FFFEBCC (mono) [object.c:5457] mono_message_invoke 0x00007FFB4001EB8B (mono) [threadpool.c:1019] mono_async_invoke 0x00007FFB4001F5E2 (mono) [threadpool.c:1455] async_invoke_thread 0x00007FFB4002329F (mono) [threads.c:685] start_wrapper 0x00007FFB400D78C9 (mono) [win32_threads.c:599] thread_start 0x00007FFB77FC8364 (KERNEL32) BaseThreadInitThunk

I copied full stack trace to make aware any helper where can be the problem. Because, I searched for a solution, like any threaded safe FWS, and yes, there is one, but only for .NET 4, and I need one for .NET 2

This is my code:

using System.IO; //class, namespace, redundant info...

private static FileSystemWatcher m_Watcher;

[InitializeOnLoadMethod]
static void HookWatcher() 
{
    m_Watcher = new FileSystemWatcher("path", "*.cs");
    m_Watcher.NotifyFilter = NotifyFilters.LastWrite;
    m_Watcher.IncludeSubdirectories = true;
    //m_Watcher.Created += new FileSystemEventHandler(); //Add to the solution before compile
    //m_Watcher.Renamed += new FileSystemEventHandler(); //Rename to the solution before compile
    //m_Watcher.Deleted += new FileSystemEventHandler(); //Remove to the solution before compile
    m_Watcher.Changed += Recompile;
    m_Watcher.EnableRaisingEvents = true;
}

private static void Recompile(object sender, FileSystemEventArgs e) 
{
    Debug.Log("Origin files has been changed!");
}

There nothing special as you can see...

The FSW I saw was this: https://gist.githubusercontent.com/bradsjm/2c839912294d0e2c008a/raw/c4a5c3d920ab46fdaa53b0e111e0d1204b1fe903/FileSystemWatcher.cs

My purpose with this is simple, I have a separated DLL from my current Unity project, the idea is simple, I want to recompile everything automatically from Unity when any change from the project of the DLL is changed, but I can't achieve that because of threads, so what can I do? Is there any alternative that listen files that is compatible with Unity?

Thanks.


Solution

  • I solved it with the help of @Kay, thanks @Kay!

    I wanted to make a more generic answer, so I decided to make my own class to achieve what I wanted. And this is the result:

    using System;
    using System.IO;
    using System.Collections.Generic;
    
    namespace Lerp2APIEditor.Utility
    {
        public class LerpedThread<T>
        {
            public T value = default(T);
            public bool isCalled = false;
            public string methodCalled = "";
            public Dictionary<string, Action> matchedMethods = new Dictionary<string, Action>();
    
            public FileSystemWatcher FSW
            {
                get
                {
                    return (FileSystemWatcher)(object)value;
                }
            }
            public LerpedThread(string name, FSWParams pars)
            {
                if(typeof(T) == typeof(FileSystemWatcher))
                {
                    FileSystemWatcher watcher = new FileSystemWatcher(pars.path, pars.filter);
    
                    watcher.NotifyFilter = pars.notifiers;
                    watcher.IncludeSubdirectories = pars.includeSubfolders;
    
                    watcher.Changed += new FileSystemEventHandler(OnChanged);
                    watcher.Created += new FileSystemEventHandler(OnCreated);
                    watcher.Deleted += new FileSystemEventHandler(OnDeleted);
                    watcher.Renamed += new RenamedEventHandler(OnRenamed);
    
                    ApplyChanges(watcher);
                }
            }
            private void OnChanged(object source, FileSystemEventArgs e)
            {
                methodCalled = "OnChanged";
                isCalled = true;
            }
            private void OnCreated(object source, FileSystemEventArgs e)
            {
                methodCalled = "OnCreated";
                isCalled = true;
            }
            private void OnDeleted(object source, FileSystemEventArgs e)
            {
                methodCalled = "OnDeleted";
                isCalled = true;
            }
            private void OnRenamed(object source, RenamedEventArgs e)
            {
                methodCalled = "OnRenamed";
                isCalled = true;
            }
            public void StartFSW()
            {
                FSW.EnableRaisingEvents = true;
            }
            public void CancelFSW()
            {
                FSW.EnableRaisingEvents = false;
            }
            public void ApplyChanges<T1>(T1 obj)
            {
                value = (T)(object)obj;
            }
        }
        public class FSWParams
        {
            public string path,
                          filter;
            public NotifyFilters notifiers;
            public bool includeSubfolders;
            public FSWParams(string p, string f, NotifyFilters nf, bool isf)
            {
                path = p;
                filter = f;
                notifiers = nf;
                includeSubfolders = isf;
            }
        }
    }
    

    Main class code:

    namespace Lerp2APIEditor
    {
        public class LerpedEditorCore
        {
    
            private static LerpedThread<FileSystemWatcher> m_Watcher;
    
            [InitializeOnLoadMethod]
            static void HookWatchers() 
            {
                EditorApplication.update += OnEditorApplicationUpdate;
    
                m_Watcher.matchedMethods.Add("OnChanged", () => {
                    Debug.Log("Origin files has been changed!");
                });
    
                m_Watcher.StartFSW();
            }
    
            static void OnEditorApplicationUpdate()
            {
                if(EditorApplication.timeSinceStartup > nextSeek)
                {
                    if (m_Watcher.isCalled)
                    {
                        foreach (KeyValuePair<string, Action> kv in m_Watcher.matchedMethods)
                            if (m_Watcher.methodCalled == kv.Key)
                                kv.Value();
                        m_Watcher.isCalled = false;
                    }
                    nextSeek = EditorApplication.timeSinceStartup + threadSeek;
                }
            }
        }
    }
    

    The thing I have done is very simple. I only created a generic class that create a FSW instance or whatever you want to listen. One time created, I attach the events that only activates the bool @Kay suggested me to use, and also the method called to know exactly what method have been called.

    Later in the main class, a foreach loops every method listed every second if a change has been detected, and the method linked to the string get called.