Files
lserp_cs_6.0/插件库/Lskj.AgentBridge/NamedPipeAgentBridgeServer.cs
2026-08-14 14:28:28 +08:00

392 lines
15 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Pipes;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using Newtonsoft.Json;
namespace Lskj.AgentBridge
{
public sealed class NamedPipeAgentBridgeServer : IDisposable
{
internal const int MaximumMessageBytes = DeadlineFrameCodec.MaximumMessageBytes;
internal const int MaximumConcurrentConnections = 4;
internal const int ListenerStartupTimeoutMilliseconds = 5000;
internal const int ListenerStopTimeoutMilliseconds = 2000;
internal static readonly TimeSpan RequestReadTimeout = TimeSpan.FromSeconds(15);
internal static readonly TimeSpan ResponseWriteTimeout = TimeSpan.FromSeconds(15);
private static readonly Regex SafePipeName = new Regex(
"^[A-Za-z0-9_.-]{1,128}$",
RegexOptions.Compiled | RegexOptions.CultureInvariant);
private readonly string _pipeName;
private readonly IAgentBridgeRuntime _runtime;
private readonly SecurityIdentifier _userSid;
private readonly object _syncRoot = new object();
private readonly Semaphore _connectionSlots = new Semaphore(
MaximumConcurrentConnections,
MaximumConcurrentConnections);
private readonly HashSet<NamedPipeServerStream> _connections =
new HashSet<NamedPipeServerStream>();
private readonly ManualResetEvent _listenerReady =
new ManualResetEvent(false);
private volatile bool _running;
private bool _disposed;
private Exception _listenerStartupError;
private Thread _acceptThread;
private NamedPipeServerStream _listener;
public NamedPipeAgentBridgeServer(string pipeName, IAgentBridgeRuntime runtime)
{
if (string.IsNullOrWhiteSpace(pipeName) || !SafePipeName.IsMatch(pipeName))
throw new ArgumentException("管道名称只允许 1-128 位字母、数字、点、下划线和连字符。", "pipeName");
if (runtime == null) throw new ArgumentNullException("runtime");
_pipeName = pipeName;
_runtime = runtime;
using (WindowsIdentity identity = WindowsIdentity.GetCurrent())
{
SecurityIdentifier user = identity == null ? null : identity.User;
if (user == null) throw new InvalidOperationException("无法识别当前 Windows 用户。");
// Pin the interactive ERP identity at construction time. Listener/worker
// thread token changes must never widen a later pipe instance ACL.
_userSid = new SecurityIdentifier(user.Value);
}
}
public string PipeName
{
get { return _pipeName; }
}
public void Start()
{
lock (_syncRoot)
{
if (_disposed)
throw new ObjectDisposedException("NamedPipeAgentBridgeServer");
if (_running) return;
if (_acceptThread != null && _acceptThread.IsAlive)
throw new InvalidOperationException("上一次命名管道监听线程尚未结束。");
// Validate the Windows identity/ACL and pipe creation synchronously.
// The ERP bootstrap writes discovery only after Start returns.
using (NamedPipeServerStream validation = CreateServer()) { }
_listenerReady.Reset();
_listenerStartupError = null;
_running = true;
_acceptThread = new Thread(AcceptLoop);
_acceptThread.Name = "Lskj.AgentBridge.Accept";
_acceptThread.IsBackground = true;
try
{
_acceptThread.Start();
}
catch
{
_running = false;
_acceptThread = null;
throw;
}
}
if (!_listenerReady.WaitOne(ListenerStartupTimeoutMilliseconds, false))
{
Stop();
throw new TimeoutException("命名管道监听线程未在安全期限内就绪。");
}
Exception startupError;
bool running;
bool disposed;
lock (_syncRoot)
{
startupError = _listenerStartupError;
running = _running;
disposed = _disposed;
}
if (disposed) throw new ObjectDisposedException("NamedPipeAgentBridgeServer");
if (startupError != null)
{
Stop();
throw new InvalidOperationException("命名管道监听线程启动失败。", startupError);
}
if (!running)
throw new InvalidOperationException("命名管道监听线程在启动期间停止。");
}
public void Stop()
{
StopCore();
}
public void Dispose()
{
lock (_syncRoot) _disposed = true;
StopCore();
}
private void StopCore()
{
List<NamedPipeServerStream> active;
Thread acceptThread;
lock (_syncRoot)
{
_running = false;
_listenerReady.Set();
if (_listener != null)
{
try { _listener.Dispose(); }
catch (ObjectDisposedException) { }
_listener = null;
}
active = new List<NamedPipeServerStream>(_connections);
_connections.Clear();
acceptThread = _acceptThread;
}
foreach (NamedPipeServerStream connection in active)
{
try { connection.Dispose(); }
catch (ObjectDisposedException) { }
}
bool stopped = acceptThread == null
|| ReferenceEquals(acceptThread, Thread.CurrentThread)
|| acceptThread.Join(ListenerStopTimeoutMilliseconds);
if (stopped)
{
lock (_syncRoot)
{
if (ReferenceEquals(_acceptThread, acceptThread))
_acceptThread = null;
}
}
}
private void AcceptLoop()
{
bool startupSignalled = false;
while (_running)
{
NamedPipeServerStream server = null;
bool slotAcquired = false;
try
{
while (_running && !_connectionSlots.WaitOne(250, false)) { }
if (!_running) return;
slotAcquired = true;
server = CreateServer();
lock (_syncRoot)
{
if (!_running)
{
server.Dispose();
return;
}
_listener = server;
}
_listenerReady.Set();
startupSignalled = true;
server.WaitForConnection();
lock (_syncRoot)
{
if (ReferenceEquals(_listener, server)) _listener = null;
if (!_running)
{
server.Dispose();
return;
}
_connections.Add(server);
}
Thread worker = new Thread(HandleConnection);
worker.Name = "Lskj.AgentBridge.Client";
worker.IsBackground = true;
try
{
worker.Start(server);
}
catch
{
lock (_syncRoot) _connections.Remove(server);
throw;
}
server = null;
slotAcquired = false;
}
catch (IOException ex)
{
if (!HandleAcceptFailure(ex, startupSignalled)) return;
}
catch (ObjectDisposedException ex)
{
if (!HandleAcceptFailure(ex, startupSignalled)) return;
}
catch (Exception ex)
{
if (!HandleAcceptFailure(ex, startupSignalled)) return;
}
finally
{
lock (_syncRoot)
{
if (ReferenceEquals(_listener, server)) _listener = null;
}
if (server != null) server.Dispose();
if (slotAcquired) _connectionSlots.Release();
}
}
}
private bool HandleAcceptFailure(Exception error, bool startupSignalled)
{
if (!_running) return false;
if (!startupSignalled)
{
lock (_syncRoot)
{
_listenerStartupError = error;
_running = false;
}
_listenerReady.Set();
return false;
}
Thread.Sleep(250);
return _running;
}
private void HandleConnection(object state)
{
NamedPipeServerStream pipe = (NamedPipeServerStream)state;
try
{
using (pipe)
{
BridgeRequest request = null;
BridgeResponse response;
try
{
byte[] payload = DeadlineFrameCodec.ReadFrame(
pipe,
RequestReadTimeout);
request = BridgeRequestParser.Parse(BridgeUtf8Codec.Decode(payload));
uint clientProcessId;
bool clientProcessKnown;
try
{
clientProcessKnown = GetNamedPipeClientProcessId(
pipe.SafePipeHandle,
out clientProcessId);
}
catch (DllNotFoundException)
{
clientProcessKnown = false;
clientProcessId = 0;
}
catch (EntryPointNotFoundException)
{
clientProcessKnown = false;
clientProcessId = 0;
}
request.TransportClientProcessId = clientProcessKnown
&& clientProcessId <= int.MaxValue
? (int)clientProcessId : 0;
response = _runtime.Handle(request);
}
catch (TimeoutException)
{
return;
}
catch (ObjectDisposedException)
{
return;
}
catch (DecoderFallbackException)
{
response = BridgeResponse.Error(
request,
"invalid_utf8",
"请求必须使用有效 UTF-8 编码。");
}
catch (JsonException)
{
response = BridgeResponse.Error(request, "invalid_json", "请求不是有效 JSON。");
}
catch (InvalidDataException ex)
{
response = BridgeResponse.Error(request, "invalid_frame", ex.Message);
}
catch (Exception)
{
response = BridgeResponse.Error(request, "bridge_transport_error", "命令桥传输失败。");
}
try
{
byte[] body = BridgeUtf8Codec.Encode(
JsonConvert.SerializeObject(response));
DeadlineFrameCodec.WriteFrame(
pipe,
body,
ResponseWriteTimeout);
}
catch (IOException) { }
catch (ObjectDisposedException) { }
catch (TimeoutException) { }
catch (Exception) { }
}
}
finally
{
lock (_syncRoot) _connections.Remove(pipe);
_connectionSlots.Release();
}
}
private NamedPipeServerStream CreateServer()
{
PipeSecurity security = new PipeSecurity();
security.SetAccessRuleProtection(true, false);
security.SetOwner(_userSid);
// A matching domain user SID is not sufficient to prove that a client is
// local: Windows named pipes can also be reached over the network. Explicit
// deny ACEs prevent SMB/anonymous clients before any protocol bytes are read.
security.AddAccessRule(new PipeAccessRule(
new SecurityIdentifier(WellKnownSidType.NetworkSid, null),
PipeAccessRights.FullControl,
AccessControlType.Deny));
security.AddAccessRule(new PipeAccessRule(
new SecurityIdentifier(WellKnownSidType.AnonymousSid, null),
PipeAccessRights.FullControl,
AccessControlType.Deny));
security.AddAccessRule(new PipeAccessRule(
_userSid,
PipeAccessRights.ReadWrite | PipeAccessRights.CreateNewInstance,
AccessControlType.Allow));
security.AddAccessRule(new PipeAccessRule(
new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null),
PipeAccessRights.FullControl,
AccessControlType.Allow));
return new NamedPipeServerStream(
_pipeName,
PipeDirection.InOut,
MaximumConcurrentConnections,
PipeTransmissionMode.Byte,
PipeOptions.Asynchronous,
MaximumMessageBytes,
MaximumMessageBytes,
security);
}
[System.Runtime.InteropServices.DllImport(
"kernel32.dll",
SetLastError = true)]
private static extern bool GetNamedPipeClientProcessId(
Microsoft.Win32.SafeHandles.SafePipeHandle pipe,
out uint clientProcessId);
}
}