初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.IO.Pipes;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using PCL.Core.App.IoC;
|
||||
using PCL.Core.IO;
|
||||
using PCL.Core.Logging;
|
||||
using PCL.Core.Utils.OS;
|
||||
using PCL.Core.Utils;
|
||||
|
||||
namespace PCL.Core.App.Essentials;
|
||||
|
||||
public delegate string? PromoteOperationFunction(string? arg);
|
||||
|
||||
/// <summary>
|
||||
/// 标记一个方法,使其能够被提权进程调用,方法签名需符合 <see cref="PromoteOperationFunction"/>。
|
||||
/// </summary>
|
||||
/// <param name="name">提权操作名</param>
|
||||
[DependencyCollector<PromoteOperationFunction>("promote", AttributeTargets.Method)]
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class PromoteOperationAttribute(string name) : Attribute;
|
||||
|
||||
[LifecycleService(LifecycleState.BeforeLoading, Priority = -10)]
|
||||
[LifecycleScope("promote", "提权服务", false)]
|
||||
public sealed partial class PromoteService
|
||||
{
|
||||
private static Process? _promoteProcess;
|
||||
private static NamedPipeServerStream? _promotePipeServer;
|
||||
|
||||
private static readonly ConcurrentQueue<PromoteOperation> _PendingOperations = [];
|
||||
|
||||
private readonly record struct PromoteOperation(string Command, Action<string?>? Callback, bool DetailLog);
|
||||
|
||||
/// <summary>
|
||||
/// 提权进程是否正在运行。
|
||||
/// </summary>
|
||||
public static bool IsPromoteProcessRunning => _promoteProcess is not null;
|
||||
|
||||
/// <summary>
|
||||
/// 当前进程是否是提权进程。
|
||||
/// </summary>
|
||||
public static bool IsCurrentProcessPromoted { get; private set; }
|
||||
|
||||
private static string _GetPromotePipeName(int processId) => $"PCLCE_PM@{processId}";
|
||||
|
||||
private static readonly Dictionary<string, PromoteOperationFunction> _OperationFunctions = new();
|
||||
|
||||
/// <summary>
|
||||
/// 添加提权操作,仅在提权进程中有效。
|
||||
/// </summary>
|
||||
/// <param name="name">操作名</param>
|
||||
/// <param name="operation">操作实现,接收参数并返回结果,返回值会被自动压缩为单行</param>
|
||||
/// <returns>是否添加成功,若在主进程中调用或已存在相同操作名,则为 <c>false</c></returns>
|
||||
public static bool AddOperationFunction(string name, PromoteOperationFunction operation)
|
||||
{
|
||||
return IsCurrentProcessPromoted && _OperationFunctions.TryAdd(name, operation);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加自动将参数 JSON 反序列化的提权操作,仅在提权进程中有效。
|
||||
/// </summary>
|
||||
/// <param name="name">操作名</param>
|
||||
/// <param name="operation">操作实现,接收反序列化的并返回结果,返回值会被自动压缩为单行</param>
|
||||
/// <typeparam name="TValue">反序列化的目标类型</typeparam>
|
||||
/// <returns>是否添加成功,若在主进程中调用或已存在相同操作名,则为 <c>false</c></returns>
|
||||
public static bool AddJsonOperationFunction<TValue>(string name, Func<TValue?, string?> operation)
|
||||
{
|
||||
return AddOperationFunction(name, arg =>
|
||||
{
|
||||
if (arg is null) return OperationErrEmpty;
|
||||
var obj = JsonSerializer.Deserialize<TValue>(arg, JsonCompat.SerializerOptions);
|
||||
return operation(obj);
|
||||
});
|
||||
}
|
||||
|
||||
private const string OperationErrNotFound = "ERR_OPERATION_NOT_FOUND";
|
||||
private const string OperationErrInvalidArgument = "ERR_ILLEGAL_ARGUMENT";
|
||||
private const string OperationErrExceptionThrown = "ERR_UNHANDLED_EXCEPTION";
|
||||
private const string OperationErrEmpty = "ERR_EMPTY";
|
||||
|
||||
/// <summary>
|
||||
/// 提权进程接收到操作请求时触发的事件,接收一个字符串作为操作命令并返回一个字符串作为结果。<br/>
|
||||
/// <b>注意:如果你不知道这是做什么的,请勿覆盖默认实现。</b>请使用 <see cref="AddOperationFunction"/>。
|
||||
/// </summary>
|
||||
public static Func<string, string?> Operate {
|
||||
private get => field ??= command =>
|
||||
{
|
||||
var split = command.Split([' '], 2);
|
||||
_OperationFunctions.TryGetValue(split[0], out var operation);
|
||||
if (operation is null) return OperationErrNotFound;
|
||||
try
|
||||
{
|
||||
return operation(split.Length > 1 ? split[1] : null) ?? OperationErrEmpty;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Context.Warn("操作出错", ex);
|
||||
return OperationErrExceptionThrown;
|
||||
}
|
||||
};
|
||||
set;
|
||||
} = null!;
|
||||
|
||||
private static string _ShortenString(string str)
|
||||
{
|
||||
#if TRACE
|
||||
const int maxLength = 40;
|
||||
#else
|
||||
const int maxLength = 15;
|
||||
#endif
|
||||
if (str.Length <= maxLength) return str;
|
||||
return str[..maxLength] + "...";
|
||||
}
|
||||
|
||||
// 提权进程: 连接管道开始通信
|
||||
private static void _PerformAsPromoteProcess(string pid)
|
||||
{
|
||||
Context.Info("正在连接提权通信管道");
|
||||
var process = Process.GetProcessById(int.Parse(pid));
|
||||
// 验证来源
|
||||
var mainProcessPath = Path.GetFullPath(process.MainModule!.FileName);
|
||||
if (!string.Equals(mainProcessPath, Basics.ExecutablePath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Context.Error("来源验证失败,正在退出");
|
||||
return;
|
||||
}
|
||||
// 连接管道
|
||||
var pipeName = _GetPromotePipeName(process.Id);
|
||||
var pipe = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut);
|
||||
pipe.Connect(10000);
|
||||
Context.Info("已连接,开始通信");
|
||||
var reader = new StreamReader(pipe);
|
||||
var writer = new StreamWriter(pipe);
|
||||
while (true)
|
||||
{
|
||||
var command = reader.ReadLine();
|
||||
if (string.IsNullOrEmpty(command))
|
||||
{
|
||||
Context.Info("管道已关闭,正在退出");
|
||||
break;
|
||||
}
|
||||
Context.Debug($"正在执行: {_ShortenString(command)}");
|
||||
var result = Operate(command) ?? OperationErrEmpty;
|
||||
Context.Trace($"返回结果: {_ShortenString(result)}");
|
||||
writer.WriteLine(result.Replace("\r\n", " ").Replace('\n', ' ').Replace('\r', ' '));
|
||||
writer.Flush();
|
||||
Context.Trace("返回成功");
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly AutoResetEvent _ActivateEvent = new(false);
|
||||
|
||||
// 主进程: 管道连接回调
|
||||
private static bool _PromotePipeCallback(StreamReader reader, StreamWriter writer, Process? client)
|
||||
{
|
||||
while (IsPromoteProcessRunning)
|
||||
{
|
||||
if (!_PendingOperations.TryDequeue(out var operation))
|
||||
{
|
||||
_ActivateEvent.WaitOne();
|
||||
continue;
|
||||
}
|
||||
var command = operation.Command.Replace("\r\n", " ").Replace('\n', ' ').Replace('\r', ' ');
|
||||
var commandLog = operation.DetailLog ? command : _ShortenString(command);
|
||||
Context.Debug($"正在执行: {commandLog}");
|
||||
writer.WriteLine(command);
|
||||
writer.Flush();
|
||||
var result = reader.ReadLine();
|
||||
if (result is null)
|
||||
{
|
||||
Context.Warn("管道输入流已结束");
|
||||
break;
|
||||
}
|
||||
var resultLog = operation.DetailLog ? result : _ShortenString(result);
|
||||
Context.Trace($"执行结果: {resultLog}");
|
||||
if (result == OperationErrEmpty) result = null;
|
||||
operation.Callback?.Invoke(result);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 主进程: 初始化提权后台服务
|
||||
private static bool _StartPromoteProcess()
|
||||
{
|
||||
// 启动提权进程
|
||||
_promoteProcess = ProcessInterop.Start(
|
||||
Basics.ExecutablePath, $"promote {Basics.CurrentProcessId}", true);
|
||||
if (_promoteProcess is null)
|
||||
{
|
||||
Context.Warn("提权进程启动失败");
|
||||
return false;
|
||||
}
|
||||
_promoteProcess.Exited += (_, _) => _promoteProcess = null;
|
||||
// 启动提权通信管道服务端
|
||||
_promotePipeServer ??= PipeComm.StartPipeServer(
|
||||
"Promote", _GetPromotePipeName(Basics.CurrentProcessId), _PromotePipeCallback,
|
||||
() => _promotePipeServer = null, true, [_promoteProcess.Id]);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 向等待区添加操作。
|
||||
/// </summary>
|
||||
/// <param name="command">操作命令</param>
|
||||
/// <param name="callback">结果返回后的回调</param>
|
||||
/// <param name="detailLog">指定是否打印详细日志,若为 <c>false</c>,则日志仅保留前 40 或 15 字符(取决于是否为调试构建)</param>
|
||||
public static void Append(string command, Action<string?>? callback = null, bool detailLog = true)
|
||||
{
|
||||
_PendingOperations.Enqueue(new PromoteOperation(command, callback, detailLog));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试启动提权进程并开始执行操作。
|
||||
/// </summary>
|
||||
/// <returns>是否成功开始执行,若提权进程启动失败则为 <c>false</c></returns>
|
||||
public static bool Activate()
|
||||
{
|
||||
if (!IsPromoteProcessRunning && !_StartPromoteProcess())
|
||||
{
|
||||
_PendingOperations.Clear();
|
||||
return false;
|
||||
}
|
||||
_ActivateEvent.Set();
|
||||
return true;
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, Process> _RunningProcesses = new();
|
||||
|
||||
// name: kill
|
||||
// arg: process-id [timeout]
|
||||
// return: kill result (false over timeout)
|
||||
[PromoteOperation("kill")]
|
||||
public static string? KillProcess(string? arg)
|
||||
{
|
||||
if (arg is null) return OperationErrInvalidArgument;
|
||||
var split = arg.Split(' ');
|
||||
if (!_RunningProcesses.TryGetValue(split[0], out var process)) return null;
|
||||
process.Kill();
|
||||
if (split.Length > 1)
|
||||
{
|
||||
int.TryParse(split[1], out var timeout);
|
||||
return process.WaitForExit(timeout).ToString();
|
||||
}
|
||||
process.WaitForExit();
|
||||
return true.ToString();
|
||||
}
|
||||
|
||||
// name: start
|
||||
// arg: path\to\executable[.] ; arguments
|
||||
// return: process id
|
||||
[PromoteOperation("start")]
|
||||
public static string? StartProcess(string? arg)
|
||||
{
|
||||
if (arg is null) return OperationErrInvalidArgument;
|
||||
var split = arg.Split([" ; "], 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
var createNoWindow = false;
|
||||
if (split[0].EndsWith('.'))
|
||||
{
|
||||
split[0] = split[0][..^1];
|
||||
createNoWindow = true;
|
||||
}
|
||||
var psi = new ProcessStartInfo(split[0]);
|
||||
if (createNoWindow)
|
||||
{
|
||||
psi.CreateNoWindow = true;
|
||||
psi.UseShellExecute = false;
|
||||
psi.RedirectStandardInput = true;
|
||||
psi.RedirectStandardOutput = true;
|
||||
psi.RedirectStandardError = true;
|
||||
}
|
||||
if (split.Length > 1) psi.Arguments = split[1];
|
||||
return _StartProcessWithInfo(psi);
|
||||
}
|
||||
|
||||
// name: start-json
|
||||
// arg: {...}
|
||||
// return: process id
|
||||
private static string? _StartProcessWithInfo(ProcessStartInfo? info)
|
||||
{
|
||||
if (info is null) return OperationErrInvalidArgument;
|
||||
var process = Process.Start(info);
|
||||
if (process is null) return null;
|
||||
var id = process.Id.ToString();
|
||||
process.Exited += (_, _) => _RunningProcesses.Remove(id);
|
||||
_RunningProcesses[id] = process;
|
||||
return id;
|
||||
}
|
||||
|
||||
[DependencyInjectionPoint("promote", false)]
|
||||
private static void _CollectOperationFunction(PromoteOperationFunction operation, string name)
|
||||
=> AddOperationFunction(name, operation);
|
||||
|
||||
[LifecycleStart]
|
||||
private static void _Start()
|
||||
{
|
||||
var args = Basics.CommandLineArguments;
|
||||
if (args is ["promote", _])
|
||||
{
|
||||
Context.Info("当前进程为提权进程");
|
||||
IsCurrentProcessPromoted = true;
|
||||
// 预定义操作
|
||||
Context.Info("正在加载提权操作");
|
||||
_CollectOperationFunction_InvokeInjection_Promote();
|
||||
AddJsonOperationFunction<ProcessStartInfo>("start-json", _StartProcessWithInfo);
|
||||
// 结束生命周期管理,启动提权操作线程
|
||||
Lifecycle.PendingLogFileName = "LastPending_Promote.log";
|
||||
LogWrapper.OnLog += (level, msg, module, ex) => Context.CustomLog($"[{module}] {msg}", ex, level);
|
||||
Context.Info("已接管通用日志");
|
||||
Context.Info("正在启动服务线程");
|
||||
new Thread(() => _PerformAsPromoteProcess(args[1])) { Name = "Promote" }.Start();
|
||||
Context.RequestStopLoading();
|
||||
Context.DeclareStopped();
|
||||
}
|
||||
else
|
||||
{
|
||||
Context.Info("当前进程为主进程");
|
||||
IsCurrentProcessPromoted = false;
|
||||
// TODO 提权进程自动启动
|
||||
}
|
||||
}
|
||||
|
||||
[LifecycleStop]
|
||||
private static void _Stop()
|
||||
{
|
||||
if (_promotePipeServer is not null)
|
||||
{
|
||||
Context.Debug("正在结束提权管道服务");
|
||||
_promotePipeServer.Dispose();
|
||||
}
|
||||
if (_promoteProcess is not null && !_promoteProcess.WaitForExit(3000))
|
||||
{
|
||||
Context.Debug("正在结束提权进程");
|
||||
ProcessInterop.Kill(_promoteProcess, 0, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user