mirror of
https://github.com/gnh1201/welsonjs.git
synced 2026-03-03 01:50:55 +00:00
Use HTTP forwarder for stdio-jsonrpc2
Replace the old StdioServer-based stdio JSON-RPC flow with an in-process HTTP forwarder. Program.cs now reads raw stdin, posts payloads to the configured ResourceServerPrefix + "jsonrpc2" using HttpClient (configurable timeout), writes HTTP response bytes back to stdout, and includes improved logging and SafePreviewUtf8 for safe payload previews. StdioServer.cs and its project entry were removed, and Resources.resx HttpClientTimeout was increased to 300 (Designer version updated). This centralizes stdio handling and HTTP forwarding with better error handling and diagnostics.
This commit is contained in:
parent
33148ec826
commit
91e18bbb0e
|
|
@ -11,9 +11,12 @@ using System.Diagnostics;
|
|||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace WelsonJS.Launcher
|
||||
|
|
@ -61,10 +64,11 @@ namespace WelsonJS.Launcher
|
|||
return;
|
||||
}
|
||||
|
||||
// if use stdio JSON-RPC 2.0 mode
|
||||
// if use the stdio-jsonrpc2 forwarder
|
||||
if (HasArg(args, "--stdio-jsonrpc2"))
|
||||
{
|
||||
RunJsonRpc2StdioServer();
|
||||
_logger.Info("Starting in the stdio-jsonrpc2 forwarder...");
|
||||
ProcessStdioJsonRpc2().GetAwaiter().GetResult();
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -102,40 +106,121 @@ namespace WelsonJS.Launcher
|
|||
return false;
|
||||
}
|
||||
|
||||
private static void RunJsonRpc2StdioServer()
|
||||
private async static Task ProcessStdioJsonRpc2()
|
||||
{
|
||||
var server = new StdioServer(async (payload, ct) =>
|
||||
string serverPrefix = GetAppConfig("ResourceServerPrefix");
|
||||
string endpoint = $"{serverPrefix}jsonrpc2";
|
||||
int timeout = int.TryParse(GetAppConfig("HttpClientTimeout"), out timeout) ? timeout : 300;
|
||||
|
||||
var http = new HttpClient
|
||||
{
|
||||
var dispatcher = new JsonRpc2Dispatcher(_logger);
|
||||
var body = Encoding.UTF8.GetString(payload);
|
||||
Timeout = TimeSpan.FromSeconds(timeout)
|
||||
};
|
||||
|
||||
using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(300)))
|
||||
using (var stdin = Console.OpenStandardInput())
|
||||
using (var stdout = Console.OpenStandardOutput())
|
||||
{
|
||||
var buffer = new byte[8192];
|
||||
|
||||
while (true)
|
||||
{
|
||||
string result = await dispatcher.HandleAsync(
|
||||
body,
|
||||
async (method, ser, _ct) =>
|
||||
int read;
|
||||
try
|
||||
{
|
||||
read = await stdin.ReadAsync(buffer, 0, buffer.Length);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("[stdio] stdin read failed", ex);
|
||||
break;
|
||||
}
|
||||
|
||||
if (read <= 0)
|
||||
{
|
||||
_logger.Info("[stdio] EOF received, exiting loop");
|
||||
break;
|
||||
}
|
||||
|
||||
byte[] payload = new byte[read];
|
||||
Buffer.BlockCopy(buffer, 0, payload, 0, read);
|
||||
|
||||
_logger.Debug($"[stdio] recv {payload.Length} bytes");
|
||||
_logger.Debug($"[stdio] payload preview: {SafePreviewUtf8(payload, 512)}");
|
||||
|
||||
try
|
||||
{
|
||||
using (var content = new ByteArrayContent(payload))
|
||||
{
|
||||
switch (method)
|
||||
// Content-Type: application/json
|
||||
content.Headers.ContentType =
|
||||
new MediaTypeHeaderValue("application/json")
|
||||
{
|
||||
CharSet = "utf-8"
|
||||
};
|
||||
|
||||
_logger.Debug($"[http] POST {endpoint}");
|
||||
|
||||
using (var response = await http.PostAsync(endpoint, content))
|
||||
{
|
||||
case "tools/list":
|
||||
return Encoding.UTF8.GetString(ResourceServer.GetResource("McpToolList.json"));
|
||||
_logger.Info(
|
||||
$"[http] status={(int)response.StatusCode} {response.ReasonPhrase}");
|
||||
|
||||
case "tools/call":
|
||||
// TODO: implement tool call handling
|
||||
return string.Empty;
|
||||
foreach (var h in response.Headers)
|
||||
_logger.Debug($"[http] H {h.Key}: {string.Join(", ", h.Value)}");
|
||||
|
||||
default:
|
||||
return string.Empty;
|
||||
foreach (var h in response.Content.Headers)
|
||||
_logger.Debug($"[http] HC {h.Key}: {string.Join(", ", h.Value)}");
|
||||
|
||||
byte[] responseBytes =
|
||||
await response.Content.ReadAsByteArrayAsync();
|
||||
|
||||
_logger.Debug($"[http] body {responseBytes.Length} bytes");
|
||||
|
||||
_logger.Debug(
|
||||
$"[http] body preview: {SafePreviewUtf8(responseBytes, 2048)}");
|
||||
|
||||
await stdout.WriteAsync(responseBytes, 0, responseBytes.Length);
|
||||
await stdout.FlushAsync();
|
||||
}
|
||||
},
|
||||
cts.Token);
|
||||
|
||||
// Fix: Convert string result to byte[] before returning
|
||||
return Encoding.UTF8.GetBytes(result);
|
||||
}
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
_logger.Error("[http] request timed out or canceled", ex);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.Error("[http] request failed", ex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error("[http] unexpected error", ex);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
server.Run();
|
||||
private static string SafePreviewUtf8(byte[] bytes, int maxBytes)
|
||||
{
|
||||
if (bytes == null || bytes.Length == 0)
|
||||
return "(empty)";
|
||||
|
||||
try
|
||||
{
|
||||
int len = Math.Min(bytes.Length, maxBytes);
|
||||
string s = Encoding.UTF8.GetString(bytes, 0, len);
|
||||
|
||||
s = s.Replace("\r", "\\r").Replace("\n", "\\n");
|
||||
|
||||
if (bytes.Length > maxBytes)
|
||||
s += $"...(truncated, total {bytes.Length} bytes)";
|
||||
|
||||
return s;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "(binary / non-utf8 response)";
|
||||
}
|
||||
}
|
||||
|
||||
private static void InitializeAssemblyLoader()
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ namespace WelsonJS.Launcher.Properties {
|
|||
// 클래스에서 자동으로 생성되었습니다.
|
||||
// 멤버를 추가하거나 제거하려면 .ResX 파일을 편집한 다음 /str 옵션을 사용하여 ResGen을
|
||||
// 다시 실행하거나 VS 프로젝트를 다시 빌드하십시오.
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "18.0.0.0")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
|
@ -197,7 +197,7 @@ namespace WelsonJS.Launcher.Properties {
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 90과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// 300과(와) 유사한 지역화된 문자열을 찾습니다.
|
||||
/// </summary>
|
||||
internal static string HttpClientTimeout {
|
||||
get {
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@
|
|||
<value>https://catswords.blob.core.windows.net/welsonjs/blob.config.xml</value>
|
||||
</data>
|
||||
<data name="HttpClientTimeout" xml:space="preserve">
|
||||
<value>90</value>
|
||||
<value>300</value>
|
||||
</data>
|
||||
<data name="IpQueryApiKey" xml:space="preserve">
|
||||
<value />
|
||||
|
|
|
|||
|
|
@ -1,209 +0,0 @@
|
|||
// StdioServer.cs
|
||||
// Minimal stdio server (Content-Length framing) with delegate-based extension.
|
||||
// - Handles ONLY stdio framing (read/write message boundaries)
|
||||
// - No JSON parsing/formatting
|
||||
// - Sequential processing
|
||||
// - Cancellation via Ctrl+C, and EOF handling
|
||||
//
|
||||
// Delegate contract:
|
||||
// - input: raw UTF-8 payload bytes (exactly Content-Length)
|
||||
// - output: raw UTF-8 payload bytes to write (or null/empty to write nothing)
|
||||
//
|
||||
// Typical use: plug JSON-RPC/MCP dispatcher outside of this class.
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace WelsonJS.Launcher
|
||||
{
|
||||
public sealed class StdioServer
|
||||
{
|
||||
public delegate Task<byte[]> Handler(byte[] payload, CancellationToken ct);
|
||||
|
||||
private readonly Stream _inStream;
|
||||
private readonly Stream _outStream;
|
||||
private readonly Handler _handler;
|
||||
|
||||
public StdioServer(Handler handler)
|
||||
{
|
||||
if (handler == null)
|
||||
throw new ArgumentNullException("handler");
|
||||
|
||||
_handler = handler;
|
||||
_inStream = Console.OpenStandardInput();
|
||||
_outStream = Console.OpenStandardOutput();
|
||||
}
|
||||
|
||||
public void Run()
|
||||
{
|
||||
using (var cts = new CancellationTokenSource())
|
||||
{
|
||||
Console.CancelKeyPress += (s, e) =>
|
||||
{
|
||||
e.Cancel = true;
|
||||
cts.Cancel();
|
||||
};
|
||||
|
||||
RunAsync(cts.Token).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RunAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
byte[] payload;
|
||||
|
||||
// 1) read one framed message (blocks here waiting for stdin)
|
||||
try
|
||||
{
|
||||
payload = await ReadOneAsync(ct).ConfigureAwait(false);
|
||||
if (payload == null) return; // EOF => exit
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// framing broken or stream error => stop (or continue if you want resync)
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) handle + write response (never kill the loop on handler failure)
|
||||
try
|
||||
{
|
||||
var resp = await _handler(payload, ct).ConfigureAwait(false);
|
||||
if (resp == null) resp = new byte[0];
|
||||
|
||||
await WriteOneAsync(resp, ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// keep listening even if handler fails
|
||||
// optionally write empty response so client doesn't hang waiting
|
||||
try { await WriteOneAsync(new byte[0], ct).ConfigureAwait(false); } catch { }
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<byte[]> ReadOneAsync(CancellationToken ct)
|
||||
{
|
||||
// Read headers until \r\n\r\n (blocks on stdin)
|
||||
string headers = await ReadHeadersAsync(ct).ConfigureAwait(false);
|
||||
if (headers == null) return null; // EOF
|
||||
|
||||
int contentLength = ParseContentLength(headers);
|
||||
if (contentLength < 0) throw new InvalidDataException("Missing Content-Length");
|
||||
|
||||
return await ReadExactAsync(_inStream, contentLength, ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task WriteOneAsync(byte[] payload, CancellationToken ct)
|
||||
{
|
||||
if (payload == null) payload = new byte[0];
|
||||
|
||||
string header = "Content-Length: " + payload.Length.ToString(CultureInfo.InvariantCulture) + "\r\n\r\n";
|
||||
byte[] headerBytes = Encoding.ASCII.GetBytes(header);
|
||||
|
||||
await _outStream.WriteAsync(headerBytes, 0, headerBytes.Length, ct).ConfigureAwait(false);
|
||||
if (payload.Length > 0)
|
||||
await _outStream.WriteAsync(payload, 0, payload.Length, ct).ConfigureAwait(false);
|
||||
|
||||
await _outStream.FlushAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task<string> ReadHeadersAsync(CancellationToken ct)
|
||||
{
|
||||
// read byte-by-byte until CRLFCRLF
|
||||
var buf = new byte[4096];
|
||||
int len = 0;
|
||||
|
||||
while (true)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
int b = await ReadByteAsync(_inStream, ct).ConfigureAwait(false);
|
||||
if (b < 0)
|
||||
{
|
||||
if (len == 0) return null; // clean EOF
|
||||
throw new EndOfStreamException("EOF while reading headers");
|
||||
}
|
||||
|
||||
if (len == buf.Length)
|
||||
{
|
||||
// grow
|
||||
var nb = new byte[buf.Length * 2];
|
||||
Buffer.BlockCopy(buf, 0, nb, 0, buf.Length);
|
||||
buf = nb;
|
||||
}
|
||||
|
||||
buf[len++] = (byte)b;
|
||||
|
||||
if (len >= 4 &&
|
||||
buf[len - 4] == 13 &&
|
||||
buf[len - 3] == 10 &&
|
||||
buf[len - 2] == 13 &&
|
||||
buf[len - 1] == 10)
|
||||
{
|
||||
return Encoding.ASCII.GetString(buf, 0, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int ParseContentLength(string headers)
|
||||
{
|
||||
// minimal parser: Content-Length: N
|
||||
var lines = headers.Split(new[] { "\r\n" }, StringSplitOptions.None);
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
var line = lines[i];
|
||||
int colon = line.IndexOf(':');
|
||||
if (colon <= 0) continue;
|
||||
|
||||
var name = line.Substring(0, colon).Trim();
|
||||
if (!name.Equals("Content-Length", StringComparison.OrdinalIgnoreCase)) continue;
|
||||
|
||||
var val = line.Substring(colon + 1).Trim();
|
||||
int n;
|
||||
if (int.TryParse(val, NumberStyles.Integer, CultureInfo.InvariantCulture, out n))
|
||||
return n;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static async Task<byte[]> ReadExactAsync(Stream s, int nBytes, CancellationToken ct)
|
||||
{
|
||||
if (nBytes == 0) return new byte[0];
|
||||
|
||||
var buf = new byte[nBytes];
|
||||
int read = 0;
|
||||
|
||||
while (read < nBytes)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
int n = await s.ReadAsync(buf, read, nBytes - read, ct).ConfigureAwait(false);
|
||||
if (n <= 0) throw new EndOfStreamException("EOF while reading payload");
|
||||
read += n;
|
||||
}
|
||||
|
||||
return buf;
|
||||
}
|
||||
|
||||
private static async Task<int> ReadByteAsync(Stream s, CancellationToken ct)
|
||||
{
|
||||
var b = new byte[1];
|
||||
int n = await s.ReadAsync(b, 0, 1, ct).ConfigureAwait(false);
|
||||
if (n <= 0) return -1;
|
||||
return b[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -133,7 +133,6 @@
|
|||
<DependentUpon>GlobalSettingsForm.cs</DependentUpon>
|
||||
</Compile>
|
||||
<Compile Include="ResourceServer.cs" />
|
||||
<Compile Include="StdioServer.cs" />
|
||||
<Compile Include="WebSocketManager.cs" />
|
||||
<EmbeddedResource Include="EnvForm.resx">
|
||||
<DependentUpon>EnvForm.cs</DependentUpon>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user