using System; using System.Collections.Concurrent; using System.IO; using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; using UnityEditor; using UnityEngine; namespace Jovian.Logger { /// /// TCP server that receives log messages from RemoteLogSender on device builds. /// Managed by Custom Console — not intended for standalone use. /// internal static class RemoteLogReceiver { public const int DefaultPort = 9876; public struct RemoteLogEntry { public string message; public string stackTrace; public string timestamp; public int frameCount; public JovianLogType type; public LogCategory logCategory; public bool isCustomLog; } public struct RemoteWatchEntry { public string key; public string value; public LogCategory logCategory; public string timestamp; public int frameCount; } public static event Action OnRemoteLog; public static event Action OnRemoteWatch; public static event Action OnRemoteUnwatch; public static event Action OnClientConnected; public static event Action OnClientDisconnected; private static TcpListener listener; private static Thread listenThread; private static volatile bool running; private static int port = DefaultPort; public static bool IsRunning => running; public static int Port => port; public static string ConnectedClientName { get; private set; } = ""; public static bool HasClient { get; private set; } private static readonly ConcurrentQueue mainThreadQueue = new ConcurrentQueue(); public static void Start(int listenPort = DefaultPort) { if (running) return; port = listenPort; try { listener = new TcpListener(IPAddress.Any, port); listener.Start(); running = true; listenThread = new Thread(ListenLoop) { IsBackground = true, Name = "RemoteLogReceiver" }; listenThread.Start(); EditorApplication.update += ProcessMainThreadQueue; Debug.Log($"[RemoteLog] Listening on port {port}"); } catch (Exception e) { Debug.LogError($"[RemoteLog] Failed to start listener: {e.Message}"); running = false; } } public static void Stop() { running = false; HasClient = false; ConnectedClientName = ""; try { listener?.Stop(); } catch { } listener = null; EditorApplication.update -= ProcessMainThreadQueue; } private static void ProcessMainThreadQueue() { while (mainThreadQueue.TryDequeue(out var action)) { action?.Invoke(); } } private static void ListenLoop() { while (running) { try { if (!listener.Pending()) { Thread.Sleep(100); continue; } var client = listener.AcceptTcpClient(); client.NoDelay = true; client.ReceiveTimeout = 0; HandleClient(client); } catch (SocketException) { if (!running) break; } catch (ObjectDisposedException) { break; } } } private static void HandleClient(TcpClient client) { var thread = new Thread(() => ClientReadLoop(client)) { IsBackground = true, Name = "RemoteLogClient" }; thread.Start(); } private static void ClientReadLoop(TcpClient client) { try { using var stream = client.GetStream(); using var reader = new StreamReader(stream, Encoding.UTF8); HasClient = true; while (running && client.Connected) { string line = reader.ReadLine(); if (line == null) break; if (string.IsNullOrWhiteSpace(line)) continue; ParseAndDispatch(line); } } catch (IOException) { // Connection lost } catch (ObjectDisposedException) { // Shutting down } finally { try { client.Close(); } catch { } HasClient = false; ConnectedClientName = ""; mainThreadQueue.Enqueue(() => OnClientDisconnected?.Invoke()); } } private static void ParseAndDispatch(string json) { try { // Check for handshake if (json.Contains("\"handshake\"")) { string appName = ExtractJsonString(json, "app"); string platform = ExtractJsonString(json, "platform"); ConnectedClientName = $"{appName} ({platform})"; mainThreadQueue.Enqueue(() => OnClientConnected?.Invoke(ConnectedClientName)); return; } // Check for watch if (json.Contains("\"watch\"")) { var watchEntry = new RemoteWatchEntry { key = ExtractJsonString(json, "wk"), value = ExtractJsonString(json, "wv"), logCategory = (LogCategory)ExtractJsonInt(json, "c"), timestamp = ExtractJsonString(json, "ts"), frameCount = ExtractJsonInt(json, "fc"), }; mainThreadQueue.Enqueue(() => OnRemoteWatch?.Invoke(watchEntry)); return; } // Check for unwatch if (json.Contains("\"unwatch\"")) { string key = ExtractJsonString(json, "wk"); mainThreadQueue.Enqueue(() => OnRemoteUnwatch?.Invoke(key)); return; } var entry = new RemoteLogEntry { message = ExtractJsonString(json, "m"), stackTrace = ExtractJsonString(json, "s"), timestamp = ExtractJsonString(json, "ts"), frameCount = ExtractJsonInt(json, "fc"), type = (JovianLogType)ExtractJsonInt(json, "t"), logCategory = (LogCategory)ExtractJsonInt(json, "c"), isCustomLog = ExtractJsonBool(json, "f"), }; mainThreadQueue.Enqueue(() => OnRemoteLog?.Invoke(entry)); } catch (Exception e) { Debug.LogWarning($"[RemoteLog] Failed to parse: {e.Message}"); } } // Lightweight JSON field extractors (avoids dependency on full JSON parser for simple protocol) private static string ExtractJsonString(string json, string key) { string pattern = "\"" + key + "\":\""; int start = json.IndexOf(pattern, StringComparison.Ordinal); if (start < 0) return ""; start += pattern.Length; var sb = new StringBuilder(); for (int i = start; i < json.Length; i++) { char c = json[i]; if (c == '\\' && i + 1 < json.Length) { char next = json[i + 1]; switch (next) { case '"': sb.Append('"'); i++; break; case '\\': sb.Append('\\'); i++; break; case 'n': sb.Append('\n'); i++; break; case 'r': sb.Append('\r'); i++; break; case 't': sb.Append('\t'); i++; break; default: sb.Append(c); break; } } else if (c == '"') { break; } else { sb.Append(c); } } return sb.ToString(); } private static int ExtractJsonInt(string json, string key) { string pattern = "\"" + key + "\":"; int start = json.IndexOf(pattern, StringComparison.Ordinal); if (start < 0) return 0; start += pattern.Length; int end = start; while (end < json.Length && (char.IsDigit(json[end]) || json[end] == '-')) end++; if (end == start) return 0; return int.TryParse(json.AsSpan(start, end - start), out int val) ? val : 0; } private static bool ExtractJsonBool(string json, string key) { string pattern = "\"" + key + "\":"; int start = json.IndexOf(pattern, StringComparison.Ordinal); if (start < 0) return false; start += pattern.Length; return start < json.Length && json[start] == 't'; } } }