初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using PCL.Core.App.IoC;
|
||||
using PCL.Core.IO;
|
||||
|
||||
namespace PCL.Core.App.Essentials;
|
||||
|
||||
/// <summary>
|
||||
/// 用于终止 Pipe RPC 执行过程并返回错误信息的异常<br/>
|
||||
/// 当抛出该异常时 RPC 服务端将会返回内容为 <c>Reason</c> 的 <c>ERR</c> 响应
|
||||
/// </summary>
|
||||
public class RpcException(string reason) : Exception
|
||||
{
|
||||
public string Reason => reason;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记一个方法或属性,使其成为 RPC 函数/属性。<br/>
|
||||
/// 方法签名需兼容 <see cref="RpcFunction"/> 委托,属性仅支持 <see langword="string"/> 类型。
|
||||
/// </summary>
|
||||
/// <param name="name">该 RPC 函数/属性的名称</param>
|
||||
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method)]
|
||||
[DependencyCollector<RpcFunction>("rpc-function", AttributeTargets.Method)]
|
||||
[DependencyCollector<string>("rpc-property", AttributeTargets.Property)]
|
||||
public sealed class RegisterRpc(string name) : Attribute;
|
||||
|
||||
/// <summary>
|
||||
/// RPC 服务项
|
||||
/// </summary>
|
||||
[LifecycleService(LifecycleState.Loaded)]
|
||||
[LifecycleScope("rpc", "远程执行服务")]
|
||||
public sealed partial class RpcService
|
||||
{
|
||||
private NamedPipeServerStream? _pipe;
|
||||
|
||||
[LifecycleStart]
|
||||
private void _Start()
|
||||
{
|
||||
_pipe = PipeComm.StartPipeServer("Echo", _EchoPipeName, _EchoPipeCallback);
|
||||
}
|
||||
|
||||
[LifecycleStop]
|
||||
private async Task _StopAsync()
|
||||
{
|
||||
if (_pipe is not null) await _pipe.DisposeAsync();
|
||||
}
|
||||
|
||||
[LifecycleDependencyInjection("rpc-function", AttributeTargets.Method)]
|
||||
private static void _CollectRpcFunctionRegistry(ImmutableList<(RpcFunction func, string name)> items)
|
||||
{
|
||||
foreach (var (func, name) in items)
|
||||
{
|
||||
AddFunction(name, func);
|
||||
}
|
||||
}
|
||||
|
||||
[LifecycleDependencyInjection("rpc-property", AttributeTargets.Property)]
|
||||
private static void _CollectRpcPropertyRegistry(ImmutableList<(PropertyAccessor<string> prop, string name)> items)
|
||||
{
|
||||
foreach (var (prop, name) in items)
|
||||
{
|
||||
AddProperty(new RpcProperty(
|
||||
name,
|
||||
() => prop.Value,
|
||||
value => prop.Value = value ?? "",
|
||||
prop.CanSet
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
public const string PipePrefix = "PCLCE_RPC";
|
||||
|
||||
private static readonly string _EchoPipeName = $"{PipePrefix}@{Basics.CurrentProcess.Id}";
|
||||
private static readonly string[] _RequestTypeArray = ["GET", "SET", "REQ"];
|
||||
private static readonly HashSet<string> _RequestType = [.._RequestTypeArray];
|
||||
|
||||
#region Property
|
||||
|
||||
private static readonly Dictionary<string, RpcProperty> _PropertyMap = new();
|
||||
|
||||
/// <summary>
|
||||
/// 添加一个新的 RPC 属性,若有多个使用 foreach 即可
|
||||
/// </summary>
|
||||
/// <param name="prop">要添加的属性</param>
|
||||
/// <returns>是否成功添加(若已存在相同名称的属性则无法添加)</returns>
|
||||
public static bool AddProperty(RpcProperty prop) => _PropertyMap.TryAdd(prop.Name, prop);
|
||||
|
||||
/// <summary>
|
||||
/// 通过指定的名称删除已存在的 RPC 属性
|
||||
/// </summary>
|
||||
/// <param name="name">属性名称</param>
|
||||
/// <returns>是否成功删除(若不存在该名称则无法删除)</returns>
|
||||
public static bool RemoveProperty(string name)
|
||||
{
|
||||
return _PropertyMap.Remove(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除已存在的 RPC 属性,实质上仍然是通过属性的名称删除,但会检查是否是同一个对象
|
||||
/// </summary>
|
||||
/// <param name="prop">要删除的属性</param>
|
||||
/// <returns></returns>
|
||||
public static bool RemoveProperty(RpcProperty prop)
|
||||
{
|
||||
var key = prop.Name;
|
||||
var result = _PropertyMap.TryGetValue(key, out var value);
|
||||
if (!result || value != prop) return false;
|
||||
_PropertyMap.Remove(key);
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Function
|
||||
|
||||
private static readonly Dictionary<string, RpcFunction> _FunctionMap = new() {
|
||||
["ping"] = ((_, _, _) => RpcResponse.EmptySuccess),
|
||||
["activate"] = ((_, _, _) =>
|
||||
{
|
||||
if (Lifecycle.CurrentState >= LifecycleState.WindowCreated) ActivateMainWindow();
|
||||
else Lifecycle.When(LifecycleState.WindowCreated, ActivateMainWindow);
|
||||
return RpcResponse.EmptySuccess;
|
||||
|
||||
void ActivateMainWindow()
|
||||
{
|
||||
var app = Lifecycle.CurrentApplication;
|
||||
app.Dispatcher.BeginInvoke(() =>
|
||||
{
|
||||
var window = app.MainWindow!;
|
||||
if (window.WindowState == WindowState.Minimized) window.WindowState = WindowState.Normal;
|
||||
if (!window.Topmost)
|
||||
{
|
||||
window.Topmost = true;
|
||||
window.Topmost = false;
|
||||
}
|
||||
window.Activate();
|
||||
});
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 添加一个新的 RPC 函数,若有多个使用 foreach 即可
|
||||
/// </summary>
|
||||
/// <param name="name">函数名称</param>
|
||||
/// <param name="func">函数过程</param>
|
||||
/// <returns>是否成功添加(若已存在相同名称的函数则无法添加)</returns>
|
||||
public static bool AddFunction(string name, RpcFunction func) => _FunctionMap.TryAdd(name, func);
|
||||
|
||||
/// <summary>
|
||||
/// 通过指定的名称删除已存在的 RPC 函数
|
||||
/// </summary>
|
||||
/// <param name="name">函数名称</param>
|
||||
/// <returns>是否成功删除(若不存在该名称则无法删除)</returns>
|
||||
public static bool RemoveFunction(string name)
|
||||
{
|
||||
return _FunctionMap.Remove(name);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static bool _EchoPipeCallback(StreamReader reader, StreamWriter writer, Process? client)
|
||||
{
|
||||
try
|
||||
{
|
||||
// GET/SET/REQ [target]
|
||||
// [content]
|
||||
var header = reader.ReadLine(); // 读入请求头
|
||||
Context.Info($"客户端请求: {header}");
|
||||
|
||||
var args = header?.Split([' '], 2) ?? []; // 分离请求类型和参数
|
||||
if (args.Length < 2 || args[1].Length == 0) throw new RpcException("请求参数过少");
|
||||
var type = args[0].ToUpperInvariant();
|
||||
if (!_RequestType.Contains(type)) throw new RpcException($"请求类型必须为 {string.Join("/", _RequestTypeArray)} 其中之一");
|
||||
var target = args[1];
|
||||
|
||||
// 读入请求内容(可能没有)
|
||||
var buffer = new StringBuilder();
|
||||
var tmp = reader.Read();
|
||||
while (tmp != PipeComm.PipeEndingChar)
|
||||
{
|
||||
buffer.Append((char)tmp);
|
||||
tmp = reader.Read();
|
||||
}
|
||||
var content = buffer.Length == 0 ? null : buffer.ToString();
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case "GET": case "SET": {
|
||||
target = target.ToLowerInvariant();
|
||||
var result = _PropertyMap.TryGetValue(target, out var prop);
|
||||
if (!result) throw new RpcException($"不存在属性 {target}");
|
||||
RpcResponse response;
|
||||
if (type == "GET")
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = prop!.Value;
|
||||
response = new RpcResponse(RpcResponseStatus.Success, RpcResponseType.Text, value, target);
|
||||
Context.Trace($"返回值: {value}");
|
||||
}
|
||||
catch (RpcPropertyOperationFailedException)
|
||||
{
|
||||
response = RpcResponse.EmptyFailure;
|
||||
Context.Debug("设置失败: 只写属性或请求被拒绝");
|
||||
}
|
||||
}
|
||||
else if (prop!.Settable)
|
||||
{
|
||||
try
|
||||
{
|
||||
prop.Value = content;
|
||||
response = RpcResponse.EmptySuccess;
|
||||
Context.Trace($"设置成功: {content}");
|
||||
}
|
||||
catch (RpcPropertyOperationFailedException)
|
||||
{
|
||||
response = RpcResponse.EmptyFailure;
|
||||
Context.Debug("设置失败: 请求被拒绝");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
response = RpcResponse.EmptyFailure;
|
||||
Context.Debug("设置失败: 只读属性");
|
||||
}
|
||||
response.Response(writer);
|
||||
break;
|
||||
}
|
||||
|
||||
case "REQ": {
|
||||
var targetArgs = target.Split([' '], 2); // 分离函数名和参数
|
||||
var name = targetArgs[0].ToLowerInvariant();
|
||||
var indent = false; // 检测缩进指示
|
||||
if (name.EndsWith('$'))
|
||||
{
|
||||
indent = true;
|
||||
name = name[..^1];
|
||||
}
|
||||
var result = _FunctionMap.TryGetValue(name, out var func);
|
||||
if (!result) throw new RpcException($"不存在函数 {name}");
|
||||
string? argument = null;
|
||||
if (targetArgs.Length > 1)
|
||||
argument = targetArgs[1];
|
||||
Context.Trace($"正在调用函数 {name} {argument}");
|
||||
var response = func!(argument, content, indent);
|
||||
response.Response(writer);
|
||||
Context.Trace($"函数已退出,返回状态 {response.Status}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is RpcException rpcEx)
|
||||
{
|
||||
var reason = rpcEx.Reason;
|
||||
RpcResponse.Err(reason).Response(writer);
|
||||
Context.Info($"出错: {reason}");
|
||||
}
|
||||
else
|
||||
{
|
||||
RpcResponse.Err(ex.ToString(), "stacktrace").Response(writer);
|
||||
Context.Error("处理请求时发生异常", ex);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user