feat: add ERP agent pet bridge and startup guide
This commit is contained in:
@@ -0,0 +1,391 @@
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user