using System;
using System.Collections.Generic;
using System.Globalization;
using System.Net;
using System.Net.Sockets;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Codely.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEngine;
using UnityTcp.Editor.Helpers;
using UnityTcp.Editor.Native;
#if UNITY_EDITOR_WIN
using Cn.Tuanjie.Codely.Editor;
#endif
namespace UnityTcp.Editor.Tools
{
///
/// Bridges editor-window operations for browser/window bridge integrations via
/// NativeWindowBridge (libdatachannel) — window listing, input, and native streaming server.
///
// Auto-start the native stream server on domain reload so the stream page
// can reconnect immediately after Unity restarts without manual intervention.
[InitializeOnLoad]
public static class ManageWindowBridge
{
private const int AutoStartRetryDelayMs = 2000;
private const int AutoStartRetryMaxAttempts = 30;
private const double WindowListPushDebounceSeconds = 0.1;
private static int s_AutoStartRetryCount;
private static bool s_AutoStartRetryStopped;
private static bool s_WindowListChangeTrackingRegistered;
private static bool s_WindowListPushScheduled;
private static double s_WindowListPushDueTime;
private static string s_LastPushedWindowListJson = "";
#if UNITY_EDITOR_WIN
private const uint EVENT_OBJECT_CREATE = 0x8000;
private const uint EVENT_OBJECT_DESTROY = 0x8001;
private const uint WINEVENT_OUTOFCONTEXT = 0x0000;
private const int OBJID_WINDOW = 0;
private delegate void WinEventDelegate(
IntPtr hWinEventHook,
uint eventType,
IntPtr hwnd,
int idObject,
int idChild,
uint dwEventThread,
uint dwmsEventTime);
private static IntPtr s_WinEventHook = IntPtr.Zero;
private static WinEventDelegate s_WinEventDelegate;
private static int s_WinWindowEventPending;
[DllImport("user32.dll")]
private static extern IntPtr SetWinEventHook(
uint eventMin,
uint eventMax,
IntPtr hmodWinEventProc,
WinEventDelegate lpfnWinEventProc,
uint idProcess,
uint idThread,
uint dwFlags);
[DllImport("user32.dll")]
private static extern bool UnhookWinEvent(IntPtr hWinEventHook);
#endif
static ManageWindowBridge()
{
// Explicitly reset retry state on domain reload for clarity.
s_AutoStartRetryCount = 0;
s_AutoStartRetryStopped = false;
// Unhook WinEvent before domain reload / editor quit so the managed
// delegate does not dangle after Mono tears down the old AppDomain.
// Without this the hook callback fires into freed managed memory
// during the post-shutdown message loop, causing a native crash.
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
EditorApplication.quitting += OnEditorQuitting;
if (!IsStreamingSupportedOnCurrentPlatform())
{
#if UNITY_EDITOR_LINUX
CodelyLogger.Log("[NWB] Native streaming server is disabled on Linux.");
#elif UNITY_EDITOR_OSX
CodelyLogger.Log("[NWB] Native streaming server is disabled on macOS. ");
#endif
return;
}
if (EditorAutomationGuard.ShouldSkipBridgeAutoStart())
{
return;
}
// Delay to let the editor finish loading before starting the server.
EditorApplication.delayCall += AutoStartStreamServer;
}
private static void OnBeforeAssemblyReload()
{
// Unregister the WinEvent hook so the old managed delegate is not
// invoked after the current AppDomain is torn down.
UnregisterWinWindowEventHook();
}
private static void OnEditorQuitting()
{
// Stop the WebRTC streaming server BEFORE the native DLL is
// unloaded. NWB_StopServer tears down the PeerConnection whose
// destructor joins libdatachannel's internal processor thread.
// Without this, native threads survive DLL unload and Mono's
// thread manager fails to abort them — hanging the editor.
// StopServer already runs NWB_StopServer on a background thread
// with a timeout so the main thread is not blocked.
NativeWindowBridgeHost.StopServer();
// Must unhook BEFORE Application.Shutdown.CleanupMono destroys the
// Mono runtime; otherwise PeekMessage dispatches a pending
// WinEvent callback into freed managed thunk memory → crash.
UnregisterWindowListChangeTracking();
}
///
/// Linux/macOS support for NativeWindowBridge streaming is not production-ready yet.
/// Keep this hard-disabled until Linux/macOS native plugin/runtime paths are completed.
///
private static bool IsStreamingSupportedOnCurrentPlatform()
{
#if UNITY_EDITOR_LINUX
return false;
#elif UNITY_EDITOR_OSX
return false;
#else
return true;
#endif
}
private static void AutoStartStreamServer()
{
if (EditorAutomationGuard.ShouldSkipBridgeAutoStart())
{
return;
}
if (!IsStreamingSupportedOnCurrentPlatform())
{
return;
}
if (!NativeWindowBridgeHost.IsAvailable(forceRefresh: true))
{
string reason = NativeWindowBridgeHost.GetAvailabilityReason();
if (IsNonRetryablePluginFailure(reason))
{
if (!s_AutoStartRetryStopped)
{
s_AutoStartRetryStopped = true;
CodelyLogger.LogError(
"[NWB] AutoStartStreamServer detected non-retryable plugin failure. " +
$"reason={reason}. Please deploy a macOS x86_64/universal NativeWindowBridge + libdatachannel.");
}
return;
}
s_AutoStartRetryCount++;
if (s_AutoStartRetryCount <= AutoStartRetryMaxAttempts)
{
if (s_AutoStartRetryCount == 1 || s_AutoStartRetryCount % 5 == 0)
{
CodelyLogger.LogWarning(
"[NWB] AutoStartStreamServer: plugin not available yet; " +
$"attempt={s_AutoStartRetryCount}/{AutoStartRetryMaxAttempts}, reason={reason}. " +
$"Retrying in {AutoStartRetryDelayMs}ms...");
}
EditorApplication.delayCall += () =>
{
System.Threading.Tasks.Task.Delay(AutoStartRetryDelayMs).ContinueWith(_ =>
EditorApplication.delayCall += AutoStartStreamServer);
};
}
else if (!s_AutoStartRetryStopped)
{
s_AutoStartRetryStopped = true;
CodelyLogger.LogWarning(
"[NWB] AutoStartStreamServer stopped retrying after max attempts. " +
$"reason={reason}. You can still call start_stream_server manually after fixing plugin loading.");
}
return;
}
// Reset retry state once plugin becomes available.
s_AutoStartRetryCount = 0;
s_AutoStartRetryStopped = false;
// Use port=0 for OS-assigned dynamic port to avoid conflicts
// when multiple Unity Editor instances run on the same machine.
if (NativeWindowBridgeHost.StartOrAttach(0, out int boundPort))
{
PushWindowListToNative(force: true);
RegisterWindowListChangeTracking();
CodelyLogger.Log("[NWB] AutoStartStreamServer success on port " + boundPort);
}
else
{
CodelyLogger.LogWarning(
"[NWB] AutoStartStreamServer failed (dynamic port). " +
"detail=" + NativeWindowBridgeHost.GetAvailabilityReason());
}
}
private static bool IsNonRetryablePluginFailure(string reason)
{
if (string.IsNullOrEmpty(reason))
{
return false;
}
string normalized = reason.ToLowerInvariant();
// Architecture mismatch will never recover by waiting; retry only adds log noise.
return normalized.Contains("wrong architecture")
|| normalized.Contains("incompatible architecture")
|| normalized.Contains("mach-o file, but is an incompatible architecture")
|| normalized.Contains("no suitable image found");
}
private static readonly Dictionary> ActionHandlers =
new Dictionary>
{
{ "list_windows", _ => ListWindows() },
{ "focus_window", FocusWindow },
{ "resolve_native_window", ResolveNativeWindow },
{ "input", SendInput },
{ "start_stream_server", StartStreamServer },
{ "stop_stream_server", _ => StopStreamServer() },
{ "get_stream_server_status", _ => GetStreamServerStatus() },
{ "start_offscreen_stream", StartOffscreenStream },
{ "stop_offscreen_stream", _ => StopOffscreenStream() },
};
public static object HandleCommand(JObject @params)
=> ActionRouter.Route(@params, ActionHandlers);
private static object ListWindows()
{
try
{
EditorWindow[] allWindows = Resources.FindObjectsOfTypeAll();
var windows = new List