|
3 | 3 | using System.IO; |
4 | 4 | using System.Linq; |
5 | 5 | using System.Reflection; |
| 6 | +using System.Text.RegularExpressions; |
6 | 7 | using Newtonsoft.Json.Linq; |
7 | 8 | using UnityEditor; |
8 | 9 | using UnityEngine; |
@@ -362,6 +363,198 @@ public int GetLogCount() |
362 | 363 | return error + warning + log; |
363 | 364 | } |
364 | 365 |
|
| 366 | + /// <summary> |
| 367 | + /// Search logs with keyword or regex pattern |
| 368 | + /// </summary> |
| 369 | + public JObject SearchLogsAsJson(string keyword = null, string regex = null, string logType = null, |
| 370 | + bool includeStackTrace = true, bool caseSensitive = false, int offset = 0, int limit = 50) |
| 371 | + { |
| 372 | + // Prepare search criteria |
| 373 | + bool hasSearchCriteria = !string.IsNullOrEmpty(keyword) || !string.IsNullOrEmpty(regex); |
| 374 | + Regex searchRegex = null; |
| 375 | + string searchKeyword = keyword; |
| 376 | + |
| 377 | + // If regex is provided, use it instead of keyword |
| 378 | + if (!string.IsNullOrEmpty(regex)) |
| 379 | + { |
| 380 | + try |
| 381 | + { |
| 382 | + searchRegex = new Regex(regex, caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase); |
| 383 | + } |
| 384 | + catch (ArgumentException ex) |
| 385 | + { |
| 386 | + return new JObject |
| 387 | + { |
| 388 | + ["logs"] = new JArray(), |
| 389 | + ["error"] = $"Invalid regex pattern: {ex.Message}", |
| 390 | + ["success"] = false |
| 391 | + }; |
| 392 | + } |
| 393 | + } |
| 394 | + else if (!string.IsNullOrEmpty(keyword) && !caseSensitive) |
| 395 | + { |
| 396 | + searchKeyword = keyword.ToLower(); |
| 397 | + } |
| 398 | + |
| 399 | + // Map MCP log types to Unity log types |
| 400 | + HashSet<string> unityLogTypes = null; |
| 401 | + if (!string.IsNullOrEmpty(logType)) |
| 402 | + { |
| 403 | + if (LogTypeMapping.TryGetValue(logType, out var mapped)) |
| 404 | + { |
| 405 | + unityLogTypes = mapped; |
| 406 | + } |
| 407 | + else |
| 408 | + { |
| 409 | + unityLogTypes = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { logType }; |
| 410 | + } |
| 411 | + } |
| 412 | + |
| 413 | + JArray logsArray = new JArray(); |
| 414 | + int totalCount = 0; |
| 415 | + int filteredCount = 0; |
| 416 | + int matchedCount = 0; |
| 417 | + int currentIndex = 0; |
| 418 | + |
| 419 | + // Get total count using reflection |
| 420 | + try |
| 421 | + { |
| 422 | + totalCount = (int)_getCountMethod.Invoke(null, null); |
| 423 | + } |
| 424 | + catch (Exception ex) |
| 425 | + { |
| 426 | + Debug.LogError($"[MCP Unity] Error getting log count: {ex.Message}"); |
| 427 | + return new JObject |
| 428 | + { |
| 429 | + ["logs"] = logsArray, |
| 430 | + ["error"] = "Failed to access Unity console logs", |
| 431 | + ["success"] = false |
| 432 | + }; |
| 433 | + } |
| 434 | + |
| 435 | + if (totalCount == 0) |
| 436 | + { |
| 437 | + return new JObject |
| 438 | + { |
| 439 | + ["logs"] = logsArray, |
| 440 | + ["_totalCount"] = 0, |
| 441 | + ["_filteredCount"] = 0, |
| 442 | + ["_matchedCount"] = 0, |
| 443 | + ["_returnedCount"] = 0, |
| 444 | + ["success"] = true |
| 445 | + }; |
| 446 | + } |
| 447 | + |
| 448 | + try |
| 449 | + { |
| 450 | + // Start getting entries |
| 451 | + _startGettingEntriesMethod?.Invoke(null, null); |
| 452 | + |
| 453 | + // Search through logs (newest first) |
| 454 | + for (int i = totalCount - 1; i >= 0; i--) |
| 455 | + { |
| 456 | + // Create LogEntry instance |
| 457 | + var logEntry = Activator.CreateInstance(_logEntryType); |
| 458 | + |
| 459 | + // GetEntryInternal(int row, LogEntry outputEntry) |
| 460 | + bool success = (bool)_getEntryInternalMethod.Invoke(null, new object[] { i, logEntry }); |
| 461 | + |
| 462 | + if (!success) continue; |
| 463 | + |
| 464 | + // Extract fields |
| 465 | + string fullMessage = _messageField?.GetValue(logEntry) as string ?? ""; |
| 466 | + string file = _fileField?.GetValue(logEntry) as string ?? ""; |
| 467 | + int line = _lineField?.GetValue(logEntry) as int? ?? 0; |
| 468 | + int mode = _modeField?.GetValue(logEntry) as int? ?? 0; |
| 469 | + int callstackStartUTF8 = _callstackTextStartUTF8Field?.GetValue(logEntry) as int? ?? 0; |
| 470 | + int callstackStartUTF16 = _callstackTextStartUTF16Field?.GetValue(logEntry) as int? ?? 0; |
| 471 | + |
| 472 | + // Parse message and stack trace |
| 473 | + var (actualMessage, stackTrace) = ParseMessageAndStackTrace(fullMessage, callstackStartUTF16, callstackStartUTF8); |
| 474 | + |
| 475 | + // Determine log type |
| 476 | + string entryLogType = DetermineLogTypeFromModeAndContent(mode, stackTrace); |
| 477 | + |
| 478 | + // Skip if filtering by log type and entry doesn't match |
| 479 | + if (unityLogTypes != null && !unityLogTypes.Contains(entryLogType)) |
| 480 | + continue; |
| 481 | + |
| 482 | + filteredCount++; |
| 483 | + |
| 484 | + // Check if entry matches search criteria |
| 485 | + bool matches = true; |
| 486 | + if (hasSearchCriteria) |
| 487 | + { |
| 488 | + matches = false; |
| 489 | + |
| 490 | + // Search in message |
| 491 | + if (searchRegex != null) |
| 492 | + { |
| 493 | + matches = searchRegex.IsMatch(actualMessage); |
| 494 | + if (!matches && includeStackTrace && !string.IsNullOrEmpty(stackTrace)) |
| 495 | + { |
| 496 | + matches = searchRegex.IsMatch(stackTrace); |
| 497 | + } |
| 498 | + } |
| 499 | + else if (!string.IsNullOrEmpty(searchKeyword)) |
| 500 | + { |
| 501 | + string messageToSearch = caseSensitive ? actualMessage : actualMessage.ToLower(); |
| 502 | + matches = messageToSearch.Contains(searchKeyword); |
| 503 | + |
| 504 | + if (!matches && includeStackTrace && !string.IsNullOrEmpty(stackTrace)) |
| 505 | + { |
| 506 | + string stackTraceToSearch = caseSensitive ? stackTrace : stackTrace.ToLower(); |
| 507 | + matches = stackTraceToSearch.Contains(searchKeyword); |
| 508 | + } |
| 509 | + } |
| 510 | + } |
| 511 | + |
| 512 | + if (!matches) continue; |
| 513 | + |
| 514 | + matchedCount++; |
| 515 | + |
| 516 | + // Check if we're in the offset range and haven't reached the limit yet |
| 517 | + if (currentIndex >= offset && logsArray.Count < limit) |
| 518 | + { |
| 519 | + var logObject = new JObject |
| 520 | + { |
| 521 | + ["message"] = actualMessage, |
| 522 | + ["type"] = entryLogType, |
| 523 | + ["timestamp"] = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") |
| 524 | + }; |
| 525 | + |
| 526 | + // Only include stack trace if requested |
| 527 | + if (includeStackTrace) |
| 528 | + { |
| 529 | + logObject["stackTrace"] = stackTrace; |
| 530 | + } |
| 531 | + |
| 532 | + logsArray.Add(logObject); |
| 533 | + } |
| 534 | + |
| 535 | + currentIndex++; |
| 536 | + |
| 537 | + // Early exit if we've collected enough logs |
| 538 | + if (currentIndex >= offset + limit) break; |
| 539 | + } |
| 540 | + } |
| 541 | + finally |
| 542 | + { |
| 543 | + // End getting entries |
| 544 | + _endGettingEntriesMethod?.Invoke(null, null); |
| 545 | + } |
| 546 | + |
| 547 | + return new JObject |
| 548 | + { |
| 549 | + ["logs"] = logsArray, |
| 550 | + ["_totalCount"] = totalCount, |
| 551 | + ["_filteredCount"] = filteredCount, |
| 552 | + ["_matchedCount"] = matchedCount, |
| 553 | + ["_returnedCount"] = logsArray.Count, |
| 554 | + ["success"] = true |
| 555 | + }; |
| 556 | + } |
| 557 | + |
365 | 558 | #if MCP_UNITY_DEBUG_MODE_VALUES |
366 | 559 | /// <summary> |
367 | 560 | /// Debug method to write mode values to file for analysis |
|
0 commit comments