154 lines
5.6 KiB
C#
154 lines
5.6 KiB
C#
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
|
|
{
|
|
/// <summary>
|
|
/// Appends editor pipeline request/response transactions to <project>/Logs/pipeline.log as a
|
|
/// JSON array. <see cref="RotateForNewSession"/> 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.
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
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}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Back up an existing pipeline.log in <paramref name="logsDir"/> to pipeline_old.log,
|
|
/// replacing any prior backup. No SessionState gate — exposed as the testable rotation seam.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Append a transaction to the JSON-array log in an explicit directory. Testable seam behind
|
|
/// the public <see cref="Append(string,string)"/>.
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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<Entry> ReadEntries(string logPath)
|
|
{
|
|
if (!File.Exists(logPath))
|
|
return new List<Entry>();
|
|
|
|
var json = File.ReadAllText(logPath);
|
|
if (string.IsNullOrEmpty(json))
|
|
return new List<Entry>();
|
|
|
|
return JsonConvert.DeserializeObject<List<Entry>>(json) ?? new List<Entry>();
|
|
}
|
|
}
|
|
}
|