@mflrevan/ucp 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bridge/com.ucp.bridge/Editor/Bridge/BridgeServer.cs +7 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetController.cs +406 -35
- package/bridge/com.ucp.bridge/Editor/Controllers/AssetImportSupport.cs +41 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/CompilationController.cs +88 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/ImporterController.cs +4 -1
- package/bridge/com.ucp.bridge/Editor/Controllers/LogsController.cs +425 -85
- package/bridge/com.ucp.bridge/Editor/Controllers/PlayModeController.cs +13 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SceneController.cs +9 -3
- package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs +151 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/ShaderController.cs.meta +2 -0
- package/bridge/com.ucp.bridge/Editor/Controllers/SnapshotController.cs +303 -8
- package/bridge/com.ucp.bridge/Editor/Controllers/TestRunnerController.cs +80 -4
- package/bridge/com.ucp.bridge/Tests/Editor/ControllerSmokeTests.cs +281 -0
- package/bridge/com.ucp.bridge/package.json +1 -1
- package/package.json +2 -2
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
using System;
|
|
2
2
|
using System.Collections.Generic;
|
|
3
|
+
using System.IO;
|
|
3
4
|
using System.Linq;
|
|
5
|
+
using System.Reflection;
|
|
4
6
|
using System.Text.RegularExpressions;
|
|
7
|
+
using UnityEditor;
|
|
5
8
|
using UnityEngine;
|
|
6
9
|
|
|
7
10
|
namespace UCP.Bridge
|
|
@@ -14,7 +17,10 @@ namespace UCP.Bridge
|
|
|
14
17
|
|
|
15
18
|
private static readonly object s_historyLock = new object();
|
|
16
19
|
private static readonly List<LogRecord> s_history = new List<LogRecord>();
|
|
20
|
+
private static Func<List<ConsoleBackfillEntry>> s_consoleBackfillProvider = CaptureRecentConsoleEntries;
|
|
17
21
|
private static long s_nextId = 1;
|
|
22
|
+
private static StreamWriter s_captureWriter;
|
|
23
|
+
private static string s_capturePath;
|
|
18
24
|
|
|
19
25
|
public static void Register(CommandRouter router)
|
|
20
26
|
{
|
|
@@ -24,6 +30,8 @@ namespace UCP.Bridge
|
|
|
24
30
|
router.Register("logs/search", HandleSearch);
|
|
25
31
|
router.Register("logs/get", HandleGet);
|
|
26
32
|
router.Register("logs/status", HandleStatus);
|
|
33
|
+
router.Register("logs/capture/start", HandleCaptureStart);
|
|
34
|
+
router.Register("logs/capture/stop", HandleCaptureStop);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
public static Dictionary<string, object> RecordLog(string message, string stackTrace, LogType type)
|
|
@@ -37,6 +45,7 @@ namespace UCP.Bridge
|
|
|
37
45
|
{
|
|
38
46
|
s_history.Clear();
|
|
39
47
|
s_nextId = 1;
|
|
48
|
+
s_consoleBackfillProvider = CaptureRecentConsoleEntries;
|
|
40
49
|
}
|
|
41
50
|
}
|
|
42
51
|
|
|
@@ -45,6 +54,84 @@ namespace UCP.Bridge
|
|
|
45
54
|
return RecordLog(level, message, stackTrace);
|
|
46
55
|
}
|
|
47
56
|
|
|
57
|
+
public static void SetConsoleBackfillProviderForTests(Func<List<ConsoleBackfillEntry>> provider)
|
|
58
|
+
{
|
|
59
|
+
lock (s_historyLock)
|
|
60
|
+
{
|
|
61
|
+
s_consoleBackfillProvider = provider ?? CaptureRecentConsoleEntries;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public static void SeedHistoryFromConsole()
|
|
66
|
+
{
|
|
67
|
+
lock (s_historyLock)
|
|
68
|
+
{
|
|
69
|
+
SeedHistoryFromConsoleLocked();
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public static Dictionary<string, object> StartFileCapture(string path)
|
|
74
|
+
{
|
|
75
|
+
lock (s_historyLock)
|
|
76
|
+
{
|
|
77
|
+
StopFileCaptureLocked();
|
|
78
|
+
|
|
79
|
+
var resolvedPath = ResolveCapturePath(path);
|
|
80
|
+
var directory = Path.GetDirectoryName(resolvedPath);
|
|
81
|
+
if (!string.IsNullOrEmpty(directory))
|
|
82
|
+
Directory.CreateDirectory(directory);
|
|
83
|
+
|
|
84
|
+
s_captureWriter = new StreamWriter(resolvedPath, false);
|
|
85
|
+
s_captureWriter.AutoFlush = true;
|
|
86
|
+
s_capturePath = resolvedPath;
|
|
87
|
+
s_captureWriter.WriteLine($"# UCP play-mode log capture started {DateTime.UtcNow:o}");
|
|
88
|
+
|
|
89
|
+
return new Dictionary<string, object>
|
|
90
|
+
{
|
|
91
|
+
["status"] = "ok",
|
|
92
|
+
["path"] = resolvedPath
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public static Dictionary<string, object> StopFileCapture()
|
|
98
|
+
{
|
|
99
|
+
lock (s_historyLock)
|
|
100
|
+
{
|
|
101
|
+
var path = s_capturePath;
|
|
102
|
+
StopFileCaptureLocked();
|
|
103
|
+
return new Dictionary<string, object>
|
|
104
|
+
{
|
|
105
|
+
["status"] = "ok",
|
|
106
|
+
["path"] = path ?? string.Empty,
|
|
107
|
+
["active"] = false
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
public static long GetLatestId()
|
|
113
|
+
{
|
|
114
|
+
SeedHistoryFromConsole();
|
|
115
|
+
lock (s_historyLock)
|
|
116
|
+
{
|
|
117
|
+
return s_history.Count == 0 ? 0 : s_history[s_history.Count - 1].Id;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public static Dictionary<string, object> BuildStatusSummary(long afterId = 0)
|
|
122
|
+
{
|
|
123
|
+
SeedHistoryFromConsole();
|
|
124
|
+
lock (s_historyLock)
|
|
125
|
+
{
|
|
126
|
+
var ordered = s_history
|
|
127
|
+
.Where(entry => entry.Id > afterId)
|
|
128
|
+
.OrderBy(entry => entry.Id)
|
|
129
|
+
.ToList();
|
|
130
|
+
|
|
131
|
+
return BuildStatusResult(ordered, afterId);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
48
135
|
private static object HandleTail(string paramsJson)
|
|
49
136
|
{
|
|
50
137
|
var query = ParseQuery(paramsJson, includePattern: false);
|
|
@@ -65,6 +152,7 @@ namespace UCP.Bridge
|
|
|
65
152
|
|
|
66
153
|
long id = Convert.ToInt64(idObj);
|
|
67
154
|
|
|
155
|
+
SeedHistoryFromConsole();
|
|
68
156
|
lock (s_historyLock)
|
|
69
157
|
{
|
|
70
158
|
var entry = s_history.FirstOrDefault(record => record.Id == id);
|
|
@@ -77,106 +165,82 @@ namespace UCP.Bridge
|
|
|
77
165
|
|
|
78
166
|
private static object HandleStatus(string paramsJson)
|
|
79
167
|
{
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
{
|
|
85
|
-
["info"] = ordered.Count(entry => entry.Level == "info"),
|
|
86
|
-
["warning"] = ordered.Count(entry => entry.Level == "warning"),
|
|
87
|
-
["error"] = ordered.Count(entry => entry.Level == "error"),
|
|
88
|
-
["exception"] = ordered.Count(entry => entry.Level == "exception")
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
var grouped = ordered
|
|
92
|
-
.GroupBy(entry => $"{entry.Level}|{Fingerprint(entry.Message)}")
|
|
93
|
-
.Select(group =>
|
|
94
|
-
{
|
|
95
|
-
var first = group.First();
|
|
96
|
-
var last = group.Last();
|
|
97
|
-
return new Dictionary<string, object>
|
|
98
|
-
{
|
|
99
|
-
["level"] = first.Level,
|
|
100
|
-
["fingerprint"] = Fingerprint(first.Message),
|
|
101
|
-
["sampleMessage"] = Preview(first.Message, MaxPreviewLength),
|
|
102
|
-
["count"] = group.Count(),
|
|
103
|
-
["firstTimestamp"] = first.Timestamp,
|
|
104
|
-
["lastTimestamp"] = last.Timestamp,
|
|
105
|
-
["latestId"] = last.Id
|
|
106
|
-
};
|
|
107
|
-
})
|
|
108
|
-
.OrderByDescending(entry => Convert.ToInt32(entry["count"]))
|
|
109
|
-
.ThenBy(entry => entry["sampleMessage"].ToString())
|
|
110
|
-
.ToList();
|
|
111
|
-
|
|
112
|
-
var result = new Dictionary<string, object>
|
|
113
|
-
{
|
|
114
|
-
["total"] = ordered.Count,
|
|
115
|
-
["byLevel"] = byLevel,
|
|
116
|
-
["uniqueCount"] = grouped.Count,
|
|
117
|
-
["topCategories"] = grouped.Take(8).Cast<object>().ToList()
|
|
118
|
-
};
|
|
168
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
169
|
+
long afterId = 0;
|
|
170
|
+
if (p != null && p.TryGetValue("afterId", out var afterIdObj) && afterIdObj != null)
|
|
171
|
+
afterId = Math.Max(0, Convert.ToInt64(afterIdObj));
|
|
119
172
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
var first = ordered.First();
|
|
123
|
-
var last = ordered.Last();
|
|
124
|
-
result["firstTimestamp"] = first.Timestamp;
|
|
125
|
-
result["lastTimestamp"] = last.Timestamp;
|
|
126
|
-
result["historyWindowSeconds"] = Math.Max(0d, (last.TimestampUtc - first.TimestampUtc).TotalSeconds);
|
|
127
|
-
result["latestId"] = last.Id;
|
|
128
|
-
}
|
|
173
|
+
return BuildStatusSummary(afterId);
|
|
174
|
+
}
|
|
129
175
|
|
|
130
|
-
|
|
131
|
-
|
|
176
|
+
private static object HandleCaptureStart(string paramsJson)
|
|
177
|
+
{
|
|
178
|
+
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
179
|
+
if (p == null || !p.TryGetValue("path", out var pathObj) || pathObj == null)
|
|
180
|
+
throw new ArgumentException("Missing 'path' parameter");
|
|
132
181
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
var sessionEnd = playSession.Playing
|
|
136
|
-
? DateTime.UtcNow
|
|
137
|
-
: (playSession.LastExitedPlayAtUtc ?? DateTime.UtcNow);
|
|
138
|
-
var sessionLogs = ordered
|
|
139
|
-
.Where(entry => entry.TimestampUtc >= playSession.LastEnteredPlayAtUtc.Value
|
|
140
|
-
&& entry.TimestampUtc <= sessionEnd)
|
|
141
|
-
.ToList();
|
|
142
|
-
|
|
143
|
-
result["lastPlayWindow"] = new Dictionary<string, object>
|
|
144
|
-
{
|
|
145
|
-
["startedAt"] = playSession.LastEnteredPlayAtUtc.Value.ToString("o"),
|
|
146
|
-
["endedAt"] = sessionEnd.ToString("o"),
|
|
147
|
-
["durationSeconds"] = Math.Max(0d, (sessionEnd - playSession.LastEnteredPlayAtUtc.Value).TotalSeconds),
|
|
148
|
-
["total"] = sessionLogs.Count,
|
|
149
|
-
["warnings"] = sessionLogs.Count(entry => entry.Level == "warning"),
|
|
150
|
-
["errors"] = sessionLogs.Count(entry => entry.Level == "error" || entry.Level == "exception")
|
|
151
|
-
};
|
|
152
|
-
}
|
|
182
|
+
return StartFileCapture(pathObj.ToString());
|
|
183
|
+
}
|
|
153
184
|
|
|
154
|
-
|
|
155
|
-
|
|
185
|
+
private static object HandleCaptureStop(string paramsJson)
|
|
186
|
+
{
|
|
187
|
+
return StopFileCapture();
|
|
156
188
|
}
|
|
157
189
|
|
|
158
190
|
private static Dictionary<string, object> RecordLog(string level, string message, string stackTrace)
|
|
159
191
|
{
|
|
160
192
|
lock (s_historyLock)
|
|
161
193
|
{
|
|
162
|
-
var entry =
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
};
|
|
170
|
-
entry.Timestamp = entry.TimestampUtc.ToString("o");
|
|
171
|
-
|
|
172
|
-
s_history.Add(entry);
|
|
173
|
-
if (s_history.Count > MaxHistoryEntries)
|
|
174
|
-
s_history.RemoveAt(0);
|
|
194
|
+
var entry = AppendRecordLocked(
|
|
195
|
+
NormalizeLevel(level),
|
|
196
|
+
message ?? string.Empty,
|
|
197
|
+
stackTrace ?? string.Empty,
|
|
198
|
+
DateTime.UtcNow
|
|
199
|
+
);
|
|
200
|
+
WriteCaptureLineLocked(entry);
|
|
175
201
|
|
|
176
202
|
return SerializeFull(entry);
|
|
177
203
|
}
|
|
178
204
|
}
|
|
179
205
|
|
|
206
|
+
private static void WriteCaptureLineLocked(LogRecord entry)
|
|
207
|
+
{
|
|
208
|
+
if (s_captureWriter == null)
|
|
209
|
+
return;
|
|
210
|
+
|
|
211
|
+
var message = (entry.Message ?? string.Empty).Replace("\r", "\\r").Replace("\n", "\\n");
|
|
212
|
+
s_captureWriter.WriteLine($"{entry.Timestamp} [{entry.Level}] {message}");
|
|
213
|
+
if (!string.IsNullOrEmpty(entry.StackTrace))
|
|
214
|
+
{
|
|
215
|
+
var stack = entry.StackTrace.Replace("\r", "\\r").Replace("\n", "\\n");
|
|
216
|
+
s_captureWriter.WriteLine($"{entry.Timestamp} [stack] {stack}");
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private static void StopFileCaptureLocked()
|
|
221
|
+
{
|
|
222
|
+
if (s_captureWriter != null)
|
|
223
|
+
{
|
|
224
|
+
s_captureWriter.WriteLine($"# UCP log capture stopped {DateTime.UtcNow:o}");
|
|
225
|
+
s_captureWriter.Dispose();
|
|
226
|
+
s_captureWriter = null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
s_capturePath = null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private static string ResolveCapturePath(string path)
|
|
233
|
+
{
|
|
234
|
+
if (string.IsNullOrEmpty(path))
|
|
235
|
+
throw new ArgumentException("Log capture path cannot be empty");
|
|
236
|
+
|
|
237
|
+
if (Path.IsPathRooted(path))
|
|
238
|
+
return Path.GetFullPath(path);
|
|
239
|
+
|
|
240
|
+
var projectRoot = Path.GetDirectoryName(Application.dataPath);
|
|
241
|
+
return Path.GetFullPath(Path.Combine(projectRoot, path));
|
|
242
|
+
}
|
|
243
|
+
|
|
180
244
|
private static LogQuery ParseQuery(string paramsJson, bool includePattern)
|
|
181
245
|
{
|
|
182
246
|
var p = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
@@ -186,6 +250,8 @@ namespace UCP.Bridge
|
|
|
186
250
|
{
|
|
187
251
|
if (p.TryGetValue("level", out var levelObj) && levelObj != null)
|
|
188
252
|
query.Level = NormalizeLevel(levelObj.ToString());
|
|
253
|
+
if (p.TryGetValue("channel", out var channelObj) && channelObj != null)
|
|
254
|
+
query.Channel = channelObj.ToString();
|
|
189
255
|
if (p.TryGetValue("count", out var countObj) && countObj != null)
|
|
190
256
|
query.Count = Math.Max(1, Convert.ToInt32(countObj));
|
|
191
257
|
if (p.TryGetValue("beforeId", out var beforeObj) && beforeObj != null)
|
|
@@ -215,6 +281,7 @@ namespace UCP.Bridge
|
|
|
215
281
|
|
|
216
282
|
private static LogQueryResult QueryHistory(LogQuery query)
|
|
217
283
|
{
|
|
284
|
+
SeedHistoryFromConsole();
|
|
218
285
|
lock (s_historyLock)
|
|
219
286
|
{
|
|
220
287
|
IEnumerable<LogRecord> candidates = s_history;
|
|
@@ -225,6 +292,10 @@ namespace UCP.Bridge
|
|
|
225
292
|
candidates = candidates.Where(entry => entry.Id > query.AfterId.Value);
|
|
226
293
|
if (!string.IsNullOrEmpty(query.Level))
|
|
227
294
|
candidates = candidates.Where(entry => PassesLevel(entry.Level, query.Level));
|
|
295
|
+
if (!string.IsNullOrEmpty(query.Channel))
|
|
296
|
+
candidates = candidates.Where(entry =>
|
|
297
|
+
(entry.Message ?? string.Empty).IndexOf(query.Channel, StringComparison.OrdinalIgnoreCase) >= 0
|
|
298
|
+
|| (entry.StackTrace ?? string.Empty).IndexOf(query.Channel, StringComparison.OrdinalIgnoreCase) >= 0);
|
|
228
299
|
|
|
229
300
|
if (query.Regex != null)
|
|
230
301
|
{
|
|
@@ -257,6 +328,83 @@ namespace UCP.Bridge
|
|
|
257
328
|
};
|
|
258
329
|
}
|
|
259
330
|
|
|
331
|
+
private static Dictionary<string, object> BuildStatusResult(List<LogRecord> ordered, long afterId)
|
|
332
|
+
{
|
|
333
|
+
var byLevel = new Dictionary<string, object>
|
|
334
|
+
{
|
|
335
|
+
["info"] = ordered.Count(entry => entry.Level == "info"),
|
|
336
|
+
["warning"] = ordered.Count(entry => entry.Level == "warning"),
|
|
337
|
+
["error"] = ordered.Count(entry => entry.Level == "error"),
|
|
338
|
+
["exception"] = ordered.Count(entry => entry.Level == "exception")
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
var grouped = ordered
|
|
342
|
+
.GroupBy(entry => $"{entry.Level}|{Fingerprint(entry.Message)}")
|
|
343
|
+
.Select(group =>
|
|
344
|
+
{
|
|
345
|
+
var first = group.First();
|
|
346
|
+
var last = group.Last();
|
|
347
|
+
return new Dictionary<string, object>
|
|
348
|
+
{
|
|
349
|
+
["level"] = first.Level,
|
|
350
|
+
["fingerprint"] = Fingerprint(first.Message),
|
|
351
|
+
["sampleMessage"] = Preview(first.Message, MaxPreviewLength),
|
|
352
|
+
["count"] = group.Count(),
|
|
353
|
+
["firstTimestamp"] = first.Timestamp,
|
|
354
|
+
["lastTimestamp"] = last.Timestamp,
|
|
355
|
+
["latestId"] = last.Id
|
|
356
|
+
};
|
|
357
|
+
})
|
|
358
|
+
.OrderByDescending(entry => Convert.ToInt32(entry["count"]))
|
|
359
|
+
.ThenBy(entry => entry["sampleMessage"].ToString())
|
|
360
|
+
.ToList();
|
|
361
|
+
|
|
362
|
+
var result = new Dictionary<string, object>
|
|
363
|
+
{
|
|
364
|
+
["afterId"] = afterId,
|
|
365
|
+
["total"] = ordered.Count,
|
|
366
|
+
["byLevel"] = byLevel,
|
|
367
|
+
["uniqueCount"] = grouped.Count,
|
|
368
|
+
["topCategories"] = grouped.Take(8).Cast<object>().ToList()
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
if (ordered.Count > 0)
|
|
372
|
+
{
|
|
373
|
+
var first = ordered.First();
|
|
374
|
+
var last = ordered.Last();
|
|
375
|
+
result["firstTimestamp"] = first.Timestamp;
|
|
376
|
+
result["lastTimestamp"] = last.Timestamp;
|
|
377
|
+
result["historyWindowSeconds"] = Math.Max(0d, (last.TimestampUtc - first.TimestampUtc).TotalSeconds);
|
|
378
|
+
result["latestId"] = last.Id;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
var playSession = PlayModeController.GetSessionSnapshot();
|
|
382
|
+
result["play"] = SerializePlaySession(playSession);
|
|
383
|
+
|
|
384
|
+
if (playSession.LastEnteredPlayAtUtc.HasValue)
|
|
385
|
+
{
|
|
386
|
+
var sessionEnd = playSession.Playing
|
|
387
|
+
? DateTime.UtcNow
|
|
388
|
+
: (playSession.LastExitedPlayAtUtc ?? DateTime.UtcNow);
|
|
389
|
+
var sessionLogs = ordered
|
|
390
|
+
.Where(entry => entry.TimestampUtc >= playSession.LastEnteredPlayAtUtc.Value
|
|
391
|
+
&& entry.TimestampUtc <= sessionEnd)
|
|
392
|
+
.ToList();
|
|
393
|
+
|
|
394
|
+
result["lastPlayWindow"] = new Dictionary<string, object>
|
|
395
|
+
{
|
|
396
|
+
["startedAt"] = playSession.LastEnteredPlayAtUtc.Value.ToString("o"),
|
|
397
|
+
["endedAt"] = sessionEnd.ToString("o"),
|
|
398
|
+
["durationSeconds"] = Math.Max(0d, (sessionEnd - playSession.LastEnteredPlayAtUtc.Value).TotalSeconds),
|
|
399
|
+
["total"] = sessionLogs.Count,
|
|
400
|
+
["warnings"] = sessionLogs.Count(entry => entry.Level == "warning"),
|
|
401
|
+
["errors"] = sessionLogs.Count(entry => entry.Level == "error" || entry.Level == "exception")
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
|
|
260
408
|
private static Dictionary<string, object> SerializeSummary(LogRecord entry)
|
|
261
409
|
{
|
|
262
410
|
return new Dictionary<string, object>
|
|
@@ -333,6 +481,184 @@ namespace UCP.Bridge
|
|
|
333
481
|
}
|
|
334
482
|
}
|
|
335
483
|
|
|
484
|
+
private static void SeedHistoryFromConsoleLocked()
|
|
485
|
+
{
|
|
486
|
+
if (s_history.Count > 0)
|
|
487
|
+
return;
|
|
488
|
+
|
|
489
|
+
List<ConsoleBackfillEntry> consoleEntries;
|
|
490
|
+
try
|
|
491
|
+
{
|
|
492
|
+
consoleEntries = s_consoleBackfillProvider != null
|
|
493
|
+
? s_consoleBackfillProvider.Invoke()
|
|
494
|
+
: new List<ConsoleBackfillEntry>();
|
|
495
|
+
}
|
|
496
|
+
catch
|
|
497
|
+
{
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (consoleEntries == null || consoleEntries.Count == 0)
|
|
502
|
+
return;
|
|
503
|
+
|
|
504
|
+
var baseTimestamp = DateTime.UtcNow;
|
|
505
|
+
for (var index = 0; index < consoleEntries.Count; index++)
|
|
506
|
+
{
|
|
507
|
+
var entry = consoleEntries[index];
|
|
508
|
+
if (entry == null || string.IsNullOrEmpty(entry.Message) || entry.Message.StartsWith("[UCP]", StringComparison.Ordinal))
|
|
509
|
+
continue;
|
|
510
|
+
|
|
511
|
+
AppendRecordLocked(
|
|
512
|
+
entry.Level,
|
|
513
|
+
entry.Message,
|
|
514
|
+
entry.StackTrace,
|
|
515
|
+
baseTimestamp.AddMilliseconds(index)
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private static LogRecord AppendRecordLocked(string level, string message, string stackTrace, DateTime timestampUtc)
|
|
521
|
+
{
|
|
522
|
+
var entry = new LogRecord
|
|
523
|
+
{
|
|
524
|
+
Id = s_nextId++,
|
|
525
|
+
Level = NormalizeLevel(level),
|
|
526
|
+
Message = message ?? string.Empty,
|
|
527
|
+
StackTrace = stackTrace ?? string.Empty,
|
|
528
|
+
TimestampUtc = timestampUtc
|
|
529
|
+
};
|
|
530
|
+
entry.Timestamp = entry.TimestampUtc.ToString("o");
|
|
531
|
+
|
|
532
|
+
s_history.Add(entry);
|
|
533
|
+
if (s_history.Count > MaxHistoryEntries)
|
|
534
|
+
s_history.RemoveAt(0);
|
|
535
|
+
|
|
536
|
+
return entry;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private static List<ConsoleBackfillEntry> CaptureRecentConsoleEntries()
|
|
540
|
+
{
|
|
541
|
+
try
|
|
542
|
+
{
|
|
543
|
+
var assembly = typeof(Editor).Assembly;
|
|
544
|
+
var logEntriesType = assembly.GetType("UnityEditor.LogEntries");
|
|
545
|
+
var logEntryType = assembly.GetType("UnityEditor.LogEntry");
|
|
546
|
+
if (logEntriesType == null || logEntryType == null)
|
|
547
|
+
return new List<ConsoleBackfillEntry>();
|
|
548
|
+
|
|
549
|
+
var getCount = logEntriesType.GetMethod("GetCount", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static);
|
|
550
|
+
var getEntry = logEntriesType
|
|
551
|
+
.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static)
|
|
552
|
+
.FirstOrDefault(method => method.Name == "GetEntryInternal");
|
|
553
|
+
var messageField = logEntryType.GetField("message", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
|
554
|
+
var modeField = logEntryType.GetField("mode", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
|
555
|
+
if (getCount == null || getEntry == null || messageField == null || modeField == null)
|
|
556
|
+
return new List<ConsoleBackfillEntry>();
|
|
557
|
+
|
|
558
|
+
var count = Convert.ToInt32(getCount.Invoke(null, null));
|
|
559
|
+
if (count <= 0)
|
|
560
|
+
return new List<ConsoleBackfillEntry>();
|
|
561
|
+
|
|
562
|
+
var captured = new List<ConsoleBackfillEntry>();
|
|
563
|
+
var start = Math.Max(0, count - MaxHistoryEntries);
|
|
564
|
+
for (var index = start; index < count; index++)
|
|
565
|
+
{
|
|
566
|
+
var args = new object[] { index, Activator.CreateInstance(logEntryType) };
|
|
567
|
+
if (!Convert.ToBoolean(getEntry.Invoke(null, args)))
|
|
568
|
+
continue;
|
|
569
|
+
|
|
570
|
+
var entry = CreateConsoleBackfillEntry(
|
|
571
|
+
messageField.GetValue(args[1]) as string,
|
|
572
|
+
Convert.ToInt32(modeField.GetValue(args[1]))
|
|
573
|
+
);
|
|
574
|
+
if (entry != null)
|
|
575
|
+
captured.Add(entry);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return captured;
|
|
579
|
+
}
|
|
580
|
+
catch
|
|
581
|
+
{
|
|
582
|
+
return new List<ConsoleBackfillEntry>();
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private static ConsoleBackfillEntry CreateConsoleBackfillEntry(string rawMessage, int mode)
|
|
587
|
+
{
|
|
588
|
+
var normalized = (rawMessage ?? string.Empty).Replace("\r\n", "\n").TrimEnd();
|
|
589
|
+
if (string.IsNullOrEmpty(normalized) || normalized.StartsWith("[UCP]", StringComparison.Ordinal))
|
|
590
|
+
return null;
|
|
591
|
+
|
|
592
|
+
var split = SplitConsoleMessage(normalized);
|
|
593
|
+
return new ConsoleBackfillEntry
|
|
594
|
+
{
|
|
595
|
+
Level = InferConsoleLevel(mode, split.Message, split.StackTrace),
|
|
596
|
+
Message = split.Message,
|
|
597
|
+
StackTrace = split.StackTrace
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private static SplitLogMessage SplitConsoleMessage(string rawMessage)
|
|
602
|
+
{
|
|
603
|
+
var firstNewline = rawMessage.IndexOf('\n');
|
|
604
|
+
if (firstNewline < 0)
|
|
605
|
+
{
|
|
606
|
+
return new SplitLogMessage
|
|
607
|
+
{
|
|
608
|
+
Message = rawMessage,
|
|
609
|
+
StackTrace = string.Empty
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
var firstLine = rawMessage.Substring(0, firstNewline).TrimEnd();
|
|
614
|
+
var remainder = rawMessage.Substring(firstNewline + 1).Trim();
|
|
615
|
+
if (LooksLikeStackTrace(remainder))
|
|
616
|
+
{
|
|
617
|
+
return new SplitLogMessage
|
|
618
|
+
{
|
|
619
|
+
Message = firstLine,
|
|
620
|
+
StackTrace = remainder
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return new SplitLogMessage
|
|
625
|
+
{
|
|
626
|
+
Message = rawMessage,
|
|
627
|
+
StackTrace = string.Empty
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private static bool LooksLikeStackTrace(string value)
|
|
632
|
+
{
|
|
633
|
+
if (string.IsNullOrEmpty(value))
|
|
634
|
+
return false;
|
|
635
|
+
|
|
636
|
+
return value.StartsWith("UnityEngine.", StringComparison.Ordinal)
|
|
637
|
+
|| value.StartsWith("UnityEditor.", StringComparison.Ordinal)
|
|
638
|
+
|| value.StartsWith("System.", StringComparison.Ordinal)
|
|
639
|
+
|| value.StartsWith("---", StringComparison.Ordinal)
|
|
640
|
+
|| value.Contains(" (at ")
|
|
641
|
+
|| value.IndexOf(":Log", StringComparison.Ordinal) >= 0
|
|
642
|
+
|| value.IndexOf(":LogWarning", StringComparison.Ordinal) >= 0
|
|
643
|
+
|| value.IndexOf(":LogError", StringComparison.Ordinal) >= 0
|
|
644
|
+
|| value.IndexOf(":LogException", StringComparison.Ordinal) >= 0;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private static string InferConsoleLevel(int mode, string message, string stackTrace)
|
|
648
|
+
{
|
|
649
|
+
var combined = (message ?? string.Empty) + "\n" + (stackTrace ?? string.Empty);
|
|
650
|
+
if ((mode & 0x100) != 0 || combined.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0)
|
|
651
|
+
return combined.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0 ? "exception" : "error";
|
|
652
|
+
if ((mode & 0x200) != 0)
|
|
653
|
+
return "warning";
|
|
654
|
+
if (Regex.IsMatch(combined, @"\bwarning\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
|
|
655
|
+
return "warning";
|
|
656
|
+
if (Regex.IsMatch(combined, @"\berror\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant))
|
|
657
|
+
return combined.IndexOf("Exception", StringComparison.OrdinalIgnoreCase) >= 0 ? "exception" : "error";
|
|
658
|
+
|
|
659
|
+
return "info";
|
|
660
|
+
}
|
|
661
|
+
|
|
336
662
|
private static string Preview(string value, int maxChars)
|
|
337
663
|
{
|
|
338
664
|
if (string.IsNullOrEmpty(value) || value.Length <= maxChars)
|
|
@@ -389,9 +715,23 @@ namespace UCP.Bridge
|
|
|
389
715
|
public DateTime TimestampUtc;
|
|
390
716
|
}
|
|
391
717
|
|
|
718
|
+
public sealed class ConsoleBackfillEntry
|
|
719
|
+
{
|
|
720
|
+
public string Level;
|
|
721
|
+
public string Message;
|
|
722
|
+
public string StackTrace;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
private sealed class SplitLogMessage
|
|
726
|
+
{
|
|
727
|
+
public string Message;
|
|
728
|
+
public string StackTrace;
|
|
729
|
+
}
|
|
730
|
+
|
|
392
731
|
private sealed class LogQuery
|
|
393
732
|
{
|
|
394
733
|
public string Level;
|
|
734
|
+
public string Channel;
|
|
395
735
|
public string Pattern;
|
|
396
736
|
public Regex Regex;
|
|
397
737
|
public int Count;
|
|
@@ -64,7 +64,10 @@ namespace UCP.Bridge
|
|
|
64
64
|
|
|
65
65
|
var saveDirtyScenes = GetBoolParam(paramsJson, "saveDirtyScenes", true);
|
|
66
66
|
var discardUntitled = GetBoolParam(paramsJson, "discardUntitled", true);
|
|
67
|
+
var logFile = GetStringParam(paramsJson, "logFile");
|
|
67
68
|
SaveDirtyScenesIfRequested(saveDirtyScenes, discardUntitled);
|
|
69
|
+
if (!string.IsNullOrEmpty(logFile))
|
|
70
|
+
LogsController.StartFileCapture(logFile);
|
|
68
71
|
|
|
69
72
|
lock (s_sessionLock)
|
|
70
73
|
{
|
|
@@ -151,11 +154,21 @@ namespace UCP.Bridge
|
|
|
151
154
|
break;
|
|
152
155
|
case PlayModeStateChange.EnteredEditMode:
|
|
153
156
|
s_lastExitedPlayAtUtc = DateTime.UtcNow;
|
|
157
|
+
LogsController.StopFileCapture();
|
|
154
158
|
break;
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
162
|
|
|
163
|
+
private static string GetStringParam(string paramsJson, string key)
|
|
164
|
+
{
|
|
165
|
+
var parameters = MiniJson.Deserialize(paramsJson) as Dictionary<string, object>;
|
|
166
|
+
if (parameters != null && parameters.TryGetValue(key, out var valueObj) && valueObj != null)
|
|
167
|
+
return valueObj.ToString();
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
159
172
|
private static object SerializeSessionSnapshot(SessionSnapshot snapshot)
|
|
160
173
|
{
|
|
161
174
|
var now = DateTime.UtcNow;
|
|
@@ -44,20 +44,26 @@ namespace UCP.Bridge
|
|
|
44
44
|
throw new System.ArgumentException("Missing 'path' parameter");
|
|
45
45
|
|
|
46
46
|
var path = pathObj.ToString();
|
|
47
|
+
var additive = GetBoolParam(p, "additive", false);
|
|
47
48
|
var saveDirtyScenes = GetBoolParam(p, "saveDirtyScenes", true);
|
|
48
49
|
var discardUntitled = GetBoolParam(p, "discardUntitled", true);
|
|
49
50
|
|
|
50
51
|
if (EditorApplication.isPlaying)
|
|
51
52
|
{
|
|
52
|
-
SceneManager.LoadScene(path);
|
|
53
|
+
SceneManager.LoadScene(path, additive ? LoadSceneMode.Additive : LoadSceneMode.Single);
|
|
53
54
|
}
|
|
54
55
|
else
|
|
55
56
|
{
|
|
56
57
|
SaveDirtyScenesIfRequested(saveDirtyScenes, discardUntitled);
|
|
57
|
-
EditorSceneManager.OpenScene(path, OpenSceneMode.Single);
|
|
58
|
+
EditorSceneManager.OpenScene(path, additive ? OpenSceneMode.Additive : OpenSceneMode.Single);
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
return new
|
|
61
|
+
return new Dictionary<string, object>
|
|
62
|
+
{
|
|
63
|
+
["status"] = "ok",
|
|
64
|
+
["loaded"] = path,
|
|
65
|
+
["additive"] = additive
|
|
66
|
+
};
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
private static bool GetBoolParam(Dictionary<string, object> parameters, string key, bool defaultValue)
|