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.
From my experience you can use threads but you have to take care that access to Unity classes is performed from the main thread only. My suggestion is to hand over control to the main thread whenever your watchdog alerts.
static bool _triggerRecompile = false;
[InitializeOnLoadMethod]
static void HookWatcher()
{
m_Watcher = new FileSystemWatcher("path", "*.cs");
// ....
m_Watcher.Changed += Recompile;
EditorApplication.update += OnEditorApplicationUpdate;
}
private static void Recompile(object sender, FileSystemEventArgs e)
{
bool _triggerRecompile = true;
// Never call any Unity classes as we are not in the main thread
}
static void OnEditorApplicationUpdate ()
{
// note that this is called very often (100/sec)
if (_triggerRecompile)
{
_triggerRecompile = false;
Debug.Log("Origin files has been changed!");
DoRecompile();
}
}
Polling is of course kind of nasty and ugly. In general I prefer event based approaches. But in this special case I see no chance to cheat the main thread rule.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With