Initial commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce2f52369a953aa419ecda9c2ed4e8a2
|
||||
129
skills/assets/unity-package/Editor/Utils/Logger.cs
Normal file
129
skills/assets/unity-package/Editor/Utils/Logger.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
skills/assets/unity-package/Editor/Utils/Logger.cs.meta
Normal file
2
skills/assets/unity-package/Editor/Utils/Logger.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2ba586fdee5d81d469336faddb749a37
|
||||
221
skills/assets/unity-package/Editor/Utils/ResponseQueue.cs
Normal file
221
skills/assets/unity-package/Editor/Utils/ResponseQueue.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82b4b7666613c1941b9a6c7871daba9e
|
||||
Reference in New Issue
Block a user