Skip to content

Commit c6facbd

Browse files
Enhance the command preprocessor
1 parent d9dd531 commit c6facbd

22 files changed

+528
-228
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
**AI-Powered Windows Crash Dump Analysis Platform**
44

5-
[![Tests](https://img.shields.io/badge/tests-1,861%20total-brightgreen?style=flat-square)](https://github.com/CapulusCodeNinja/mcp_nexus)
5+
[![Tests](https://img.shields.io/badge/tests-1,853%20total-brightgreen?style=flat-square)](https://github.com/CapulusCodeNinja/mcp_nexus)
66
[![Coverage](https://img.shields.io/badge/coverage-62.62%25-good?style=flat-square)](https://github.com/CapulusCodeNinja/mcp_nexus)
77
[![Build](https://img.shields.io/badge/build-0%20warnings-brightgreen?style=flat-square)](https://github.com/CapulusCodeNinja/mcp_nexus)
88
[![License](https://img.shields.io/badge/license-Apache%202.0-blue?style=flat-square)](LICENSE)
@@ -317,7 +317,7 @@ dotnet test --filter "Notification"
317317

318318
### Test Statistics
319319

320-
-**1,861 total tests** (1,861 passing)
320+
-**1,853 total tests** (1,853 passing)
321321
-**62.62% line coverage** with comprehensive analysis testing
322322
-**0 warnings** in build (clean codebase)
323323
-**17+ test categories** covering all major functionality including Extensions

mcp_nexus/Configuration/ServiceRegistration.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,12 @@ private static void RegisterCoreServices(IServiceCollection services, IConfigura
136136
services.AddSingleton<IMcpNotificationService, McpNotificationService>();
137137
services.AddSingleton<IMcpToolDefinitionService, McpToolDefinitionService>();
138138

139-
logger.Info("Registered core services (CDB, Session, Notifications, Protocol)");
139+
// Register utility services for path handling and command preprocessing
140+
services.AddSingleton<mcp_nexus.Utilities.IWslPathConverter, mcp_nexus.Utilities.WslPathConverter>();
141+
services.AddSingleton<mcp_nexus.Utilities.IPathHandler, mcp_nexus.Utilities.PathHandler>();
142+
services.AddSingleton<mcp_nexus.Utilities.ICommandPreprocessor, mcp_nexus.Utilities.CommandPreprocessor>();
143+
144+
logger.Info("Registered core services (CDB, Session, Notifications, Protocol, Utilities)");
140145
}
141146

142147

mcp_nexus/Debugger/CdbSession.cs

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public class CdbSession : IDisposable, ICdbSession
1313
private readonly CdbProcessManager m_processManager;
1414
private readonly CdbCommandExecutor m_commandExecutor;
1515
private readonly CdbOutputParser m_outputParser;
16+
private readonly mcp_nexus.Utilities.ICommandPreprocessor? m_commandPreprocessor;
1617

1718
// CRITICAL: Semaphore to ensure only ONE command executes at a time in CDB
1819
// CDB is single-threaded and cannot handle concurrent commands
@@ -34,6 +35,7 @@ public class CdbSession : IDisposable, ICdbSession
3435
/// <param name="outputReadingTimeoutMs">The output reading timeout in milliseconds. Default is 300000ms (5 minutes).</param>
3536
/// <param name="enableCommandPreprocessing">Whether to enable command preprocessing (path conversion and directory creation). Default is true.</param>
3637
/// <param name="sessionId">Optional session ID for creating session-specific log files. If null, uses default naming.</param>
38+
/// <param name="commandPreprocessor">Optional command preprocessor for path conversion. If null and preprocessing is enabled, preprocessing will be skipped.</param>
3739
/// <exception cref="ArgumentNullException">Thrown when <paramref name="logger"/> is null.</exception>
3840
public CdbSession(
3941
ILogger<CdbSession> logger,
@@ -45,9 +47,11 @@ public CdbSession(
4547
int startupDelayMs = 1000,
4648
int outputReadingTimeoutMs = 300000,
4749
bool enableCommandPreprocessing = true,
48-
string? sessionId = null)
50+
string? sessionId = null,
51+
mcp_nexus.Utilities.ICommandPreprocessor? commandPreprocessor = null)
4952
{
5053
m_logger = logger ?? throw new ArgumentNullException(nameof(logger));
54+
m_commandPreprocessor = commandPreprocessor;
5155

5256
// Create configuration with validation
5357
m_config = new CdbSessionConfiguration(
@@ -86,14 +90,17 @@ public CdbSession(
8690
/// <param name="logger">The logger instance for recording session operations and errors.</param>
8791
/// <param name="config">The CDB session configuration object.</param>
8892
/// <param name="sessionId">Optional session ID for creating session-specific log files. If null, uses default naming.</param>
93+
/// <param name="commandPreprocessor">Optional command preprocessor for path conversion. If null and preprocessing is enabled, preprocessing will be skipped.</param>
8994
/// <exception cref="ArgumentNullException">Thrown when <paramref name="logger"/> or <paramref name="config"/> is null.</exception>
9095
public CdbSession(
9196
ILogger<CdbSession> logger,
9297
CdbSessionConfiguration config,
93-
string? sessionId = null)
98+
string? sessionId = null,
99+
mcp_nexus.Utilities.ICommandPreprocessor? commandPreprocessor = null)
94100
{
95101
m_logger = logger ?? throw new ArgumentNullException(nameof(logger));
96102
m_config = config ?? throw new ArgumentNullException(nameof(config));
103+
m_commandPreprocessor = commandPreprocessor;
97104

98105
// CRITICAL FIX: Logger casting was failing, causing NullLogger to be used!
99106
// This resulted in zero visibility into CdbProcessManager init consumer
@@ -311,16 +318,20 @@ public async Task<string> ExecuteCommand(string command, string commandId, Cance
311318
m_logger.LogInformation("🔒 SEMAPHORE: About to execute command '{Command}' (TRUE ASYNC)", command);
312319

313320
// Preprocess the command to fix common issues (e.g., .srcpath path conversion and directory creation)
314-
// Only if preprocessing is enabled in configuration
321+
// Only if preprocessing is enabled in configuration and preprocessor is available
315322
var preprocessedCommand = command;
316-
if (m_config.EnableCommandPreprocessing)
323+
if (m_config.EnableCommandPreprocessing && m_commandPreprocessor != null)
317324
{
318-
preprocessedCommand = mcp_nexus.Utilities.CommandPreprocessor.PreprocessCommand(command);
325+
preprocessedCommand = m_commandPreprocessor.PreprocessCommand(command);
319326
if (preprocessedCommand != command)
320327
{
321328
m_logger.LogInformation("🔧 Command preprocessed: '{Original}' -> '{Preprocessed}'", command, preprocessedCommand);
322329
}
323330
}
331+
else if (m_config.EnableCommandPreprocessing && m_commandPreprocessor == null)
332+
{
333+
m_logger.LogWarning("⚠️ Command preprocessing is ENABLED but no preprocessor provided - sending command as-is: '{Command}'", command);
334+
}
324335
else
325336
{
326337
m_logger.LogDebug("⚠️ Command preprocessing is DISABLED - sending command as-is: '{Command}'", command);

mcp_nexus/Session/SessionLifecycleManager.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,24 @@ namespace mcp_nexus.Session
2121
/// <param name="notificationService">The notification service for sending session events.</param>
2222
/// <param name="config">The session manager configuration.</param>
2323
/// <param name="sessions">The thread-safe dictionary for storing session information.</param>
24+
/// <param name="commandPreprocessor">The command preprocessor for path conversions.</param>
2425
/// <exception cref="ArgumentNullException">Thrown when any of the required parameters are null.</exception>
2526
public class SessionLifecycleManager(
2627
ILogger logger,
2728
IServiceProvider serviceProvider,
2829
ILoggerFactory loggerFactory,
2930
IMcpNotificationService notificationService,
3031
SessionManagerConfiguration config,
31-
ConcurrentDictionary<string, SessionInfo> sessions)
32+
ConcurrentDictionary<string, SessionInfo> sessions,
33+
mcp_nexus.Utilities.ICommandPreprocessor commandPreprocessor)
3234
{
3335
private readonly ILogger m_Logger = logger ?? throw new ArgumentNullException(nameof(logger));
3436
private readonly IServiceProvider m_ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
3537
private readonly ILoggerFactory m_LoggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
3638
private readonly IMcpNotificationService m_NotificationService = notificationService ?? throw new ArgumentNullException(nameof(notificationService));
3739
private readonly SessionManagerConfiguration m_Config = config ?? throw new ArgumentNullException(nameof(config));
3840
private readonly ConcurrentDictionary<string, SessionInfo> m_Sessions = sessions ?? throw new ArgumentNullException(nameof(sessions));
41+
private readonly mcp_nexus.Utilities.ICommandPreprocessor m_CommandPreprocessor = commandPreprocessor ?? throw new ArgumentNullException(nameof(commandPreprocessor));
3942
private readonly ConcurrentDictionary<string, SessionCommandResultCache> m_SessionCaches = new ConcurrentDictionary<string, SessionCommandResultCache>();
4043
private readonly IExtensionCommandTracker? m_ExtensionTracker = serviceProvider.GetService<IExtensionCommandTracker>();
4144
private readonly IExtensionExecutor? m_ExtensionExecutor = serviceProvider.GetService<IExtensionExecutor>();
@@ -424,7 +427,8 @@ private ICdbSession CreateCdbSession(ILogger sessionLogger, string sessionId)
424427
m_Config.CdbOptions.StartupDelayMs,
425428
m_Config.CdbOptions.OutputReadingTimeoutMs,
426429
m_Config.CdbOptions.EnableCommandPreprocessing,
427-
sessionId
430+
sessionId,
431+
m_CommandPreprocessor
428432
);
429433
}
430434

mcp_nexus/Session/ThreadSafeSessionManager.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ public ThreadSafeSessionManager(
5252

5353
// Create focused components
5454
m_config = new SessionManagerConfiguration(config, cdbOptions);
55+
var commandPreprocessor = serviceProvider.GetRequiredService<mcp_nexus.Utilities.ICommandPreprocessor>();
5556
m_lifecycleManager = new SessionLifecycleManager(
56-
logger, serviceProvider, loggerFactory, notificationService, m_config, m_sessions);
57+
logger, serviceProvider, loggerFactory, notificationService, m_config, m_sessions, commandPreprocessor);
5758
m_monitoringService = new SessionMonitoringService(
5859
logger, notificationService, m_config, m_sessions, m_lifecycleManager, m_shutdownCts);
5960
m_statisticsCollector = new SessionStatisticsCollector(

mcp_nexus/Utilities/CommandPreprocessor.cs

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,43 @@
33
namespace mcp_nexus.Utilities
44
{
55
/// <summary>
6-
/// Preprocesses WinDBG commands for path conversion and directory creation only.
6+
/// Service for preprocessing WinDBG commands for path conversion and directory creation.
77
/// This class ONLY handles WSL to Windows path conversion and ensures directories exist.
88
/// NO syntax fixing, NO adding quotes, NO adding semicolons.
9+
/// Injectable service that supports proper dependency injection and testing.
910
/// </summary>
10-
public static partial class CommandPreprocessor
11+
public partial class CommandPreprocessor : ICommandPreprocessor
1112
{
12-
private static readonly Regex PathPattern = MyRegex();
13+
private static readonly Regex s_PathPattern = MyRegex();
14+
private readonly IPathHandler m_PathHandler;
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="CommandPreprocessor"/> class.
18+
/// </summary>
19+
/// <param name="pathHandler">The path handler for WSL/Windows path conversions.</param>
20+
public CommandPreprocessor(IPathHandler pathHandler)
21+
{
22+
m_PathHandler = pathHandler ?? throw new ArgumentNullException(nameof(pathHandler));
23+
}
1324

1425
/// <summary>
1526
/// Preprocesses a WinDBG command to convert WSL paths to Windows paths and ensure directories exist.
1627
/// ONLY does path conversion - NO syntax changes.
1728
/// </summary>
1829
/// <param name="command">The original command</param>
1930
/// <returns>The command with paths converted</returns>
20-
public static string PreprocessCommand(string command)
31+
public string PreprocessCommand(string command)
2132
{
2233
if (string.IsNullOrWhiteSpace(command))
2334
{
2435
return command;
2536
}
2637

2738
// Find and convert all WSL paths in the command (generic /mnt/<drive>/...)
28-
var result = PathPattern.Replace(command, match =>
39+
var result = s_PathPattern.Replace(command, match =>
2940
{
3041
var wslPath = match.Value;
31-
var windowsPath = PathHandler.ConvertToWindowsPath(wslPath);
42+
var windowsPath = m_PathHandler.ConvertToWindowsPath(wslPath);
3243

3344
// Ensure directory exists
3445
EnsureDirectoryExists(windowsPath);
@@ -44,7 +55,7 @@ public static string PreprocessCommand(string command)
4455
(Match m) =>
4556
{
4657
var wsl = m.Groups[1].Value.Replace('\\', '/');
47-
var win = PathHandler.ConvertToWindowsPath(wsl);
58+
var win = m_PathHandler.ConvertToWindowsPath(wsl);
4859
return "srv*" + win;
4960
},
5061
RegexOptions.IgnoreCase);
@@ -109,7 +120,7 @@ public static string PreprocessCommand(string command)
109120
/// Ensures that a directory exists, creating it if necessary.
110121
/// </summary>
111122
/// <param name="path">The directory path to ensure exists</param>
112-
private static void EnsureDirectoryExists(string path)
123+
private void EnsureDirectoryExists(string path)
113124
{
114125
if (string.IsNullOrWhiteSpace(path))
115126
return;
@@ -124,14 +135,13 @@ private static void EnsureDirectoryExists(string path)
124135
}
125136

126137
// Check if it's a valid Windows path
127-
if (PathHandler.IsWindowsPath(path))
138+
if (m_PathHandler.IsWindowsPath(path))
128139
{
129140
// Create directory if it doesn't exist
130141
if (!Directory.Exists(path))
131142
{
132143
Directory.CreateDirectory(path);
133-
// Note: We can't use ILogger here since this is a static utility class
134-
// Directory creation will be logged at the command execution level
144+
// Directory creation is logged at the command execution level if needed
135145
}
136146
}
137147
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace mcp_nexus.Utilities
2+
{
3+
/// <summary>
4+
/// Interface for preprocessing WinDBG commands before execution.
5+
/// Handles path conversion and directory creation.
6+
/// </summary>
7+
public interface ICommandPreprocessor
8+
{
9+
/// <summary>
10+
/// Preprocesses a WinDBG command to convert WSL paths to Windows paths and ensure directories exist.
11+
/// ONLY does path conversion - NO syntax changes.
12+
/// </summary>
13+
/// <param name="command">The original command.</param>
14+
/// <returns>The command with paths converted.</returns>
15+
string PreprocessCommand(string command);
16+
}
17+
}
18+
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
namespace mcp_nexus.Utilities
2+
{
3+
/// <summary>
4+
/// Interface for path conversion and handling operations.
5+
/// Abstracts WSL path conversion, Windows path validation, and path normalization.
6+
/// </summary>
7+
public interface IPathHandler
8+
{
9+
/// <summary>
10+
/// Converts a WSL path to a Windows path.
11+
/// </summary>
12+
/// <param name="path">The WSL path to convert.</param>
13+
/// <returns>The converted Windows path.</returns>
14+
string ConvertToWindowsPath(string path);
15+
16+
/// <summary>
17+
/// Converts a Windows path to a WSL path.
18+
/// </summary>
19+
/// <param name="path">The Windows path to convert.</param>
20+
/// <returns>The converted WSL path.</returns>
21+
string ConvertToWslPath(string path);
22+
23+
/// <summary>
24+
/// Normalizes a path for Windows by converting WSL paths if detected.
25+
/// </summary>
26+
/// <param name="path">The path to normalize.</param>
27+
/// <returns>The normalized Windows path.</returns>
28+
string NormalizeForWindows(string path);
29+
30+
/// <summary>
31+
/// Normalizes multiple paths for Windows by converting WSL paths if detected.
32+
/// </summary>
33+
/// <param name="paths">The paths to normalize.</param>
34+
/// <returns>The normalized Windows paths.</returns>
35+
string[] NormalizeForWindows(string[] paths);
36+
37+
/// <summary>
38+
/// Determines if a path is a Windows path.
39+
/// </summary>
40+
/// <param name="path">The path to check.</param>
41+
/// <returns>True if the path is a Windows path; otherwise, false.</returns>
42+
bool IsWindowsPath(string path);
43+
44+
/// <summary>
45+
/// Clears the internal path conversion cache.
46+
/// </summary>
47+
void ClearCache();
48+
}
49+
}
50+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
namespace mcp_nexus.Utilities
2+
{
3+
/// <summary>
4+
/// Interface for WSL path conversion operations.
5+
/// This allows for mocking in tests.
6+
/// </summary>
7+
public interface IWslPathConverter
8+
{
9+
/// <summary>
10+
/// Attempts to convert a WSL path to Windows format using wsl.exe.
11+
/// </summary>
12+
/// <param name="wslPath">The WSL path to convert</param>
13+
/// <param name="windowsPath">The converted Windows path if successful</param>
14+
/// <returns>True if conversion succeeded, false otherwise</returns>
15+
bool TryConvertToWindowsPath(string wslPath, out string windowsPath);
16+
17+
/// <summary>
18+
/// Attempts to load WSL mount mappings from /etc/fstab.
19+
/// </summary>
20+
/// <returns>Dictionary of mount point to Windows path mappings</returns>
21+
Dictionary<string, string> LoadFstabMappings();
22+
}
23+
}
24+

0 commit comments

Comments
 (0)