Потокобезопасный наблюдатель файловой системы для редактора Unity

#c# #multithreading #unity3d #mono #filesystemwatcher

#c# #многопоточность #unity3d #mono #filesystemwatcher

Вопрос:

Мне нужен потокобезопасный класс для наблюдателя файловой системы для использования в редакторе Unity, я уже знаю, что обработка потоков невозможна вне сопрограмм, но я не знал, что обработка потоков также запрещена в редакторе.

Итак, вот моя ошибка:

get_isEditor может быть вызван только из основного потока. Конструкторы и инициализаторы полей будут выполняться из потока загрузки при загрузке сцены. Не используйте эту функцию в конструкторе или инициализаторах полей, вместо этого переместите код инициализации в функцию Пробуждения или Запуска. 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_propisitor 0x0000000015AC46AA (моно JIT-код) (оболочка, управляемая в собственный) UnityEngine.Приложение: get_isEditor () 0x0000000015AC42FE (моно JIT-код) [Helpers.cs: 585] Lerp2API.DebugHandler.Отладка: Журнал (объект) 0x0000000015AC41C2 (моно JIT-код) [Helpers.cs:578] Lerp2API.DebugHandler.Отладка: Журнал (строка) 0x0000000015AC40F7 (моно JIT-код) [LerpedEditorCore.cs:101] Lerp2APIEditor.LerpedEditorCore:Перекомпилировать (объект,System.IO .FileSystemEventArgs) 0x0000000015AC3F2D (моно JIT-код) (обертка runtime-invoke) :времявыполнения_вызывает_аннулирует__этот___объект_object (object,intptr, intptr,intptr) 0x00007FFB400A519B (моно) [mini.c:4937] mono_jit_runtime_invoke 0x00007FFB3FFF84FD (моно) [object.c :2623] mono_runtime_invoke 0x00007FFB3FFFE8F7 (моно) [object.c:3827] mono_runtime_invoke_array 0x00007FFB3FFFEBCC (моно) [object.c:5457] mono_message_invoke 0x00007FFB4001EB8B (моно) [threadpool.c: 1019] mono_async_invoke 0x00007FFB4001F5E2 (моно) [threadpool.c: 1455 ] async_invoke_thread 0x00007FFB4002329F (моно) [потоки.c:685] start_wrapper 0x00007FFB400D78C9 (моно) [win32_threads.c:599] thread_start 0x00007FFB77FC8364 (KERNEL32) Базовый поток

Я скопировал полную трассировку стека, чтобы сообщить любому помощнику, где может возникнуть проблема. Потому что я искал решение, подобное любому потокобезопасному FWS, и да, оно есть, но только для .NET 4, и мне нужно одно для .NET 2

Это мой код:

 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!");
}
  

Как вы можете видеть, ничего особенного…

FSW, который я видел, был таким: https://gist.githubusercontent.com/bradsjm/2c839912294d0e2c008a/raw/c4a5c3d920ab46fdaa53b0e111e0d1204b1fe903/FileSystemWatcher.cs

Моя цель с этим проста, у меня есть отдельная библиотека DLL из моего текущего проекта Unity, идея проста, я хочу автоматически перекомпилировать все из Unity при изменении любого изменения в проекте DLL, но я не могу добиться этого из-за потоков, так что я могу сделать? Есть ли какая-либо альтернатива прослушиванию файлов, совместимая с Unity?

Спасибо.

Ответ №1:

По моему опыту, вы можете использовать потоки, но вы должны позаботиться о том, чтобы доступ к классам Unity осуществлялся только из основного потока. Я предлагаю передавать управление основному потоку всякий раз, когда ваш сторожевой таймер предупреждает.

 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();
    }
}
  

Опрос, конечно, довольно противный и уродливый процесс. В целом я предпочитаю подходы, основанные на событиях. Но в этом особом случае я не вижу возможности обмануть правило основного потока.

Комментарии:

1. Итак, проблема в том, что методы из Unity вызываются в другом потоке, а не? Я должен протестировать, но спасибо!

Ответ №2:

Я решил это с помощью @Kay, спасибо @Kay!

Я хотел дать более общий ответ, поэтому я решил создать свой собственный класс, чтобы достичь того, чего я хотел. И это результат:

 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;
        }
    }
}
  

Код основного класса:

 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;
            }
        }
    }
}
  

То, что я сделал, очень просто. Я создал только универсальный класс, который создает экземпляр FSW или что-то еще, что вы хотите прослушать. После создания я прикрепляю события, которые активируют только bool @Kay, предложенный мне для использования, а также вызываемый метод, чтобы точно знать, какой метод был вызван.

Позже в основном классе, при обнаружении изменений, каждый метод, указанный в списке, повторяется каждую секунду, и вызывается метод, связанный со строкой.

Комментарии:

1. Я думаю, что более экономичным способом было бы назначить пользовательский SynchronizingObject.