using System; using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using UnityEditor; using UnityEngine; [assembly: InternalsVisibleTo("Unity.Pipeline.Tests.Editor")] namespace Unity.Pipeline.Editor { /// /// Appends editor pipeline request/response transactions to <project>/Logs/pipeline.log as a /// JSON array. rotates any existing log to pipeline_old.log on /// the first server start of a Unity session: SessionState survives domain reloads but resets on /// editor restart, so the log accumulates across a session's server start/stop cycles and starts /// fresh each new session. /// /// Rotation must run on the main thread (SessionState is main-thread only), hence it lives in /// RotateForNewSession (called from server startup), NOT in Append — which runs on the background /// HTTP thread and is restricted to thread-safe file I/O. /// internal static class PipelineTransactionLog { private const string SessionRotatedKey = "Unity.Pipeline.TransactionLog.SessionRotated"; private const string LogFileName = "pipeline.log"; private const string OldLogFileName = "pipeline_old.log"; // Field names are the JSON keys, so they intentionally stay lowercase. request/response are // JToken so the raw JSON is embedded as real JSON (not a stringified blob) in the log array. private struct Entry { public JToken request; public JToken response; public string time; } /// /// On the first call of a Unity session, back up any existing pipeline.log to pipeline_old.log /// so the session starts with a fresh log. Gated by SessionState (resets on editor restart), /// so domain-reload server restarts within a session do not rotate. Main thread only. /// public static void RotateForNewSession() { try { if (SessionState.GetBool(SessionRotatedKey, false)) return; Rotate(GetLogsDirectory()); SessionState.SetBool(SessionRotatedKey, true); } catch (Exception ex) { Debug.LogWarning($"Pipeline transaction log rotation failed: {ex.Message}"); } } /// /// Append one transaction to the JSON-array log. Runs on the background HTTP thread, so it /// uses only thread-safe file I/O (no Unity editor APIs). /// public static void Append(string requestJson, string responseJson) { try { Append(GetLogsDirectory(), requestJson, responseJson); } catch (Exception ex) { // Logging must never break request handling. Debug.LogWarning($"Pipeline transaction log write failed: {ex.Message}"); } } /// /// Back up an existing pipeline.log in to pipeline_old.log, /// replacing any prior backup. No SessionState gate — exposed as the testable rotation seam. /// internal static void Rotate(string logsDir) { Directory.CreateDirectory(logsDir); var logPath = Path.Combine(logsDir, LogFileName); if (!File.Exists(logPath)) return; var oldPath = Path.Combine(logsDir, OldLogFileName); if (File.Exists(oldPath)) File.Delete(oldPath); File.Move(logPath, oldPath); } /// /// Append a transaction to the JSON-array log in an explicit directory. Testable seam behind /// the public . /// internal static void Append(string logsDir, string requestJson, string responseJson) { Directory.CreateDirectory(logsDir); var logPath = Path.Combine(logsDir, LogFileName); var entries = ReadEntries(logPath); entries.Add(new Entry { request = ToJsonToken(requestJson), response = ToJsonToken(responseJson), time = DateTime.UtcNow.ToString("o") }); File.WriteAllText(logPath, JsonConvert.SerializeObject(entries, Formatting.Indented)); } /// /// Embed a raw JSON payload as a real JSON token. Falls back to a JSON string for empty or /// non-JSON payloads, so the log file always stays valid JSON. /// private static JToken ToJsonToken(string raw) { if (string.IsNullOrEmpty(raw)) return JValue.CreateNull(); try { return JToken.Parse(raw); } catch (JsonException) { return new JValue(raw); } } private static string GetLogsDirectory() { var projectPath = Path.GetDirectoryName(Application.dataPath); return Path.Combine(projectPath, "Logs"); } private static List ReadEntries(string logPath) { if (!File.Exists(logPath)) return new List(); var json = File.ReadAllText(logPath); if (string.IsNullOrEmpty(json)) return new List(); return JsonConvert.DeserializeObject>(json) ?? new List(); } } }