feat: 项目初始化 + 3D方块世界原型 + AI助搭系统
CI / Go Backend (push) Canceled after 0s

初始化 monorepo: Go后端(7微服务) + Unity客户端(9模块) + 启动器

HTML5原型: Three.js 3D体素世界, Perlin噪声地形, 原版材质, 22种方块

Minecraft创造模式背包: 双栏布局, 拖拽移动物品, 方向性元件引脚

AI助搭策划文档 + 客户端/服务端骨架 + Docker Compose + CI
This commit is contained in:
xyou
2026-08-08 14:07:56 +08:00
parent 9500c4c80a
commit f70b061d1a
1972 changed files with 159760 additions and 6 deletions
@@ -0,0 +1,28 @@
using Microsoft.Win32;
namespace PCL.Core.Utils.OS;
public static class AumidHelper
{
public const string Aumid = "PCLCommunity.PCLCE";
public static bool HasAumid()
{
using var key = Registry.CurrentUser.OpenSubKey(string.Concat(@"Software\Classes\AppUserModelId\", Aumid));
return key is not null;
}
public static void RegisterAumid()
{
// .NET 8 在正常情况下不可能返回 null,如果炸了不应该包住而是让他炸下去
using var key = Registry.CurrentUser.CreateSubKey(string.Concat(@"Software\Classes\AppUserModelId\", Aumid));
key.SetValue("DisplayName", "Plain Craft Launcher Community Edition");
key.SetValue("IconUri", IconHelper.GetIconPath());
key.SetValue("IconBackgroundColor", "FFDDDD");
}
public static void UnregisterAumid()
{
Registry.CurrentUser.DeleteSubKey(string.Concat(@"Software\Classes\AppUserModelId\", Aumid), false);
}
}
@@ -0,0 +1,20 @@
namespace PCL.Core.Utils.OS;
using System;
using System.Windows;
public static class ClipboardUtils {
/// <summary>
/// 将剪贴板内容设置为用于复制/粘贴操作的文件或文件夹路径列表。
/// </summary>
/// <param name="paths">要设置到剪贴板的文件或文件夹路径数组。</param>
public static void SetClipboardFiles(string[] paths) {
if (paths is null || paths.Length == 0) {
throw new ArgumentException("Paths cannot be null or empty.", nameof(paths));
}
var dataObject = new DataObject();
dataObject.SetData(DataFormats.FileDrop, paths);
Clipboard.SetDataObject(dataObject);
}
}
@@ -0,0 +1,202 @@
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;
namespace PCL.Core.Utils.OS;
// ReSharper disable InconsistentNaming
public partial class DragHelper
{
public event EventHandler? DragDrop;
public string[]? DropFilePaths { get; private set; }
public Point DropDragPoint { get; private set; }
public HwndSource? HwndSource { get; set; }
#region Public API
public void AddHook()
{
if (HwndSource is null)
throw new InvalidOperationException("HwndSource 未设置");
RemoveHook();
HwndSource.AddHook(WndProc);
IntPtr hwnd = HwndSource.Handle;
if (IsUserAnAdmin())
RevokeDragDrop(hwnd);
DragAcceptFiles(hwnd, true);
ChangeMessageFilter(hwnd);
}
public void RemoveHook()
{
if (HwndSource is null)
return;
HwndSource.RemoveHook(WndProc);
DragAcceptFiles(HwndSource.Handle, false);
}
#endregion
#region WndProc
private IntPtr WndProc(
IntPtr hwnd,
int msg,
IntPtr wParam,
IntPtr lParam,
ref bool handled)
{
if (TryGetDropInfo(msg, wParam, out var files, out var pt))
{
DropFilePaths = files;
DropDragPoint = new Point(pt.X, pt.Y);
DragDrop?.Invoke(this, EventArgs.Empty);
handled = true;
}
return IntPtr.Zero;
}
#endregion
#region Message filter (UAC)
private static unsafe void ChangeMessageFilter(IntPtr hwnd)
{
var ver = Environment.OSVersion.Version;
if (ver < new Version(6, 0))
return;
var win7OrHigher = ver >= new Version(6, 1);
var filter = new CHANGEFILTERSTRUCT
{
cbSize = (uint)sizeof(CHANGEFILTERSTRUCT)
};
uint[] messages = [
WM_DROPFILES,
WM_COPYGLOBALDATA,
WM_COPYDATA
];
foreach (var msg in messages)
{
var ok = win7OrHigher
? ChangeWindowMessageFilterEx(hwnd, msg, MSGFLT_ALLOW, ref filter)
: ChangeWindowMessageFilter(msg, MSGFLT_ADD);
if (!ok) throw new Win32Exception(Marshal.GetLastWin32Error());
}
}
#endregion
#region Drop parsing
private static bool TryGetDropInfo(
int msg,
IntPtr hDrop,
out string[]? filePaths,
out DragPoint dropPoint)
{
filePaths = null;
dropPoint = default;
if (msg != WM_DROPFILES)
return false;
var count = DragQueryFile(hDrop, uint.MaxValue, IntPtr.Zero, 0);
filePaths = new string[count];
const int maxPath = 32768, smallerMaxPath = 1024;
Span<char> gBuffer = stackalloc char[smallerMaxPath];
for (uint i = 0; i < count; i++)
{
var len = DragQueryFile(hDrop, i, IntPtr.Zero, 0) + 1;
if (len > maxPath) len = maxPath;
var buffer = len <= smallerMaxPath ? gBuffer[..(int)len] : new char[len];
_ = DragQueryFile(hDrop, i, buffer, len);
filePaths[i] = new string(buffer[..(int)(len - 1)]);
}
DragFinish(hDrop);
return true;
}
#endregion
#region Win32
private const uint WM_COPYGLOBALDATA = 0x0049;
private const uint WM_COPYDATA = 0x004A;
private const uint WM_DROPFILES = 0x0233;
private const uint MSGFLT_ALLOW = 1;
private const uint MSGFLT_ADD = 1;
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool ChangeWindowMessageFilter(
uint msg,
uint flags);
[LibraryImport("user32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool ChangeWindowMessageFilterEx(
IntPtr hwnd,
uint msg,
uint action,
ref CHANGEFILTERSTRUCT filter);
[LibraryImport("shell32.dll")]
private static partial void DragAcceptFiles(
IntPtr hwnd,
[MarshalAs(UnmanagedType.Bool)] bool accept);
[LibraryImport("shell32.dll", EntryPoint = "DragQueryFileW", StringMarshalling = StringMarshalling.Utf16)]
private static partial uint DragQueryFile(IntPtr hDrop, uint iFile, Span<char> lpszFile, uint cch);
[LibraryImport("shell32.dll", EntryPoint = "DragQueryFileW")]
private static partial uint DragQueryFile(IntPtr hDrop, uint iFile, IntPtr lpszFile, uint cch);
[LibraryImport("shell32.dll")]
private static partial void DragFinish(IntPtr hDrop);
[LibraryImport("ole32.dll")]
private static partial int RevokeDragDrop(IntPtr hwnd);
[LibraryImport("shell32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool IsUserAnAdmin();
#endregion
#region Structs
[StructLayout(LayoutKind.Sequential)]
private struct DragPoint
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential)]
private struct CHANGEFILTERSTRUCT
{
public uint cbSize;
public uint ExtStatus;
}
#endregion
}
@@ -0,0 +1,60 @@
using System;
using PCL.Core.Logging;
using PCL.Core.Utils.Exts;
namespace PCL.Core.Utils.OS;
public static partial class EnvironmentInterop
{
private const string LogModule = "Environment";
/// <summary>
/// 读取环境变量并使用 <see cref="StringExtension.Convert{T}"/> 将其转换为指定类型并写入目标引用。
/// </summary>
/// <param name="key">环境变量名</param>
/// <param name="target">需要写入的目标引用 (不存在该环境变量或转换失败时不会写入)</param>
/// <param name="detailLog">是否在日志中输出变量值</param>
/// <typeparam name="TValue">目标引用的类型</typeparam>
/// <returns>是否成功写入目标引用</returns>
public static bool ReadVariable<TValue>(string key, ref TValue target, bool detailLog = true)
{
var envValue = Environment.GetEnvironmentVariable(key);
if (envValue is null) return false;
var valueLog = detailLog ? $" = {envValue}" : string.Empty;
LogWrapper.Debug(LogModule, $"读取到环境变量 {key}{valueLog}");
var value = envValue.Convert<TValue>();
if (value is null)
{
LogWrapper.Warn(LogModule, $"环境变量 {key} 类型转换失败");
return false;
}
target = value;
return true;
}
public static string? GetSecret(string key, bool readEnv = true, bool readEnvDebugOnly = false)
{
if (!SecretDictionary.TryGetValue(key, out var value) &&
readEnv &&
#if !DEBUG
!readEnvDebugOnly &&
#endif
ReadVariable($"PCL_{key}", ref value, false)
) SecretDictionary[key] = value;
return value;
}
/// <summary>
/// 获取当前操作系统名称。
/// </summary>
/// <returns>返回小写的操作系统名称,如 "windows", "linux", "osx"。</returns>
public static string GetCurrentOsName() {
if (OperatingSystem.IsWindows())
return "windows";
if (OperatingSystem.IsLinux())
return "linux";
return OperatingSystem.IsMacOS()
? "osx"
: "unknown";
}
}
@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Management;
using PCL.Core.Logging;
namespace PCL.Core.Utils.OS;
public static class HardwareInfo
{
private static readonly object _Lock = new();
/// <summary>
/// 系统 CPU 信息
/// </summary>
public static string CPUName = "Unknown";
/// <summary>
/// 系统 GPU 信息
/// </summary>
public static IReadOnlyList<GPUInfo> GPUs { get; private set; } = [];
/// <summary>
/// 已安装物理内存大小,单位 MiB
/// </summary>
public static long SystemMemorySize = (long)KernelInterop.GetPhysicalMemoryBytes().Total / 1024 / 1024;
public readonly record struct GPUInfo(string Name, string DriverVersion, long Memory);
/// <summary>
/// 获取系统信息,例如 CPU 与 GPU,并存储到 CPUName 和 GPUs
/// </summary>
public static void GetHardwareInfo()
{
// CPU
var cpuName = (string?)null;
try
{
using var searcher = new ManagementObjectSearcher(@"root\CIMV2", "SELECT * FROM Win32_Processor");
foreach (ManagementObject queryObj in searcher.Get())
{
cpuName = queryObj["Name"]?.ToString()?.Trim();
break;
}
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "获取 CPU 信息时出错");
}
// GPU
var gpuList = new List<GPUInfo>();
try
{
using var searcher =
new ManagementObjectSearcher(@"root\CIMV2", "SELECT * FROM Win32_VideoController");
foreach (ManagementObject queryObj in searcher.Get())
{
var gpuInfo = new GPUInfo
{
Name = queryObj["Name"]?.ToString() ?? "",
DriverVersion = queryObj["DriverVersion"]?.ToString() ?? "",
Memory = queryObj["AdapterRAM"] is not null and not DBNull
? Convert.ToInt64(queryObj["AdapterRAM"]) / (1024 * 1024)
: 0
};
gpuList.Add(gpuInfo);
}
}
catch (Exception ex)
{
LogWrapper.Warn(ex, "获取 GPU 信息时出错");
}
lock (_Lock)
{
if (cpuName is not null)
CPUName = cpuName;
if (gpuList.Count > 0)
GPUs = gpuList;
}
LogWrapper.Info("已获取系统硬件信息");
}
}
@@ -0,0 +1,269 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
namespace PCL.Core.Utils.OS;
public static partial class KernelInterop
{
// ReSharper disable InconsistentNaming, UnusedMember.Local
[LibraryImport("kernel32.dll", EntryPoint = "GetCurrentThreadId", SetLastError = true)]
private static partial uint _GetCurrentThreadId();
[LibraryImport("kernel32.dll", EntryPoint = "ExitProcess", SetLastError = false)]
private static partial void _ExitProcess(uint statusCode);
[LibraryImport("kernel32.dll", EntryPoint = "GetNamedPipeClientProcessId", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool _GetNamedPipeClientProcessId(IntPtr pipeHandle, out uint clientProcessId);
[LibraryImport("kernel32.dll", EntryPoint = "GetLogicalProcessorInformationEx", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool _GetLogicalProcessorInformationEx(
LOGICAL_PROCESSOR_RELATIONSHIP relationshipType,
IntPtr buffer,
ref uint returnLength);
private const int ERROR_INSUFFICIENT_BUFFER = 122;
private enum LOGICAL_PROCESSOR_RELATIONSHIP : uint
{
RelationProcessorCore = 0,
RelationNumaNode = 1,
RelationCache = 2,
RelationProcessorPackage = 3,
RelationGroup = 4,
RelationAll = 0xffff
}
private static MEMORYSTATUSEX CreateStatus() => new() { dwLength = (uint)Marshal.SizeOf<MEMORYSTATUSEX>() };
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX lpBuffer);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct MEMORYSTATUSEX
{
public uint dwLength;
public uint dwMemoryLoad;
public ulong ullTotalPhys;
public ulong ullAvailPhys;
public ulong ullTotalPageFile;
public ulong ullAvailPageFile;
public ulong ullTotalVirtual;
public ulong ullAvailVirtual;
public ulong ullAvailExtendedVirtual;
}
private const int ERROR_ACCESS_DENIED = 5;
[LibraryImport("kernel32.dll", EntryPoint = "AllocConsole")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool _AllocConsole();
[LibraryImport("kernel32.dll", EntryPoint = "FreeConsole")]
private static partial void _FreeConsole();
[LibraryImport("kernel32.dll", EntryPoint = "GetConsoleWindow")]
private static partial nint _GetConsoleWindow();
// ReSharper restore InconsistentNaming, UnusedMember.Local
private static void _ThrowLastWin32Error(int? errorCode = null) => throw new Win32Exception(errorCode ?? Marshal.GetLastWin32Error());
/// <summary>
/// 获取当前线程的 Win32 Thread ID。若无特殊情况请用 <see cref="Thread.ManagedThreadId"/> 而不是这个方法。
/// </summary>
public static uint CurrentNativeThreadId => _GetCurrentThreadId();
/// <summary>
/// 直接结束当前进程。若无特殊情况请使用 <see cref="PCL.Core.App.IoC.Lifecycle.Shutdown"/>
/// </summary>
/// <param name="statusCode">退出状态码 (返回值)</param>
public static void ExitProcess(int statusCode = 0) => _ExitProcess((uint)statusCode);
/// <summary>
/// 获取指定命名管道当前连接的客户端进程 ID
/// </summary>
/// <param name="pipeHandle">命名管道句柄</param>
/// <returns>获取到的进程 ID</returns>
public static uint GetNamedPipeClientProcessId(IntPtr pipeHandle)
{
if (!_GetNamedPipeClientProcessId(pipeHandle, out var clientProcessId)) _ThrowLastWin32Error();
return clientProcessId;
}
/// <summary>
/// 获取仅包含性能核(P-core)的逻辑处理器数量。
/// 在不支持 EfficiencyClass(旧 OS 或非混合架构)时,会退回到 Environment.ProcessorCount。
/// </summary>
public static int GetPerformanceLogicalProcessorCount()
{
var cores = QueryProcessorCoreRelationships();
if (cores.Count == 0)
{
// 不支持 EfficiencyClass
return Environment.ProcessorCount;
}
// 原理:性能核的 EfficiencyClass 一定比能效核大
var maxEff = cores.Max(c => c.EfficiencyClass);
// 统计所有效率等级为 maxEff 的核心的掩码位数
return cores
.Where(c => c.EfficiencyClass == maxEff)
.Sum(c => CountSetBits(c.Mask));
static int CountSetBits(ulong v)
{
var cnt = 0;
while (v != 0)
{
cnt += (int)(v & 1);
v >>= 1;
}
return cnt;
}
}
/// <summary>
/// 仅承载 EfficiencyClass 和 Mask 的简单 CPU 核心信息
/// </summary>
public sealed record ProcessorCore(byte EfficiencyClass, ulong Mask);
/// <summary>
/// 枚举 RelationProcessorCore 返回的所有物理核心关系信息
/// </summary>
// Partly generated by o4-mini-high (20250709)
public static List<ProcessorCore> QueryProcessorCoreRelationships()
{
uint returnedLength = 0;
// 第一次调用仅为了获取所需缓冲区大小
if (!_GetLogicalProcessorInformationEx(
LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore,
IntPtr.Zero,
ref returnedLength)
&& Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
var list = new List<ProcessorCore>();
var buffer = Marshal.AllocHGlobal((int)returnedLength);
try
{
if (!_GetLogicalProcessorInformationEx(
LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore,
buffer,
ref returnedLength))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
var ptr = buffer;
var end = IntPtr.Add(buffer, (int)returnedLength);
// SYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX 头部:Relationship (4 字节) + Size (4 字节)
const int headerSize = sizeof(uint) + sizeof(uint);
// GROUP_AFFINITY 大小 = KAFFINITY (平台指针大小) + WORD Group + WORD[3] Reserved
var groupAffinitySize = IntPtr.Size + 8;
while (ptr.ToInt64() < end.ToInt64())
{
var relationship = (uint)Marshal.ReadInt32(ptr);
var size = (uint)Marshal.ReadInt32(ptr, sizeof(uint));
if (relationship == (uint)LOGICAL_PROCESSOR_RELATIONSHIP.RelationProcessorCore)
{
// PROCESSOR_RELATIONSHIP 结构:
// Flags BYTE @ offset 8
// EfficiencyClass BYTE @ offset 9
// Reserved[20] BYTE[20]
// GroupCount WORD @ offset 30
// GroupMask[ANYSIZE] GROUP_AFFINITY 从 offset 32 开始
var efficiencyClass = Marshal.ReadByte(ptr, headerSize + 1);
var groupCount = (ushort)Marshal.ReadInt16(ptr, headerSize + 2 + 20);
var maskBase = IntPtr.Add(ptr, headerSize + 2 + 20 + sizeof(ushort));
for (var i = 0; i < groupCount; i++)
{
var affinityPtr = IntPtr.Add(maskBase, i * groupAffinitySize);
// 只读取 Mask 部分,统计位数
var mask = (IntPtr.Size == 8 ? (ulong)Marshal.ReadInt64(affinityPtr) : (uint)Marshal.ReadInt32(affinityPtr));
list.Add(new ProcessorCore(efficiencyClass, mask));
}
}
// 移动到下一个记录
ptr = IntPtr.Add(ptr, (int)size);
}
}
finally
{
Marshal.FreeHGlobal(buffer);
}
return list;
}
/// <summary>
/// 获取系统可用物理内存 (<c>ullAvailPhys</c>) 的字节数
/// </summary>
public static ulong GetAvailablePhysicalMemoryBytes()
{
var status = CreateStatus();
if (!GlobalMemoryStatusEx(ref status)) _ThrowLastWin32Error();
return status.ullAvailPhys;
}
/// <summary>
/// 获取系统可用物理内存 (<c>ullAvailPhys</c>) 和总物理内存 (<c>ullTotalPhys</c>) 的字节数
/// </summary>
public static (ulong Total, ulong Available) GetPhysicalMemoryBytes()
{
var status = CreateStatus();
if (!GlobalMemoryStatusEx(ref status)) _ThrowLastWin32Error();
return (status.ullTotalPhys, status.ullAvailPhys);
}
/// <summary>
/// 获取以百分比表示的系统内存占用 (范围 0.0 ~ 100.0)
/// </summary>
public static double GetMemoryLoadPercent()
{
var status = CreateStatus();
if (!GlobalMemoryStatusEx(ref status)) _ThrowLastWin32Error();
return status.dwMemoryLoad;
}
/// <summary>
/// 为当前进程新建终端窗口。<br/>
/// 若进程已拥有终端窗口,该方法将无任何作用。若有需要,可在调用前使用
/// <see cref="GetConsoleWindow"/> 来确认进程是否存在关联的终端窗口。
/// </summary>
public static void AllocateConsole()
{
if (_AllocConsole()) return;
var lastError = Marshal.GetLastWin32Error();
if (lastError != ERROR_ACCESS_DENIED) _ThrowLastWin32Error(lastError);
}
/// <summary>
/// 释放当前进程的终端窗口。<br/>
/// 若进程不存在关联的终端窗口,该方法将无任何作用。
/// </summary>
public static void FreeConsole() => _FreeConsole();
/// <summary>
/// 获取当前进程关联的终端窗口句柄。
/// </summary>
/// <returns>代表终端窗口的 HWND,若当前进程无关联的终端窗口,则该值为 <see cref="nint.Zero"/></returns>
public static nint GetConsoleWindow() => _GetConsoleWindow();
}
@@ -0,0 +1,22 @@
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
namespace PCL.Core.Utils.OS;
public static class NetworkUtils
{
private static readonly IPAddress[] _LocalIpAddresses = NetworkInterface.GetAllNetworkInterfaces()
.Where(x => x is { OperationalStatus: OperationalStatus.Up })
.SelectMany(x => x.GetIPProperties().UnicastAddresses)
.Where(ua => ua.Address is { AddressFamily: AddressFamily.InterNetwork or AddressFamily.InterNetworkV6 } &&
!ua.Address.Equals(IPAddress.Any) &&
!ua.Address.Equals(IPAddress.IPv6Any) &&
!ua.Address.Equals(IPAddress.Loopback) &&
!ua.Address.Equals(IPAddress.IPv6Loopback))
.Select(ua => ua.Address)
.ToArray();
public static IPAddress[] GetAllLocalAddress() => _LocalIpAddresses;
}
@@ -0,0 +1,27 @@
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace PCL.Core.Utils.OS;
public static partial class NtInterop
{
[LibraryImport("ntdll.dll")]
private static partial void RtlGetNtVersionNumbers(
out int major,
out int minor,
out int build);
private static void _ThrowLastWin32Error(int? errorCode = null) => throw new Win32Exception(errorCode ?? Marshal.GetLastWin32Error());
/// <summary>
/// Retrieve the kernel version number of the current operating system (unaffected by compatibility settings)
/// </summary>
/// <returns>A <see cref="Version"/> instance, used to represent the current operating system kernel version number.</returns>
public static Version GetCurrentOsVersion()
{
RtlGetNtVersionNumbers(out var major, out var minor, out var build);
build &= 0xFFFF;
return new Version(major, minor, build);
}
}
@@ -0,0 +1,225 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Management;
using System.Security;
using System.Security.Principal;
using Microsoft.Win32;
using PCL.Core.Logging;
namespace PCL.Core.Utils.OS;
public class ProcessInterop {
/// <summary>
/// 检查当前程序是否以管理员权限运行。
/// </summary>
/// <returns>如果当前用户具有管理员权限,则返回 true;否则返回 false。</returns>
public static bool IsAdmin() =>
new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
/// <summary>
/// 获取指定进程 ID 的命令行参数。
/// </summary>
/// <param name="processId">进程 ID</param>
/// <returns>命令行参数文本</returns>
public static string? GetCommandLine(int processId) {
var query = $"SELECT CommandLine FROM Win32_Process WHERE ProcessId = {processId}";
using var searcher = new ManagementObjectSearcher(query);
return searcher.Get().GetEnumerator().Current["CommandLine"].ToString();
}
/// <summary>
/// 从本地可执行文件启动新的进程。
/// </summary>
/// <param name="path">可执行文件路径</param>
/// <param name="arguments">程序参数</param>
/// <param name="runAsAdmin">指定是否以管理员身份启动该进程</param>
/// <returns>新的进程实例</returns>
public static Process? Start(string path, string? arguments = null, bool runAsAdmin = false) {
var psi = new ProcessStartInfo(path);
if (arguments is not null) psi.Arguments = arguments;
if (runAsAdmin)
{
psi.UseShellExecute = true;
psi.Verb = "runas";
}
if (Directory.Exists(path))
psi.UseShellExecute = true;
return Process.Start(psi);
}
/// <summary>
/// 获取指定进程的可执行文件路径
/// </summary>
/// <param name="process">进程实例</param>
/// <returns>可执行文件路径,若无法获取则为 <c>null</c></returns>
public static string? GetExecutablePath(Process process) {
try {
var path = process.MainModule?.FileName;
return (path is null) ? null : Path.GetFullPath(path);
} catch { return null; }
}
/// <summary>
/// 从本地可执行文件以管理员身份启动新的进程。<see cref="Start"/> 的套壳。
/// </summary>
/// <param name="path">可执行文件路径</param>
/// <param name="arguments">程序参数</param>
/// <returns>新的进程实例</returns>
public static Process? StartAsAdmin(string path, string? arguments = null) => Start(path, arguments, true);
/// <summary>
/// 结束指定进程。
/// </summary>
/// <param name="process">要结束的进程实例</param>
/// <param name="timeout">等待进程退出超时,以毫秒为单位,-1 表示无限制</param>
/// <param name="force">指定是否强制结束,若为 <c>true</c> 将通过带 <c>/F</c> 参数的 <c>TASKKILL.EXE</c> 结束进程</param>
/// <returns>进程返回值,若等待超时将返回 <see cref="int.MinValue"/></returns>
public static int Kill(Process process, int timeout = 3000, bool force = false) {
if (force) Process.Start(new ProcessStartInfo("TASKKILL.EXE", $"/PID {process.Id} /F") { UseShellExecute = false });
else process.Kill();
if (timeout == -1) process.WaitForExit();
else if (timeout != 0) process.WaitForExit(timeout);
return process.HasExited ? process.ExitCode : int.MinValue;
}
/// <summary>
/// 将特定程序设置为使用高性能显卡启动。
/// </summary>
/// <param name="executable">可执行文件路径。</param>
/// <param name="wantHighPerformance">是否使用高性能显卡,默认为 true。</param>
/// <exception cref="ArgumentException">当可执行文件路径无效时抛出</exception>
/// <exception cref="UnauthorizedAccessException">当没有足够权限访问注册表时抛出</exception>
/// <exception cref="SecurityException">当安全策略不允许访问注册表时抛出</exception>
/// <exception cref="InvalidOperationException">当注册表操作失败时抛出</exception>
public static void SetGpuPreference(string executable, bool wantHighPerformance = true) {
// 参数验证
if (string.IsNullOrWhiteSpace(executable)) {
throw new ArgumentException("可执行文件路径不能为空或仅包含空白字符", nameof(executable));
}
// 验证文件路径格式
try {
var fullPath = Path.GetFullPath(executable);
if (!File.Exists(fullPath)) {
LogWrapper.Warn("System", $"指定的可执行文件不存在: {executable}");
}
} catch (Exception ex) when (ex is ArgumentException or NotSupportedException or PathTooLongException) {
throw new ArgumentException($"无效的可执行文件路径: {executable}", nameof(executable), ex);
}
const string gpuPreferenceRegKey = @"Software\Microsoft\DirectX\UserGpuPreferences";
const string gpuPreferenceRegValueHigh = "GpuPreference=2;";
const string gpuPreferenceRegValueDefault = "GpuPreference=0;";
try {
var isCurrentHighPerformance = _GetCurrentGpuPreference(executable, gpuPreferenceRegKey, gpuPreferenceRegValueHigh);
LogWrapper.Info("System", $"当前程序 ({executable}) 的显卡设置为高性能: {isCurrentHighPerformance}");
// 如果当前设置已经是期望的设置,则无需修改
if (isCurrentHighPerformance == wantHighPerformance) {
LogWrapper.Info("System", $"程序 ({executable}) 的显卡设置已经是期望的设置,无需修改");
return;
}
// 写入新设置
_SetGpuPreferenceValue(executable, wantHighPerformance, gpuPreferenceRegKey,
gpuPreferenceRegValueHigh, gpuPreferenceRegValueDefault);
} catch (UnauthorizedAccessException ex) {
var errorMsg = "没有足够的权限访问注册表。请以管理员身份运行程序或检查用户权限设置。";
LogWrapper.Error(ex, "System", errorMsg);
throw new UnauthorizedAccessException(errorMsg, ex);
} catch (SecurityException ex) {
var errorMsg = "安全策略不允许访问注册表。请联系系统管理员检查安全设置。";
LogWrapper.Error(ex, "System", errorMsg);
throw new SecurityException(errorMsg, ex);
} catch (Exception ex) {
var errorMsg = $"设置 GPU 偏好时发生未预期的错误: {ex.Message}";
LogWrapper.Error(ex, "System", errorMsg);
throw new InvalidOperationException(errorMsg, ex);
}
}
/// <summary>
/// 获取当前程序的GPU偏好设置
/// </summary>
private static bool _GetCurrentGpuPreference(string executable, string regKey, string highPerfValue) {
try {
using var readOnlyKey = Registry.CurrentUser.OpenSubKey(regKey, false);
if (readOnlyKey is null) {
LogWrapper.Info("System", "GPU 偏好注册表键不存在,将在需要时创建");
return false;
}
var currentValue = readOnlyKey.GetValue(executable)?.ToString();
return string.Equals(currentValue, highPerfValue, StringComparison.OrdinalIgnoreCase);
} catch (Exception ex) {
LogWrapper.Warn(ex, "System", $"读取当前 GPU 偏好设置时出现错误: {ex.Message}");
return false; // 假设当前不是高性能模式
}
}
/// <summary>
/// 设置GPU偏好值到注册表
/// </summary>
private static bool _SetGpuPreferenceValue(string executable, bool wantHighPerformance,
string regKey, string highPerfValue, string defaultValue) {
RegistryKey? writeKey = null;
try {
// 尝试打开现有键进行写入
writeKey = Registry.CurrentUser.OpenSubKey(regKey, true);
// 如果键不存在,创建它
if (writeKey is null) {
LogWrapper.Info("System", "创建 GPU 偏好注册表键");
writeKey = Registry.CurrentUser.CreateSubKey(regKey);
if (writeKey is null) {
throw new InvalidOperationException($"无法创建注册表键: {regKey}");
}
}
var valueToSet = wantHighPerformance ? highPerfValue : defaultValue;
writeKey.SetValue(executable, valueToSet, RegistryValueKind.String);
LogWrapper.Info("System", $"成功设置程序 ({executable}) 的GPU偏好: {(wantHighPerformance ? "" : "")}");
return true;
} catch (UnauthorizedAccessException) {
// 重新抛出,让上层处理
throw;
} catch (SecurityException) {
// 重新抛出,让上层处理
throw;
} catch (Exception ex) {
var errorMsg = $"写入注册表时发生错误: {ex.Message}";
LogWrapper.Error(ex, "System", errorMsg);
throw new InvalidOperationException(errorMsg, ex);
} finally {
writeKey?.Dispose();
}
}
}
public enum ProcessExitCode {
/// <summary>
/// Indicates that the process completed successfully.
/// </summary>
TaskDone = 0,
/// <summary>
/// Indicates a general failure of the process.
/// </summary>
Failed = 1,
/// <summary>
/// Indicates the process was canceled.
/// </summary>
Canceled = 2,
/// <summary>
/// Indicates the process failed due to insufficient permissions.
/// </summary>
AccessDenied = 5
}
@@ -0,0 +1,108 @@
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;
namespace PCL.Core.Utils.OS;
public partial class RegistryChangeMonitor : IDisposable
{
// ReSharper disable InconsistentNaming
private const int REG_NOTIFY_CHANGE_LAST_SET = 0x00000004;
private const int KEY_NOTIFY = 0x0010;
private const int KEY_QUERY_VALUE = 0x0001;
private const int KEY_READ = (KEY_QUERY_VALUE | KEY_NOTIFY);
private const UIntPtr HKEY_CURRENT_USER = 0x80000001;
[LibraryImport("advapi32.dll", EntryPoint = "RegOpenKeyExW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
private static partial int _RegOpenKeyEx(UIntPtr hKey, string subKey, uint options, int samDesired, out IntPtr phkResult);
[LibraryImport("advapi32.dll", EntryPoint = "RegNotifyChangeKeyValue", SetLastError = true)]
private static partial int _RegNotifyChangeKeyValue(IntPtr hKey, [MarshalAs(UnmanagedType.Bool)] bool bWatchSubtree, int dwNotifyFilter, IntPtr hEvent, [MarshalAs(UnmanagedType.Bool)] bool fAsynchronous);
[LibraryImport("advapi32.dll", EntryPoint = "RegCloseKey", SetLastError = true)]
private static partial int _RegCloseKey(IntPtr hKey);
// ReSharper restore InconsistentNaming
private readonly IntPtr _hKey;
private readonly ManualResetEvent _stopEvent = new(false);
private readonly ManualResetEvent _registryEvent = new(false);
private readonly Thread _monitorThread;
public event EventHandler? Changed;
public RegistryChangeMonitor(string keyPath)
{
// Open registry key with proper access rights
var result = _RegOpenKeyEx(HKEY_CURRENT_USER, keyPath, 0, KEY_READ, out _hKey);
if (result != 0) throw new Win32Exception(result);
// Start monitoring thread
_monitorThread = new Thread(_MonitorThread) { IsBackground = true };
_monitorThread.Start();
}
private void _MonitorThread()
{
try
{
// Initial registration
_RegisterForNotification();
while (!_stopEvent.WaitOne(0))
{
// Wait for either registry change or stop signal
var index = WaitHandle.WaitAny(
[_registryEvent, _stopEvent],
TimeSpan.FromSeconds(1)); // Timeout to check for stop periodically
if (index == 1) break; // Stop requested
if (index == 0)
{
_registryEvent.Reset();
Changed?.Invoke(this, EventArgs.Empty);
_RegisterForNotification(); // Re-register for next change
}
}
}
finally
{
_registryEvent.Dispose();
}
}
private void _RegisterForNotification()
{
var result = _RegNotifyChangeKeyValue(
_hKey,
true,
REG_NOTIFY_CHANGE_LAST_SET,
_registryEvent.SafeWaitHandle.DangerousGetHandle(),
true); // Must be asynchronous to allow graceful shutdown
if (result != 0)
{
// Handle error - key might have been deleted
_stopEvent.Set();
throw new Win32Exception(result);
}
}
public void Dispose()
{
_stopEvent.Set();
// Give thread a chance to exit gracefully
if (_monitorThread is {IsAlive: true})
_monitorThread.Join(1000);
if (_hKey != IntPtr.Zero)
_ = _RegCloseKey(_hKey);
_stopEvent.Dispose();
GC.SuppressFinalize(this);
}
}
@@ -0,0 +1,28 @@
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace PCL.Core.Utils.OS;
public static class SystemInfo
{
/// <summary>
/// 是否为 32 位系统。
/// </summary>
public static readonly bool Is32BitSystem = !Environment.Is64BitOperatingSystem;
/// <summary>
/// 是否为 ARM64 架构。
/// </summary>
public static readonly bool IsArm64System = RuntimeInformation.OSArchitecture == Architecture.Arm64;
/// <summary>
/// 是否使用 GBK 编码。
/// </summary>
public static readonly bool IsGBKEncoding = Encoding.Default.CodePage == 936;
/// <summary>
/// 系统信息描述,例如 Microsoft Windows 11 专业工作站版 10.0.22635.0
/// </summary>
public static readonly string OSInfo = $"{RuntimeInformation.OSDescription} {Environment.OSVersion.Version}";
}
@@ -0,0 +1,11 @@
using System;
using System.IO;
namespace PCL.Core.Utils.OS;
public static class SystemPaths {
/// <summary>
/// 系统盘符(含冒号和反斜杠),例如 "C:\"。
/// </summary>
public static string DriveLetter { get; } = Path.GetPathRoot(Environment.SystemDirectory)!;
}
@@ -0,0 +1,32 @@
using System;
using System.IO;
using System.Security;
using Microsoft.Win32;
using PCL.Core.Logging;
namespace PCL.Core.Utils.OS;
public class SystemTheme {
private const string ThemeRegistryPath = @"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize";
private const string AppsUseLightThemeKey = "AppsUseLightTheme";
/// <summary>
/// 检查系统是否处于深色模式。
/// </summary>
/// <returns>如果系统使用深色模式,则返回 true;否则返回 false(包括注册表不可访问的情况)。</returns>
public static bool IsSystemInDarkMode() {
try {
using var registryKey = Registry.CurrentUser.OpenSubKey(ThemeRegistryPath);
if (registryKey is null) {
LogWrapper.Warn($"注册表键 {ThemeRegistryPath} 不存在");
return false;
}
var value = registryKey.GetValue(AppsUseLightThemeKey) as int?;
return value == 0; // 0 表示深色模式(AppsUseLightTheme = false
} catch (Exception ex) when (ex is SecurityException or IOException) {
LogWrapper.Warn(ex, $"无法访问注册表键 {ThemeRegistryPath}");
return false;
}
}
}
@@ -0,0 +1,124 @@
using System;
using System.Runtime.InteropServices;
using System.ComponentModel;
namespace PCL.Core.Utils.OS;
public static partial class WindowInterop
{
// ReSharper disable InconsistentNaming UnusedMember.Local
// DWM 外边缘结构定义
[StructLayout(LayoutKind.Sequential)]
private struct MARGINS { public int leftWidth, rightWidth, topHeight, bottomHeight; }
[LibraryImport("dwmapi.dll")]
private static partial int DwmExtendFrameIntoClientArea(IntPtr hWnd, ref MARGINS pMarInset);
[LibraryImport("dwmapi.dll")]
private static partial int DwmIsCompositionEnabled([MarshalAs(UnmanagedType.Bool)] out bool pfEnabled);
// Win32 矩形结构定义
[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int left; public int top; public int right; public int bottom; }
[LibraryImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
// MONITOR_DPI_TYPE enum
private enum MONITOR_DPI_TYPE {
MDT_EFFECTIVE_DPI = 0,
MDT_ANGULAR_DPI = 1,
MDT_RAW_DPI = 2,
MDT_DEFAULT = MDT_EFFECTIVE_DPI
}
[LibraryImport("user32.dll")]
private static partial IntPtr MonitorFromWindow(IntPtr hWnd, uint dwFlags);
// Get the primary monitor handle
private const int MONITOR_DEFAULTTOPRIMARY = 1;
[LibraryImport("shcore.dll", EntryPoint = "GetDpiForMonitor")]
private static partial int GetDpiForMonitor(
IntPtr hMonitor,
MONITOR_DPI_TYPE dpiType,
out uint dpiX,
out uint dpiY
);
// ReSharper enable InconsistentNaming UnusedMember.Local
/// <summary>
/// 检测 DWM 组合是否可用
/// </summary>
public static bool IsCompositionEnabled()
{
var hResult = DwmIsCompositionEnabled(out var enabled);
return hResult != 0 ? throw new Win32Exception(hResult, "Failed to check DWM status") : enabled;
}
/// <summary>
/// 设置 DWM 窗口边框到客户区域的扩展大小
/// </summary>
public static void ExtendFrameIntoClientArea(
IntPtr hWnd, int marginLeft, int marginTop, int marginRight, int marginBottom)
{
MARGINS margins = new()
{
leftWidth = marginLeft,
rightWidth = marginRight,
topHeight = marginTop,
bottomHeight = marginBottom
};
if (!IsCompositionEnabled()) return;
var hResult = DwmExtendFrameIntoClientArea(hWnd, ref margins);
if (hResult != 0) throw new Win32Exception(hResult, "Failed to extend frame into client area");
}
/// <summary>
/// See <see cref="ExtendFrameIntoClientArea(IntPtr, int, int, int, int)"/>
/// </summary>
public static void ExtendFrameIntoClientArea(IntPtr hWnd, int margin)
=> ExtendFrameIntoClientArea(hWnd, margin, margin, margin, margin);
/// <summary>
/// 获取 Win32 窗口矩形定义
/// </summary>
public static (int Left, int Top, int Right, int Bottom) GetWindowRectangle(IntPtr hWnd)
{
var hResult = GetWindowRect(hWnd, out var rect);
return hResult ? (rect.left, rect.top, rect.right, rect.bottom)
: throw new Win32Exception("Failed to get window rectangle");
}
/// <summary>
/// 获取 Win32 窗口位置与大小
/// </summary>
public static (int X, int Y, int Width, int Height) ToWindowBounds(
this (int Left, int Top, int Right, int Bottom) rect)
{
var (l, t, r, b) = rect;
var x = l;
var y = t;
var width = r - l;
var height = b - t;
return (x, y, width, height);
}
/// <summary>
/// 获取指定屏幕的系统 DPI
/// </summary>
/// <param name="hWnd">位于指定屏幕上的任意窗口句柄,默认指定主屏</param>
public static int GetSystemDpi(IntPtr hWnd = 0) {
// Get the monitor handle
var hMonitor = MonitorFromWindow(hWnd, MONITOR_DEFAULTTOPRIMARY);
// 0 is S_OK
var hr = GetDpiForMonitor(hMonitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var dpiX, out _);
if (hr == 0)
return (int)dpiX;
// fallback to default DPI (96)
return 96;
}
}