Skip to content
Open
Show file tree
Hide file tree
Changes from 51 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
aa37631
add new console log cache to persist log data
jstricklin Sep 30, 2025
165bbc8
update log cache with R3 timer to remove unityeditor dependency and u…
jstricklin Oct 3, 2025
f28d16c
update log cache method and variables
jstricklin Oct 3, 2025
324261b
update log cache with simplified file i/o and update log file path
jstricklin Oct 3, 2025
c537cde
refactor logcache initialization
jstricklin Oct 3, 2025
882173d
update script to resolve merge conflict
jstricklin Oct 4, 2025
919610d
add test to validate log utils logentry count
jstricklin Oct 4, 2025
8b4f578
remove extraneous logMessageReceived subscription and update test script
jstricklin Oct 4, 2025
88e882b
extract LogWrapper class from LogCache
jstricklin Oct 4, 2025
42a390f
update console log file read with async logic
jstricklin Oct 5, 2025
d759244
refactor subscription logic to leverage existing hooks, code cleanup
jstricklin Oct 11, 2025
628cb26
remove unity editor dependency in test script
jstricklin Oct 12, 2025
6e611c6
remove unity editor dependency in test script
jstricklin Oct 15, 2025
44f4315
refactor access to _logEntries, strict variable definitions, and modi…
jstricklin Oct 15, 2025
7d537a6
update test to allow more file write/read time
jstricklin Oct 16, 2025
94c0d51
update restore semaphore to address aync/await issues and add complet…
jstricklin Oct 17, 2025
a72402f
update validate log count test
jstricklin Oct 22, 2025
49a101d
disable log count test
jstricklin Oct 27, 2025
34e4e29
disable log retention test
jstricklin Oct 27, 2025
6093dad
update logic to better handle application quit
jstricklin Oct 28, 2025
abd1d19
update console log application quit logic
jstricklin Oct 28, 2025
be6c4e7
update console log cache init
jstricklin Oct 28, 2025
e3348b2
update utils to check for logcache existance before save/load actions…
jstricklin Nov 5, 2025
5375aaf
update log cache instance null check warning to still allow save/load…
jstricklin Nov 5, 2025
326b698
update log cache logic to avoid null instances by instead not setting…
jstricklin Nov 5, 2025
765f27b
Merge branch 'main' into persist-console-logs
IvanMurzak Nov 8, 2025
a197e94
Refactor: Simplify LogCache class structure and improve resource mana…
IvanMurzak Nov 8, 2025
10b5a53
Refactor: Simplify JSON serialization in LogCache by removing unneces…
IvanMurzak Nov 8, 2025
f576b87
Refactor: Update LogEntry properties to use PascalCase and improve JS…
IvanMurzak Nov 8, 2025
443548e
Refactor: Remove LogTypeString property and update LogEntry usage in …
IvanMurzak Nov 8, 2025
c2b59ab
Refactor: Change _logEntries to readonly and update LoadFromFile to e…
IvanMurzak Nov 8, 2025
fcf345e
Initial plan
Copilot Nov 8, 2025
791fc18
Update Unity-MCP-Plugin/Assets/root/Runtime/Unity/Logs/LogCache.cs
IvanMurzak Nov 8, 2025
9529eab
Refactor: Change LogCache constructor access modifier from private to…
IvanMurzak Nov 8, 2025
edbf664
Merge branch 'fix/persist-console-logs' of https://github.com/IvanMur…
IvanMurzak Nov 8, 2025
a4bbb05
Add XML documentation to SaveToFile, LoadFromFile, and HandleQuit met…
Copilot Nov 8, 2025
da6b874
Merge pull request #301 from IvanMurzak/copilot/sub-pr-300
IvanMurzak Nov 8, 2025
43ccc65
Refactor LogEntry constructor to use named parameters and improve cla…
IvanMurzak Nov 8, 2025
8d78c80
Refactor LogCache to instance-based usage
IvanMurzak Nov 9, 2025
5faeefb
Merge branch 'main' into persist-console-logs
IvanMurzak Nov 9, 2025
da85c8f
Merge pull request #303 from IvanMurzak/update/non-static-log-cache
IvanMurzak Nov 9, 2025
fb9b1cf
Merge branch 'persist-console-logs' into fix/persist-console-logs
IvanMurzak Nov 10, 2025
596454d
Merge branch 'main' into persist-console-logs
IvanMurzak Nov 10, 2025
bfde02b
Merge branch 'persist-console-logs' into fix/persist-console-logs
IvanMurzak Nov 10, 2025
f8a1356
Refactor log caching methods for improved performance and clarity
IvanMurzak Nov 10, 2025
beddefd
Enhance logging functionality with main thread checks and improved Lo…
IvanMurzak Nov 10, 2025
db82767
Merge branch 'main' into persist-console-logs
IvanMurzak Nov 10, 2025
3a94158
Merge branch 'persist-console-logs' into fix/persist-console-logs
IvanMurzak Nov 10, 2025
6e871ed
Refactor CacheLogEntriesAsync and GetCachedLogEntriesAsync to use asy…
IvanMurzak Nov 10, 2025
350192a
Use 'using' statement for file stream in GetCachedLogEntriesAsync to …
IvanMurzak Nov 10, 2025
8cbf312
Merge pull request #300 from IvanMurzak/fix/persist-console-logs
IvanMurzak Nov 10, 2025
1b521c2
Merge branch 'main' into persist-console-logs
IvanMurzak Nov 10, 2025
9d66ba7
Apply suggestions from code review
IvanMurzak Nov 10, 2025
a2496fd
Merge branch 'main' into persist-console-logs
IvanMurzak Nov 11, 2025
8c03b3e
Merge branch 'main' into persist-console-logs
jstricklin Nov 14, 2025
8ef6be1
update cache save to file logic to handle synchronous call on applica…
jstricklin Nov 15, 2025
b23750c
update handlequit and add _saving bool to skip cache save subscriptio…
jstricklin Nov 15, 2025
a4b96ed
Merge branch 'main' into persist-console-logs
IvanMurzak Nov 15, 2025
2ce8d91
Merge branch 'main' into persist-console-logs
jstricklin Nov 25, 2025
3694856
Merge branch 'main' into persist-console-logs
IvanMurzak Dec 1, 2025
b66c529
Merge branch 'main' into persist-console-logs
IvanMurzak Dec 7, 2025
76f9b14
Suppress unused task warnings by discarding results from LogUtils.Han…
IvanMurzak Dec 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ public string GetLogs
{
var cutoffTime = DateTime.Now.AddMinutes(-lastMinutes);
allLogs = allLogs
.Where(log => log.timestamp >= cutoffTime);
.Where(log => log.Timestamp >= cutoffTime);
}

// Apply log type filter
if (logTypeFilter.HasValue)
{
allLogs = allLogs
.Where(log => log.logType == logTypeFilter.Value);
.Where(log => log.LogType == logTypeFilter.Value);
}

// Take the most recent entries (up to maxEntries)
Expand Down Expand Up @@ -92,4 +92,4 @@ public string GetLogs
});
}
}
}
}
5 changes: 5 additions & 0 deletions Unity-MCP-Plugin/Assets/root/Editor/Scripts/Startup.Editor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ static void OnApplicationUnloading()
UnityMcpPlugin.Instance.LogInfo("{method} triggered", typeof(Startup), nameof(OnApplicationUnloading));
UnityMcpPlugin.Instance.DisconnectImmediate();
}
LogUtils.SaveToFile();
}
static void OnApplicationQuitting()
{
Expand All @@ -43,6 +44,7 @@ static void OnApplicationQuitting()
UnityMcpPlugin.Instance.LogInfo("{method} triggered", typeof(Startup), nameof(OnApplicationQuitting));
UnityMcpPlugin.Instance.DisconnectImmediate();
}
LogUtils.HandleQuit();
}
static void OnBeforeAssemblyReload()
{
Expand All @@ -51,6 +53,7 @@ static void OnBeforeAssemblyReload()
UnityMcpPlugin.Instance.LogInfo("{method} triggered", typeof(Startup), nameof(OnBeforeAssemblyReload));
UnityMcpPlugin.Instance.DisconnectImmediate();
}
LogUtils.SaveToFile();
}
static void OnAfterAssemblyReload()
{
Expand All @@ -63,6 +66,8 @@ static void OnAfterAssemblyReload()

if (connectionAllowed)
UnityMcpPlugin.ConnectIfNeeded();

LogUtils.LoadFromFile();
}

static void OnPlayModeStateChanged(PlayModeStateChange state)
Expand Down
147 changes: 147 additions & 0 deletions Unity-MCP-Plugin/Assets/root/Runtime/Unity/Logs/LogCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
/*
┌──────────────────────────────────────────────────────────────────┐
│ Author: Ivan Murzak (https://github.com/IvanMurzak) │
│ Repository: GitHub (https://github.com/IvanMurzak/Unity-MCP) │
│ Copyright (c) 2025 Ivan Murzak │
│ Licensed under the Apache License, Version 2.0. │
│ See the LICENSE file in the project root for more information. │
└──────────────────────────────────────────────────────────────────┘
*/

#nullable enable
using System;
using System.IO;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using com.IvanMurzak.ReflectorNet.Utils;
using R3;
using UnityEngine;

namespace com.IvanMurzak.Unity.MCP
{
internal class LogCache : IDisposable
{
readonly string _cacheFilePath;
readonly string _cacheFileName;
readonly string _cacheFile;
readonly JsonSerializerOptions _jsonOptions;
readonly SemaphoreSlim _fileLock = new(1, 1);
readonly CancellationTokenSource _shutdownCts = new();

IDisposable? timerSubscription;

internal LogCache(string? cacheFilePath = null, string? cacheFileName = null, JsonSerializerOptions? jsonOptions = null)
{
if (!MainThread.Instance.IsMainThread)
throw new System.Exception($"{nameof(LogUtils)} must be initialized on the main thread.");

_cacheFilePath = cacheFilePath ?? (Application.isEditor
? $"{Path.GetDirectoryName(Application.dataPath)}/Temp/mcp-server"
: $"{Application.persistentDataPath}/Temp/mcp-server");

_cacheFileName = cacheFileName ?? (Application.isEditor
? "editor-logs.txt"
: "player-logs.txt");

_cacheFile = $"{Path.Combine(_cacheFilePath, _cacheFileName)}";
_jsonOptions = jsonOptions ?? new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
WriteIndented = false,
};

timerSubscription = Observable.Timer(
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(1)
)
.Subscribe(x =>
{
if (!_shutdownCts.IsCancellationRequested)
Task.Run(HandleLogCache, _shutdownCts.Token);
});
}

public async Task HandleQuit()
{
if (!_shutdownCts.IsCancellationRequested)
_shutdownCts.Cancel();
timerSubscription?.Dispose();
await HandleLogCache();
}

public async Task HandleLogCache()
{
if (LogUtils.LogEntries > 0)
{
var logs = LogUtils.GetAllLogs();
await CacheLogEntriesAsync(logs);
}
}

public Task CacheLogEntriesAsync(LogEntry[] entries)
{
return Task.Run(async () =>
{
await _fileLock.WaitAsync();
try
{
var data = new LogWrapper { Entries = entries };

if (!Directory.Exists(_cacheFilePath))
Directory.CreateDirectory(_cacheFilePath);

// Stream JSON directly to file without creating entire JSON string in memory
var tempFile = _cacheFile + ".tmp";
using (var fileStream = new FileStream(tempFile, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true))
{
await System.Text.Json.JsonSerializer.SerializeAsync(fileStream, data, _jsonOptions);
await fileStream.FlushAsync();
}

// Atomic file replacement
if (File.Exists(_cacheFile))
File.Delete(_cacheFile);
File.Move(tempFile, _cacheFile);
}
finally
{
_fileLock.Release();
}
});
}
public Task<LogWrapper?> GetCachedLogEntriesAsync()
{
return Task.Run(async () =>
{
await _fileLock.WaitAsync();
try
{
if (!File.Exists(_cacheFile))
return null;

using var fileStream = File.OpenRead(_cacheFile);
return await System.Text.Json.JsonSerializer.DeserializeAsync<LogWrapper>(fileStream, _jsonOptions);
}
finally
{
_fileLock.Release();
}
});
}

public void Dispose()
{
timerSubscription?.Dispose();
timerSubscription = null;

if (!_shutdownCts.IsCancellationRequested)
_shutdownCts.Cancel();
_shutdownCts.Dispose();

_fileLock.Dispose();
}

~LogCache() => Dispose();
}
}
11 changes: 11 additions & 0 deletions Unity-MCP-Plugin/Assets/root/Runtime/Unity/Logs/LogCache.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

48 changes: 32 additions & 16 deletions Unity-MCP-Plugin/Assets/root/Runtime/Unity/Logs/LogEntry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,48 @@

namespace com.IvanMurzak.Unity.MCP
{
[Serializable]
public class LogEntry
{
public string message;
public string stackTrace;
public LogType logType;
public DateTime timestamp;
public string logTypeString;
public LogType LogType { get; set; }
public string Message { get; set; }
public DateTime Timestamp { get; set; }
public string? StackTrace { get; set; }

public LogEntry(string message, string stackTrace, LogType logType)
public LogEntry()
{
this.message = message;
this.stackTrace = stackTrace;
this.logType = logType;
this.timestamp = DateTime.Now;
this.logTypeString = logType.ToString();
LogType = LogType.Log;
Message = string.Empty;
Timestamp = DateTime.Now;
StackTrace = null;
}
public LogEntry(LogType logType, string message)
{
LogType = logType;
Message = message;
Timestamp = DateTime.Now;
}
public LogEntry(LogType logType, string message, string? stackTrace = null)
{
LogType = logType;
Message = message;
Timestamp = DateTime.Now;
StackTrace = stackTrace;
}
public LogEntry(LogType logType, string message, DateTime timestamp, string? stackTrace = null)
{
LogType = logType;
Message = message;
Timestamp = timestamp;
StackTrace = stackTrace;
}

public override string ToString() => ToString(includeStackTrace: false);

public string ToString(bool includeStackTrace)
{
if (includeStackTrace && !string.IsNullOrEmpty(stackTrace))
return $"{timestamp:yyyy-MM-dd HH:mm:ss.fff} [{logTypeString}] {message}\nStack Trace:\n{stackTrace}";
else
return $"{timestamp:yyyy-MM-dd HH:mm:ss.fff} [{logTypeString}] {message}";
return includeStackTrace && !string.IsNullOrEmpty(StackTrace)
? $"{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{LogType}] {Message}\nStack Trace:\n{StackTrace}"
: $"{Timestamp:yyyy-MM-dd HH:mm:ss.fff} [{LogType}] {Message}";
}
}
}
Expand Down
63 changes: 52 additions & 11 deletions Unity-MCP-Plugin/Assets/root/Runtime/Unity/Logs/LogUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

#nullable enable
using System.Collections.Concurrent;
using System.Threading.Tasks;
using com.IvanMurzak.ReflectorNet.Utils;
using UnityEngine;

Expand All @@ -19,9 +20,17 @@ public static class LogUtils
{
public const int MaxLogEntries = 5000; // Default max entries to keep in memory

static readonly ConcurrentQueue<LogEntry> _logEntries = new();
static ConcurrentQueue<LogEntry> _logEntries = new();
static readonly LogCache _logCache = new();
static readonly object _lockObject = new();
static bool _isSubscribed = false;
static volatile bool _isSubscribed = false;

static LogUtils()
{
if (!MainThread.Instance.IsMainThread)
throw new System.Exception($"{nameof(LogUtils)} must be initialized on the main thread.");
EnsureSubscribed();
}

public static int LogEntries
{
Expand All @@ -38,31 +47,59 @@ public static void ClearLogs()
{
lock (_lockObject)
{
_logEntries.Clear();
_logEntries = new ConcurrentQueue<LogEntry>();
}
}
public static LogEntry[] GetAllLogs()

/// <summary>
/// Asynchronously saves all current log entries to the cache file.
/// </summary>
/// <returns>A task that completes when the save operation is finished.</returns>
public static Task SaveToFile()
{
var logEntries = GetAllLogs();
return _logCache.CacheLogEntriesAsync(logEntries);
}

/// <summary>
/// Asynchronously loads log entries from the cache file and replaces the current log entries.
/// </summary>
/// <returns>A task that completes when the load operation is finished.</returns>
public static async Task LoadFromFile()
{
var logWrapper = await _logCache.GetCachedLogEntriesAsync();
lock (_lockObject)
{
return _logEntries.ToArray();
_logEntries = new ConcurrentQueue<LogEntry>(logWrapper?.Entries ?? new LogEntry[0]);
}
}

static LogUtils()
/// <summary>
/// Asynchronously handles application quit by saving log entries to file and cleaning up resources.
/// </summary>
/// <returns>A task that completes when the quit handling is finished.</returns>
public static async Task HandleQuit()
{
EnsureSubscribed();
await SaveToFile();
await _logCache.HandleQuit();
}

public static void EnsureSubscribed()
public static LogEntry[] GetAllLogs()
{
MainThread.Instance.RunAsync(() =>
lock (_lockObject)
{
return _logEntries.ToArray();
}
}

public static Task EnsureSubscribed()
{
return MainThread.Instance.RunAsync(() =>
{
lock (_lockObject)
{
if (!_isSubscribed)
{
Application.logMessageReceived += OnLogMessageReceived;
Application.logMessageReceivedThreaded += OnLogMessageReceived;
_isSubscribed = true;
}
Expand All @@ -74,7 +111,11 @@ static void OnLogMessageReceived(string message, string stackTrace, LogType type
{
try
{
var logEntry = new LogEntry(message, stackTrace, type);
var logEntry = new LogEntry(
message: message,
stackTrace: stackTrace,
logType: type);

lock (_lockObject)
{
_logEntries.Enqueue(logEntry);
Expand Down
Loading
Loading