Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:19:28 +08:00
commit 1da7b24c8e
254 changed files with 43797 additions and 0 deletions

View File

@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using UnityEditor;
namespace UnityEditorToolkit.Editor.Utils
{
/// <summary>
/// Editor 메인 스레드에서 작업을 실행하기 위한 Static Dispatcher
/// MonoBehaviour를 사용하지 않아 Scene에 독립적으로 동작
/// WebSocket 등 다른 스레드에서 Unity API를 호출할 때 사용
/// </summary>
public static class EditorMainThreadDispatcher
{
private static readonly Queue<Action> executionQueue = new Queue<Action>();
private static readonly object @lock = new object();
private static bool isInitialized = false;
/// <summary>
/// Editor 시작 시 자동 초기화
/// </summary>
[InitializeOnLoadMethod]
private static void Initialize()
{
if (!isInitialized)
{
EditorApplication.update += ProcessQueue;
isInitialized = true;
}
}
/// <summary>
/// 메인 스레드에서 실행할 작업 등록
/// </summary>
/// <param name="action">실행할 작업</param>
public static void Enqueue(Action action)
{
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
lock (@lock)
{
executionQueue.Enqueue(action);
}
}
/// <summary>
/// 메인 스레드에서 실행할 작업 등록 (콜백 포함)
/// </summary>
/// <param name="action">실행할 작업</param>
/// <param name="callback">완료 후 콜백 (예외 발생 시 예외 전달, 성공 시 null)</param>
public static void Enqueue(Action action, Action<Exception> callback)
{
if (action == null)
{
throw new ArgumentNullException(nameof(action));
}
lock (@lock)
{
executionQueue.Enqueue(() =>
{
try
{
action.Invoke();
callback?.Invoke(null);
}
catch (Exception ex)
{
callback?.Invoke(ex);
}
});
}
}
/// <summary>
/// 큐에 있는 작업들을 메인 스레드에서 처리
/// EditorApplication.update에서 자동 호출
/// </summary>
private static void ProcessQueue()
{
// 최대 처리 시간 제한 (프레임 드롭 방지)
const int maxProcessTimeMs = 10;
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
lock (@lock)
{
while (executionQueue.Count > 0 && stopwatch.ElapsedMilliseconds < maxProcessTimeMs)
{
var action = executionQueue.Dequeue();
try
{
action.Invoke();
}
catch (Exception ex)
{
ToolkitLogger.LogError("EditorMainThreadDispatcher", $"Error executing action: {ex.Message}\n{ex.StackTrace}");
}
}
}
}
/// <summary>
/// 큐에 있는 작업 개수
/// </summary>
public static int QueueCount
{
get
{
lock (@lock)
{
return executionQueue.Count;
}
}
}
/// <summary>
/// 큐 비우기
/// </summary>
public static void ClearQueue()
{
lock (@lock)
{
executionQueue.Clear();
}
}
/// <summary>
/// 초기화 여부
/// </summary>
public static bool IsInitialized => isInitialized;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ce2f52369a953aa419ecda9c2ed4e8a2

View File

@@ -0,0 +1,129 @@
using UnityEngine;
using UnityEditor;
namespace UnityEditorToolkit.Editor.Utils
{
/// <summary>
/// Centralized logging utility with log level filtering
/// All handlers should use this instead of Debug.Log directly
/// </summary>
public static class ToolkitLogger
{
public enum LogLevel
{
None = 0,
Error = 1,
Warning = 2,
Info = 3,
Debug = 4
}
private const string PREF_LOG_LEVEL = "UnityEditorToolkit.Server.LogLevel";
private const string PREFIX = "[UnityEditorToolkit]";
/// <summary>
/// Current log level (shared with EditorWebSocketServer)
/// </summary>
public static LogLevel CurrentLogLevel
{
get => (LogLevel)EditorPrefs.GetInt(PREF_LOG_LEVEL, (int)LogLevel.Info);
set => EditorPrefs.SetInt(PREF_LOG_LEVEL, (int)value);
}
/// <summary>
/// Log an error message (always shown unless LogLevel.None)
/// </summary>
public static void LogError(string message)
{
if (CurrentLogLevel >= LogLevel.Error)
{
Debug.LogError($"{PREFIX} {message}");
}
}
/// <summary>
/// Log an error message with context
/// </summary>
public static void LogError(string category, string message)
{
if (CurrentLogLevel >= LogLevel.Error)
{
Debug.LogError($"{PREFIX}[{category}] {message}");
}
}
/// <summary>
/// Log a warning message
/// </summary>
public static void LogWarning(string message)
{
if (CurrentLogLevel >= LogLevel.Warning)
{
Debug.LogWarning($"{PREFIX} {message}");
}
}
/// <summary>
/// Log a warning message with context
/// </summary>
public static void LogWarning(string category, string message)
{
if (CurrentLogLevel >= LogLevel.Warning)
{
Debug.LogWarning($"{PREFIX}[{category}] {message}");
}
}
/// <summary>
/// Log an info message
/// </summary>
public static void Log(string message)
{
if (CurrentLogLevel >= LogLevel.Info)
{
Debug.Log($"{PREFIX} {message}");
}
}
/// <summary>
/// Log an info message with context
/// </summary>
public static void Log(string category, string message)
{
if (CurrentLogLevel >= LogLevel.Info)
{
Debug.Log($"{PREFIX}[{category}] {message}");
}
}
/// <summary>
/// Log a debug message (verbose)
/// </summary>
public static void LogDebug(string message)
{
if (CurrentLogLevel >= LogLevel.Debug)
{
Debug.Log($"{PREFIX}[DEBUG] {message}");
}
}
/// <summary>
/// Log a debug message with context (verbose)
/// </summary>
public static void LogDebug(string category, string message)
{
if (CurrentLogLevel >= LogLevel.Debug)
{
Debug.Log($"{PREFIX}[{category}][DEBUG] {message}");
}
}
/// <summary>
/// Check if a log level is enabled
/// </summary>
public static bool IsLogLevelEnabled(LogLevel level)
{
return CurrentLogLevel >= level;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 2ba586fdee5d81d469336faddb749a37

View File

@@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace UnityEditorToolkit.Editor.Utils
{
/// <summary>
/// Queue for delayed responses
/// Allows handlers to register responses that will be sent later when conditions are met
/// </summary>
public class ResponseQueue
{
private static ResponseQueue instance;
public static ResponseQueue Instance
{
get
{
if (instance == null)
{
instance = new ResponseQueue();
}
return instance;
}
}
private class PendingResponse
{
public string requestId;
public Func<bool> condition;
public Func<object> resultProvider;
public Action<string> sendCallback;
public double registeredTime;
public double timeoutSeconds;
public bool IsTimedOut(double currentTime)
{
return currentTime - registeredTime > timeoutSeconds;
}
}
private List<PendingResponse> pendingResponses = new List<PendingResponse>();
/// <summary>
/// Register a delayed response
/// </summary>
/// <param name="requestId">JSON-RPC request ID</param>
/// <param name="condition">Condition to check (returns true when ready to send)</param>
/// <param name="resultProvider">Function to provide result when condition is met</param>
/// <param name="sendCallback">Callback to send response (receives JSON string)</param>
/// <param name="timeoutSeconds">Timeout in seconds</param>
public void Register(
string requestId,
Func<bool> condition,
Func<object> resultProvider,
Action<string> sendCallback,
double timeoutSeconds = 300.0)
{
var response = new PendingResponse
{
requestId = requestId,
condition = condition,
resultProvider = resultProvider,
sendCallback = sendCallback,
registeredTime = UnityEditor.EditorApplication.timeSinceStartup,
timeoutSeconds = timeoutSeconds
};
pendingResponses.Add(response);
ToolkitLogger.Log("ResponseQueue", $" Registered delayed response for request {requestId} (timeout: {timeoutSeconds}s)");
}
/// <summary>
/// Process pending responses (called from EditorApplication.update)
/// </summary>
public void Update()
{
if (pendingResponses.Count == 0)
return;
double currentTime = UnityEditor.EditorApplication.timeSinceStartup;
List<PendingResponse> toRemove = new List<PendingResponse>();
foreach (var response in pendingResponses)
{
try
{
// Check timeout
if (response.IsTimedOut(currentTime))
{
ToolkitLogger.LogWarning("ResponseQueue", $" Request {response.requestId} timed out after {response.timeoutSeconds}s");
// Send timeout error
var errorResponse = new
{
jsonrpc = "2.0",
id = response.requestId,
error = new
{
code = -32000,
message = $"Wait condition timed out after {response.timeoutSeconds} seconds"
}
};
response.sendCallback(Newtonsoft.Json.JsonConvert.SerializeObject(errorResponse));
toRemove.Add(response);
continue;
}
// Check condition
if (response.condition())
{
ToolkitLogger.Log("ResponseQueue", $" Condition met for request {response.requestId}");
try
{
// Get result and send response
var result = response.resultProvider();
var successResponse = new
{
jsonrpc = "2.0",
id = response.requestId,
result = result
};
response.sendCallback(Newtonsoft.Json.JsonConvert.SerializeObject(successResponse));
toRemove.Add(response);
}
catch (System.InvalidOperationException ex)
{
// WebSocket already closed (client disconnected or timed out)
ToolkitLogger.LogWarning("ResponseQueue", $" WebSocket already closed for request {response.requestId}: {ex.Message}");
toRemove.Add(response);
}
catch (System.Exception ex)
{
ToolkitLogger.LogError("ResponseQueue", $" Error sending success response for request {response.requestId}: {ex.Message}");
toRemove.Add(response);
}
}
}
catch (Exception ex)
{
ToolkitLogger.LogError("ResponseQueue", $" Error processing response for request {response.requestId}: {ex.Message}");
// Send error response
var errorResponse = new
{
jsonrpc = "2.0",
id = response.requestId,
error = new
{
code = -32603,
message = $"Internal error: {ex.Message}"
}
};
response.sendCallback(Newtonsoft.Json.JsonConvert.SerializeObject(errorResponse));
toRemove.Add(response);
}
}
// Remove completed/timed out responses
foreach (var response in toRemove)
{
pendingResponses.Remove(response);
}
}
/// <summary>
/// Get number of pending responses
/// </summary>
public int PendingCount => pendingResponses.Count;
/// <summary>
/// Clear all pending responses
/// </summary>
public void Clear()
{
pendingResponses.Clear();
ToolkitLogger.Log("ResponseQueue", "Cleared all pending responses");
}
/// <summary>
/// Cancel all pending responses with error message
/// Called when server stops or domain reloads to notify clients
/// </summary>
public void CancelAllPending(string reason)
{
if (pendingResponses.Count == 0)
return;
ToolkitLogger.Log("ResponseQueue", $" Cancelling {pendingResponses.Count} pending response(s): {reason}");
foreach (var response in pendingResponses)
{
try
{
var errorResponse = new
{
jsonrpc = "2.0",
id = response.requestId,
error = new
{
code = -32000,
message = reason
}
};
response.sendCallback(Newtonsoft.Json.JsonConvert.SerializeObject(errorResponse));
}
catch (System.InvalidOperationException ex)
{
// WebSocket already closed
ToolkitLogger.LogWarning("ResponseQueue", $" WebSocket already closed for request {response.requestId}: {ex.Message}");
}
catch (System.Exception ex)
{
ToolkitLogger.LogWarning("ResponseQueue", $" Failed to send cancellation to {response.requestId}: {ex.Message}");
}
}
pendingResponses.Clear();
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 82b4b7666613c1941b9a6c7871daba9e