Forge
csharp1a59f514
1namespace TelegramRelay.Mcp;
2
3/// <summary>
4/// Runs TelegramNotifier.exe and parses output for MCP.
5/// Only one run at a time to avoid WTelegram.session file lock (each process opens the same session file).
6/// </summary>
7internal static class RelayRunner
8{
9 private static readonly SemaphoreSlim SingleRun = new(1, 1);
10
11 private static string GetRelayExePath()
12 {
13 var fromEnv = Environment.GetEnvironmentVariable("TELEGRAM_RELAY_EXE");
14 if (!string.IsNullOrWhiteSpace(fromEnv) && File.Exists(fromEnv))
15 return fromEnv;
16 var nextToMe = Path.Combine(AppContext.BaseDirectory, "TelegramNotifier.exe");
17 if (File.Exists(nextToMe))
18 return nextToMe;
19 throw new InvalidOperationException(
20 "TelegramNotifier.exe not found. Set TELEGRAM_RELAY_EXE to the path of TelegramNotifier.exe, or place it next to TelegramRelay.Mcp.");
21 }
22
23 /// <summary>
24 /// Run relay with args; returns (stdout, stderr, exitCode). Working directory = directory of the exe.
25 /// Serialized so only one Notifier process runs at a time (avoids WTelegram.session lock).
26 /// </summary>
27 public static async Task<(string StdOut, string StdErr, int ExitCode)> RunAsync(IReadOnlyList<string> args, CancellationToken cancellationToken = default)
28 {
29 await SingleRun.WaitAsync(cancellationToken).ConfigureAwait(false);
30 try
31 {
32 var exe = GetRelayExePath();
33 var workingDir = Path.GetDirectoryName(exe)!;
34 var startInfo = new System.Diagnostics.ProcessStartInfo
35 {
36 FileName = exe,
37 Arguments = string.Join(" ", args.Select(a => a.Contains(' ') ? "\"" + a.Replace("\"", "\\\"") + "\"" : a)),
38 WorkingDirectory = workingDir,
39 RedirectStandardOutput = true,
40 RedirectStandardError = true,
41 UseShellExecute = false,
42 CreateNoWindow = true
43 };
44 using var process = System.Diagnostics.Process.Start(startInfo);
45 if (process == null)
46 throw new InvalidOperationException("Failed to start TelegramNotifier.exe");
47 var outTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
48 var errTask = process.StandardError.ReadToEndAsync(cancellationToken);
49 await process.WaitForExitAsync(cancellationToken).ConfigureAwait(false);
50 var stdout = await outTask.ConfigureAwait(false);
51 var stderr = await errTask.ConfigureAwait(false);
52 return (stdout ?? "", stderr ?? "", process.ExitCode);
53 }
54 finally
55 {
56 SingleRun.Release();
57 }
58 }
59
60 /// <summary>
61 /// Get the last line that looks like JSON array (for get-messages: login line may appear before).
62 /// </summary>
63 public static string? ExtractJsonLine(string stdout)
64 {
65 var lines = stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
66 for (var i = lines.Length - 1; i >= 0; i--)
67 {
68 var line = lines[i].Trim();
69 if (line.StartsWith('[') && line.EndsWith(']'))
70 return line;
71 }
72 return null;
73 }
74}
75
View only · write via MCP/CIDE