CI / Go Backend (push) Canceled after 0s
初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器 HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块 Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚 AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
341 lines
13 KiB
C#
341 lines
13 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|