Skip to content

Commit 6feeeab

Browse files
sharpninjaCopilot
andcommitted
feat: add host infrastructure service implementations and DI wiring
P1.6: Create 5 host-side implementations in Services/Infrastructure/: - FileSystemService wraps System.IO.File/Directory - ProcessLauncherService wraps Process.Start - TimerService wraps System.Threading.Timer - JsonParsingService wraps System.Text.Json - FileSystemWatcherService wraps FileSystemWatcher P1.7: Register all 5 in UiCoreServiceProviderFactory with defaults. Update UiCoreAppRuntime to forward infrastructure params. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8b51fd1 commit 6feeeab

7 files changed

Lines changed: 307 additions & 1 deletion

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Linq;
5+
using System.Threading;
6+
using System.Threading.Tasks;
7+
using McpServer.UI.Core.Services;
8+
9+
namespace McpServerManager.Core.Services.Infrastructure;
10+
11+
/// <summary>
12+
/// Host implementation of <see cref="IFileSystemService"/> backed by <see cref="System.IO"/>.
13+
/// </summary>
14+
public sealed class FileSystemService : IFileSystemService
15+
{
16+
public bool FileExists(string path) => File.Exists(path);
17+
18+
public bool DirectoryExists(string path) => Directory.Exists(path);
19+
20+
public string ReadAllText(string path) => File.ReadAllText(path);
21+
22+
public Task<string> ReadAllTextAsync(string path, CancellationToken ct = default)
23+
=> File.ReadAllTextAsync(path, ct);
24+
25+
public string[] ReadAllLines(string path) => File.ReadAllLines(path);
26+
27+
public void WriteAllText(string path, string content) => File.WriteAllText(path, content);
28+
29+
public Task WriteAllTextAsync(string path, string content, CancellationToken ct = default)
30+
=> File.WriteAllTextAsync(path, content, ct);
31+
32+
public void MoveFile(string source, string destination) => File.Move(source, destination);
33+
34+
public void DeleteFile(string path) => File.Delete(path);
35+
36+
public DateTime GetLastWriteTimeUtc(string path) => File.GetLastWriteTimeUtc(path);
37+
38+
public IEnumerable<FileEntry> EnumerateFiles(string directory, string searchPattern, bool recursive)
39+
{
40+
var option = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
41+
return new DirectoryInfo(directory)
42+
.EnumerateFiles(searchPattern, option)
43+
.Select(fi => new FileEntry(fi.FullName, fi.Name, fi.Extension, false));
44+
}
45+
46+
public IEnumerable<DirectoryEntry> EnumerateDirectories(string directory, string searchPattern, bool recursive)
47+
{
48+
var option = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
49+
return new DirectoryInfo(directory)
50+
.EnumerateDirectories(searchPattern, option)
51+
.Select(di => new DirectoryEntry(di.FullName, di.Name));
52+
}
53+
54+
public string GetFullPath(string path) => Path.GetFullPath(path);
55+
56+
public string? GetDirectoryName(string path) => Path.GetDirectoryName(path);
57+
58+
public string CombinePath(params string[] paths) => Path.Combine(paths);
59+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
using System;
2+
using System.IO;
3+
using McpServer.UI.Core.Services;
4+
5+
namespace McpServerManager.Core.Services.Infrastructure;
6+
7+
/// <summary>
8+
/// Host implementation of <see cref="IFileSystemWatcherService"/> backed by <see cref="FileSystemWatcher"/>.
9+
/// </summary>
10+
public sealed class FileSystemWatcherService : IFileSystemWatcherService
11+
{
12+
public IWatcherHandle Watch(string directory, string filter, Action<string> onChanged)
13+
=> new WatcherHandle(directory, filter, onChanged);
14+
15+
private sealed class WatcherHandle : IWatcherHandle
16+
{
17+
private FileSystemWatcher? _watcher;
18+
19+
public WatcherHandle(string directory, string filter, Action<string> onChanged)
20+
{
21+
_watcher = new FileSystemWatcher(directory, filter)
22+
{
23+
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.CreationTime,
24+
EnableRaisingEvents = true
25+
};
26+
27+
_watcher.Changed += (_, e) => onChanged(e.FullPath);
28+
_watcher.Created += (_, e) => onChanged(e.FullPath);
29+
_watcher.Renamed += (_, e) => onChanged(e.FullPath);
30+
}
31+
32+
public void Stop()
33+
{
34+
if (_watcher is not null)
35+
_watcher.EnableRaisingEvents = false;
36+
}
37+
38+
public void Dispose()
39+
{
40+
_watcher?.Dispose();
41+
_watcher = null;
42+
}
43+
}
44+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System;
2+
using System.Text.Json;
3+
using System.Text.Json.Nodes;
4+
using McpServer.UI.Core.Services;
5+
6+
namespace McpServerManager.Core.Services.Infrastructure;
7+
8+
/// <summary>
9+
/// Host implementation of <see cref="IJsonParsingService"/> backed by <see cref="System.Text.Json"/>.
10+
/// </summary>
11+
public sealed class JsonParsingService : IJsonParsingService
12+
{
13+
private static readonly JsonSerializerOptions IndentedOptions = new() { WriteIndented = true };
14+
15+
public JsonTreeResult ParseToTree(string jsonText)
16+
{
17+
var node = JsonNode.Parse(jsonText);
18+
int count = CountNodes(node);
19+
return new JsonTreeResult(node!, count);
20+
}
21+
22+
public string? Validate(string jsonText)
23+
{
24+
try
25+
{
26+
using var doc = JsonDocument.Parse(jsonText);
27+
return null;
28+
}
29+
catch (JsonException ex)
30+
{
31+
return ex.Message;
32+
}
33+
}
34+
35+
public string PrettyPrint(string jsonText)
36+
{
37+
var node = JsonNode.Parse(jsonText);
38+
return node?.ToJsonString(IndentedOptions) ?? jsonText;
39+
}
40+
41+
private static int CountNodes(JsonNode? node)
42+
{
43+
if (node is null) return 0;
44+
45+
int count = 1;
46+
if (node is JsonObject obj)
47+
{
48+
foreach (var kvp in obj)
49+
count += CountNodes(kvp.Value);
50+
}
51+
else if (node is JsonArray arr)
52+
{
53+
foreach (var item in arr)
54+
count += CountNodes(item);
55+
}
56+
return count;
57+
}
58+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Threading;
4+
using System.Threading.Tasks;
5+
using McpServer.UI.Core.Services;
6+
7+
namespace McpServerManager.Core.Services.Infrastructure;
8+
9+
/// <summary>
10+
/// Host implementation of <see cref="IProcessLauncherService"/> backed by <see cref="Process"/>.
11+
/// </summary>
12+
public sealed class ProcessLauncherService : IProcessLauncherService
13+
{
14+
public void OpenWithDefaultApp(string pathOrUrl)
15+
{
16+
Process.Start(new ProcessStartInfo(pathOrUrl) { UseShellExecute = true });
17+
}
18+
19+
public async Task<ProcessResult> RunAsync(
20+
string fileName,
21+
string arguments,
22+
string? workingDirectory = null,
23+
CancellationToken ct = default)
24+
{
25+
using var process = new Process
26+
{
27+
StartInfo = new ProcessStartInfo
28+
{
29+
FileName = fileName,
30+
Arguments = arguments,
31+
WorkingDirectory = workingDirectory ?? string.Empty,
32+
RedirectStandardOutput = true,
33+
RedirectStandardError = true,
34+
UseShellExecute = false,
35+
CreateNoWindow = true
36+
}
37+
};
38+
39+
process.Start();
40+
41+
var stdout = await process.StandardOutput.ReadToEndAsync(ct).ConfigureAwait(false);
42+
var stderr = await process.StandardError.ReadToEndAsync(ct).ConfigureAwait(false);
43+
await process.WaitForExitAsync(ct).ConfigureAwait(false);
44+
45+
return new ProcessResult(process.ExitCode, stdout, stderr);
46+
}
47+
48+
public void ShellExecute(string command, string arguments)
49+
{
50+
Process.Start(new ProcessStartInfo(command, arguments) { UseShellExecute = true });
51+
}
52+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using McpServer.UI.Core.Services;
5+
6+
namespace McpServerManager.Core.Services.Infrastructure;
7+
8+
/// <summary>
9+
/// Host implementation of <see cref="ITimerService"/> backed by <see cref="System.Threading.Timer"/>.
10+
/// </summary>
11+
public sealed class TimerService : ITimerService
12+
{
13+
public ITimerHandle CreateRecurring(TimeSpan interval, Func<CancellationToken, Task> callback)
14+
=> new TimerHandle(interval, callback, recurring: true);
15+
16+
public ITimerHandle CreateOneShot(TimeSpan delay, Func<CancellationToken, Task> callback)
17+
=> new TimerHandle(delay, callback, recurring: false);
18+
19+
private sealed class TimerHandle : ITimerHandle
20+
{
21+
private Timer? _timer;
22+
private CancellationTokenSource? _cts;
23+
private readonly Func<CancellationToken, Task> _callback;
24+
private readonly bool _recurring;
25+
private TimeSpan _interval;
26+
27+
public TimerHandle(TimeSpan interval, Func<CancellationToken, Task> callback, bool recurring)
28+
{
29+
_interval = interval;
30+
_callback = callback;
31+
_recurring = recurring;
32+
_cts = new CancellationTokenSource();
33+
_timer = new Timer(OnTick, null, interval, recurring ? interval : Timeout.InfiniteTimeSpan);
34+
}
35+
36+
private async void OnTick(object? state)
37+
{
38+
var cts = _cts;
39+
if (cts is null || cts.IsCancellationRequested)
40+
return;
41+
42+
try
43+
{
44+
await _callback(cts.Token).ConfigureAwait(false);
45+
}
46+
catch (OperationCanceledException) { }
47+
}
48+
49+
public void Stop()
50+
{
51+
_timer?.Change(Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan);
52+
}
53+
54+
public void Restart(TimeSpan? newInterval = null)
55+
{
56+
if (newInterval.HasValue)
57+
_interval = newInterval.Value;
58+
59+
_timer?.Change(_interval, _recurring ? _interval : Timeout.InfiniteTimeSpan);
60+
}
61+
62+
public void Dispose()
63+
{
64+
_cts?.Cancel();
65+
_cts?.Dispose();
66+
_cts = null;
67+
_timer?.Dispose();
68+
_timer = null;
69+
}
70+
}
71+
}

src/McpServerManager.Core/Services/UiCoreAppRuntime.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using McpServer.UI.Core.Services;
23
using McpServer.UI.Core.ViewModels;
34
using Microsoft.Extensions.DependencyInjection;
45

@@ -12,12 +13,20 @@ public UiCoreAppRuntime(
1213
McpVoiceConversationService? voiceService = null,
1314
McpSessionLogService? sessionLogService = null,
1415
McpAgentEventStreamService? eventStreamService = null,
16+
IFileSystemService? fileSystemService = null,
17+
IProcessLauncherService? processLauncherService = null,
18+
ITimerService? timerService = null,
19+
IJsonParsingService? jsonParsingService = null,
20+
IFileSystemWatcherService? fileSystemWatcherService = null,
1521
WorkspaceContextViewModel? workspaceContext = null)
1622
{
1723
WorkspaceContext = workspaceContext ?? new WorkspaceContextViewModel();
1824
Services = UiCoreServiceProviderFactory.Build(
1925
todoService, workspaceService, voiceService,
20-
sessionLogService, eventStreamService, WorkspaceContext);
26+
sessionLogService, eventStreamService,
27+
fileSystemService, processLauncherService,
28+
timerService, jsonParsingService, fileSystemWatcherService,
29+
WorkspaceContext);
2130
}
2231

2332
public WorkspaceContextViewModel WorkspaceContext { get; }

src/McpServerManager.Core/Services/UiCoreServiceProviderFactory.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using McpServer.UI.Core;
44
using McpServer.UI.Core.Services;
55
using McpServer.UI.Core.ViewModels;
6+
using McpServerManager.Core.Services.Infrastructure;
67
using Microsoft.Extensions.DependencyInjection;
78

89
namespace McpServerManager.Core.Services;
@@ -15,6 +16,11 @@ public static ServiceProvider Build(
1516
McpVoiceConversationService? voiceService = null,
1617
McpSessionLogService? sessionLogService = null,
1718
McpAgentEventStreamService? eventStreamService = null,
19+
IFileSystemService? fileSystemService = null,
20+
IProcessLauncherService? processLauncherService = null,
21+
ITimerService? timerService = null,
22+
IJsonParsingService? jsonParsingService = null,
23+
IFileSystemWatcherService? fileSystemWatcherService = null,
1824
WorkspaceContextViewModel? workspaceContext = null)
1925
{
2026
if (todoService is null && workspaceService is null)
@@ -40,6 +46,13 @@ public static ServiceProvider Build(
4046
if (eventStreamService is not null)
4147
services.AddSingleton<IEventStreamApiClient>(_ => new UiCoreEventStreamApiClientAdapter(eventStreamService));
4248

49+
// Infrastructure services — use host-provided or default implementations
50+
services.AddSingleton(fileSystemService ?? new FileSystemService());
51+
services.AddSingleton(processLauncherService ?? new ProcessLauncherService());
52+
services.AddSingleton(timerService ?? new TimerService());
53+
services.AddSingleton(jsonParsingService ?? new JsonParsingService());
54+
services.AddSingleton(fileSystemWatcherService ?? new FileSystemWatcherService());
55+
4356
services.AddCqrsDispatcher();
4457
services.AddUiCore();
4558
if (workspaceContext is not null)

0 commit comments

Comments
 (0)