using System.Net;
using System.Diagnostics;
using System.Runtime.InteropServices;
using XFEToolBox.Core.Tools;
using XFEToolBox.Core.Models.Users;
using XFEToolBox.Server.Core.Exceptions;
using XFEToolBox.Server.Core.Services;
using XFEToolBox.Server.Core.Utilities;
using XFEToolBox.Server.Profiles.Data;
using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Utilities.Server.Services.CoreService;
namespace XFEToolBox.Server.Services;
public partial class AdminManagementService : ServerCoreUserServiceBase
{
public IToolPackageRepository? ToolPackageRepository { get; set; }
public long MaxPackageBytes { get; set; }
public string SoftwareStorageRoot { get; set; } = string.Empty;
[EntryPoint("v1/manage/overview")]
public async Task GetOverviewEntryPoint()
{
if (!await VerifyAdministrator()) return;
var packages = ToolPackageRepository is null
? []
: await ToolPackageRepository.ListAsync(publishedOnly: false);
using var process = Process.GetCurrentProcess();
var cpuUsage = await SystemCpuUsageSampler.SampleAsync();
process.Refresh();
var softwareStorageBytes = GetDirectorySize(SoftwareStorageRoot);
var memory = GetSystemMemoryInfo();
await Close(new
{
serverName = "XFEToolBoxServer",
status = "running",
utc = DateTimeOffset.UtcNow,
userCount = UserDataProfile.UserTable.Count,
activeSessionCount = UserDataProfile.LoginTable.Count,
packageCount = packages.Count,
publishedPackageCount = packages.Count(package => package.Published),
storageBytes = packages.Sum(package => package.PackageSize) + softwareStorageBytes,
softwareCount = MainDataProfile.SoftwareCatalog.Count,
publishedSoftwareCount = MainDataProfile.SoftwareCatalog.Count(item => item.Published && item.Enabled),
softwareStorageBytes,
cpuUsagePercent = Math.Clamp(cpuUsage, 0, 100),
processorCount = Environment.ProcessorCount,
workingSetBytes = process.WorkingSet64,
privateMemoryBytes = process.PrivateMemorySize64,
managedMemoryBytes = GC.GetTotalMemory(forceFullCollection: false),
totalMemoryBytes = memory.TotalBytes,
usedMemoryBytes = memory.UsedBytes,
availableMemoryBytes = memory.AvailableBytes,
memoryUsagePercent = memory.UsagePercent,
uptimeSeconds = Math.Max(0, (DateTime.Now - process.StartTime).TotalSeconds)
});
}
[EntryPoint("v1/manage/users/list")]
public async Task GetUsersEntryPoint()
{
if (!await VerifyAdministrator()) return;
await Close(UserDataProfile.UserTable
.OrderByDescending(user => user.PermissionLevel)
.ThenBy(user => user.UserName, StringComparer.OrdinalIgnoreCase)
.Select(user => new
{
user.Id,
user.UserName,
user.NickName,
user.Bio,
user.Enable,
user.PermissionLevel,
role = user.Role.ToString()
})
.ToArray());
}
[EntryPoint("v1/manage/users/create")]
public async Task CreateUserEntryPoint()
{
if (!await VerifyAdministrator()) return;
var userName = GetString("userName");
var password = GetString("password", trim: false);
var nickName = GetString("nickName") ?? userName;
if (userName is null || password is null)
{
await CloseWithError("账号和密码不能为空。", HttpStatusCode.BadRequest);
return;
}
if (userName.Length is < 3 or > 32 || password.Length is < 8 or > 128)
{
await CloseWithError("账号长度应为 3-32 个字符,密码长度应为 8-128 个字符。", HttpStatusCode.BadRequest);
return;
}
if (UserDataProfile.UserTable.Any(user =>
string.Equals(user.UserName, userName, StringComparison.OrdinalIgnoreCase)))
{
await CloseWithError("该账号已存在。", HttpStatusCode.Conflict);
return;
}
var user = new ToolBoxUser
{
UserName = userName,
Password = password,
NickName = nickName ?? userName,
Enable = true,
Role = GetBoolean("isAdministrator") ? ToolBoxUserRole.Administrator : ToolBoxUserRole.User
};
UserDataProfile.UserTable.Add(user);
UserDataProfile.SaveProfile();
await Close(new { user.Id, user.UserName, user.NickName, user.Enable, user.PermissionLevel });
}
[EntryPoint("v1/manage/users/update")]
public async Task UpdateUserEntryPoint()
{
if (!await VerifyAdministrator()) return;
var id = GetString("id");
var user = UserDataProfile.UserTable.FirstOrDefault(item => item.Id == id);
if (user is null)
{
await CloseWithError("用户不存在。", HttpStatusCode.NotFound);
return;
}
var enabled = GetNullableBoolean("enabled");
var isAdministrator = GetNullableBoolean("isAdministrator");
var nickName = GetString("nickName");
var password = GetString("password", trim: false);
if (user.Id == User.Id && (enabled == false || isAdministrator == false))
{
await CloseWithError("不能禁用当前账号或移除自己的管理员权限。", HttpStatusCode.BadRequest);
return;
}
if (password is not null && password.Length is < 8 or > 128)
{
await CloseWithError("密码长度应为 8-128 个字符。", HttpStatusCode.BadRequest);
return;
}
if (enabled.HasValue) user.Enable = enabled.Value;
if (isAdministrator.HasValue)
user.Role = isAdministrator.Value ? ToolBoxUserRole.Administrator : ToolBoxUserRole.User;
if (nickName is not null) user.NickName = nickName;
if (password is not null)
{
user.Password = password;
foreach (var login in UserDataProfile.LoginTable
.Where(item => item.UserLoginModel.Uid == user.Id)
.ToArray())
UserDataProfile.LoginTable.Remove(login);
}
UserDataProfile.SaveProfile();
await Close(new { user.Id, user.UserName, user.NickName, user.Enable, user.PermissionLevel });
}
[EntryPoint("v1/manage/tools/list")]
public async Task GetToolsEntryPoint()
{
if (!await VerifyAdministrator() || !await VerifyRepository()) return;
var packages = await ToolPackageRepository!.ListAsync(publishedOnly: false);
await Close(packages
.OrderBy(package => package.Manifest.Id, StringComparer.OrdinalIgnoreCase)
.ThenByDescending(package => package.Manifest.Version, SemanticVersionComparer.Instance)
.Select(ToolPackageContractMapper.ToUploadResult)
.ToArray());
}
[EntryPoint("v1/manage/tools/upload")]
public async Task UploadToolEntryPoint()
{
if (!await VerifyAdministrator() || !await VerifyRepository()) return;
var packageBase64 = GetString("packageBase64", trim: false);
if (packageBase64 is null || packageBase64.Length > checked(MaxPackageBytes * 2))
{
await CloseWithError("工具包内容为空或超过限制。", HttpStatusCode.RequestEntityTooLarge);
return;
}
byte[] packageBytes;
try
{
packageBytes = Convert.FromBase64String(packageBase64);
}
catch (FormatException)
{
await CloseWithError("工具包 Base64 无效。", HttpStatusCode.BadRequest);
return;
}
if (packageBytes.LongLength > MaxPackageBytes)
{
await CloseWithError("工具包超过大小限制。", HttpStatusCode.RequestEntityTooLarge);
return;
}
try
{
using var stream = new MemoryStream(packageBytes, writable: false);
var package = await ToolPackageRepository!.SaveAsync(
stream,
GetNullableBoolean("published") ?? true,
GetNullableBoolean("overwrite") ?? false);
ReturnArgs.StatusCode = HttpStatusCode.Created;
await Close(ToolPackageContractMapper.ToUploadResult(package));
}
catch (ToolPackageValidationException exception)
{
await CloseWithError(exception.Message, HttpStatusCode.BadRequest);
}
catch (ToolPackageConflictException exception)
{
await CloseWithError(exception.Message, HttpStatusCode.Conflict);
}
}
[EntryPoint("v1/manage/tools/publication")]
public async Task SetPublicationEntryPoint()
{
if (!await VerifyAdministrator() || !await VerifyRepository()) return;
var toolId = GetString("toolId");
var version = GetString("version");
var published = GetNullableBoolean("published");
if (toolId is null || version is null || !published.HasValue)
{
await CloseWithError("toolId、version 和 published 均为必填项。", HttpStatusCode.BadRequest);
return;
}
try
{
var package = await ToolPackageRepository!.SetReviewStatusAsync(
toolId,
version,
published.Value ? ToolPackageReviewStatus.Approved : ToolPackageReviewStatus.Rejected,
User.Id,
User.UserName);
await Close(ToolPackageContractMapper.ToUploadResult(package));
}
catch (ToolPackageNotFoundException exception)
{
await CloseWithError(exception.Message, HttpStatusCode.NotFound);
}
}
[EntryPoint("v1/manage/tools/review")]
public async Task ReviewToolEntryPoint()
{
if (!await VerifyAdministrator() || !await VerifyRepository()) return;
var toolId = GetString("toolId");
var version = GetString("version");
var approved = GetNullableBoolean("approved");
if (toolId is null || version is null || !approved.HasValue)
{
await CloseWithError("toolId、version 和 approved 均为必填项。", HttpStatusCode.BadRequest);
return;
}
try
{
var package = await ToolPackageRepository!.SetReviewStatusAsync(
toolId,
version,
approved.Value ? ToolPackageReviewStatus.Approved : ToolPackageReviewStatus.Rejected,
User.Id,
User.UserName,
GetString("reviewMessage"));
await Close(ToolPackageContractMapper.ToUploadResult(package));
}
catch (ToolPackageNotFoundException exception)
{
await CloseWithError(exception.Message, HttpStatusCode.NotFound);
}
}
private async Task<bool> VerifyAdministrator()
{
if (User.PermissionLevel >= (int)ToolBoxUserRole.Administrator) return true;
await CloseWithError("需要管理员权限。", HttpStatusCode.Forbidden);
return false;
}
private async Task<bool> VerifyRepository()
{
if (ToolPackageRepository is not null) return true;
await CloseWithError("工具包仓库未初始化。", HttpStatusCode.InternalServerError);
return false;
}
private string? GetString(string propertyName, bool trim = true)
{
try
{
var value = Json?[propertyName]?.GetValue<string>();
if (string.IsNullOrWhiteSpace(value)) return null;
return trim ? value.Trim() : value;
}
catch (Exception exception) when (exception is InvalidOperationException or FormatException)
{
return null;
}
}
private bool GetBoolean(string propertyName) => GetNullableBoolean(propertyName) ?? false;
private bool? GetNullableBoolean(string propertyName)
{
try
{
return Json?[propertyName]?.GetValue<bool>();
}
catch (Exception exception) when (exception is InvalidOperationException or FormatException)
{
return null;
}
}
private static long GetDirectorySize(string path)
{
if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path)) return 0;
try
{
return Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
.Sum(file => new FileInfo(file).Length);
}
catch (IOException) { return 0; }
catch (UnauthorizedAccessException) { return 0; }
}
private static SystemMemoryInfo GetSystemMemoryInfo()
{
if (OperatingSystem.IsWindows())
{
var status = new MemoryStatusEx { Length = (uint)Marshal.SizeOf<MemoryStatusEx>() };
if (GlobalMemoryStatusEx(ref status))
{
var total = (long)Math.Min(status.TotalPhysical, (ulong)long.MaxValue);
var available = (long)Math.Min(status.AvailablePhysical, (ulong)long.MaxValue);
return CreateMemoryInfo(total, available);
}
}
if (OperatingSystem.IsLinux() && File.Exists("/proc/meminfo"))
{
try
{
long totalKb = 0;
long availableKb = 0;
foreach (var line in File.ReadLines("/proc/meminfo"))
{
if (line.StartsWith("MemTotal:", StringComparison.Ordinal)) totalKb = ParseMemInfoKilobytes(line);
else if (line.StartsWith("MemAvailable:", StringComparison.Ordinal)) availableKb = ParseMemInfoKilobytes(line);
if (totalKb > 0 && availableKb > 0) break;
}
if (totalKb > 0) return CreateMemoryInfo(totalKb * 1024, availableKb * 1024);
}
catch (IOException) { }
catch (UnauthorizedAccessException) { }
}
var fallbackTotal = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes;
var fallbackAvailable = Math.Max(0, fallbackTotal - Process.GetCurrentProcess().WorkingSet64);
return CreateMemoryInfo(fallbackTotal, fallbackAvailable);
}
private static SystemMemoryInfo CreateMemoryInfo(long total, long available)
{
total = Math.Max(0, total);
available = Math.Clamp(available, 0, total);
var used = Math.Max(0, total - available);
var percent = total == 0 ? 0 : used * 100d / total;
return new SystemMemoryInfo(total, used, available, Math.Clamp(percent, 0, 100));
}
private static long ParseMemInfoKilobytes(string line)
{
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return parts.Length >= 2 && long.TryParse(parts[1], out var value) ? value : 0;
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GlobalMemoryStatusEx(ref MemoryStatusEx buffer);
[StructLayout(LayoutKind.Sequential)]
private struct MemoryStatusEx
{
public uint Length;
public uint MemoryLoad;
public ulong TotalPhysical;
public ulong AvailablePhysical;
public ulong TotalPageFile;
public ulong AvailablePageFile;
public ulong TotalVirtual;
public ulong AvailableVirtual;
public ulong AvailableExtendedVirtual;
}
private readonly record struct SystemMemoryInfo(long TotalBytes, long UsedBytes, long AvailableBytes, double UsagePercent);
}
using System.Net;
using System.Diagnostics;
using System.Runtime.InteropServices;
using XFEToolBox.Core.Tools;
using XFEToolBox.Core.Models.Users;
using XFEToolBox.Server.Core.Exceptions;
using XFEToolBox.Server.Core.Services;
using XFEToolBox.Server.Core.Utilities;
using XFEToolBox.Server.Profiles.Data;
using XFEExtension.NetCore.ServerInteractive.Attributes;
using XFEExtension.NetCore.ServerInteractive.Utilities.Server.Services.CoreService;
namespace XFEToolBox.Server.Services;
public partial class AdminManagementService : ServerCoreUserServiceBase
{
public IToolPackageRepository? ToolPackageRepository { get; set; }
public long MaxPackageBytes { get; set; }
public string SoftwareStorageRoot { get; set; } = string.Empty;
[EntryPoint("v1/manage/overview")]
public async Task GetOverviewEntryPoint()
{
if (!await VerifyAdministrator()) return;
var packages = ToolPackageRepository is null
? []
: await ToolPackageRepository.ListAsync(publishedOnly: false);
using var process = Process.GetCurrentProcess();
var cpuUsage = await SystemCpuUsageSampler.SampleAsync();
process.Refresh();
var softwareStorageBytes = GetDirectorySize(SoftwareStorageRoot);
var memory = GetSystemMemoryInfo();
await Close(new
{
serverName = "XFEToolBoxServer",
status = "running",
utc = DateTimeOffset.UtcNow,
userCount = UserDataProfile.UserTable.Count,
activeSessionCount = UserDataProfile.LoginTable.Count,
packageCount = packages.Count,
publishedPackageCount = packages.Count(package => package.Published),
storageBytes = packages.Sum(package => package.PackageSize) + softwareStorageBytes,
softwareCount = MainDataProfile.SoftwareCatalog.Count,
publishedSoftwareCount = MainDataProfile.SoftwareCatalog.Count(item => item.Published && item.Enabled),
softwareStorageBytes,
cpuUsagePercent = Math.Clamp(cpuUsage, 0, 100),
processorCount = Environment.ProcessorCount,
workingSetBytes = process.WorkingSet64,
privateMemoryBytes = process.PrivateMemorySize64,
managedMemoryBytes = GC.GetTotalMemory(forceFullCollection: false),
totalMemoryBytes = memory.TotalBytes,
usedMemoryBytes = memory.UsedBytes,
availableMemoryBytes = memory.AvailableBytes,
memoryUsagePercent = memory.UsagePercent,
uptimeSeconds = Math.Max(0, (DateTime.Now - process.StartTime).TotalSeconds)
});
}
[EntryPoint("v1/manage/users/list")]
public async Task GetUsersEntryPoint()
{
if (!await VerifyAdministrator()) return;
await Close(UserDataProfile.UserTable
.OrderByDescending(user => user.PermissionLevel)
.ThenBy(user => user.UserName, StringComparer.OrdinalIgnoreCase)
.Select(user => new
{
user.Id,
user.UserName,
user.NickName,
user.Bio,
user.Enable,
user.PermissionLevel,
role = user.Role.ToString()
})
.ToArray());
}
[EntryPoint("v1/manage/users/create")]
public async Task CreateUserEntryPoint()
{
if (!await VerifyAdministrator()) return;
var userName = GetString("userName");
var password = GetString("password", trim: false);
var nickName = GetString("nickName") ?? userName;
if (userName is null || password is null)
{
await CloseWithError("账号和密码不能为空。", HttpStatusCode.BadRequest);
return;
}
if (userName.Length is < 3 or > 32 || password.Length is < 8 or > 128)
{
await CloseWithError("账号长度应为 3-32 个字符,密码长度应为 8-128 个字符。", HttpStatusCode.BadRequest);
return;
}
if (UserDataProfile.UserTable.Any(user =>
string.Equals(user.UserName, userName, StringComparison.OrdinalIgnoreCase)))
{
await CloseWithError("该账号已存在。", HttpStatusCode.Conflict);
return;
}
var user = new ToolBoxUser
{
UserName = userName,
Password = password,
NickName = nickName ?? userName,
Enable = true,
Role = GetBoolean("isAdministrator") ? ToolBoxUserRole.Administrator : ToolBoxUserRole.User
};
UserDataProfile.UserTable.Add(user);
UserDataProfile.SaveProfile();
await Close(new { user.Id, user.UserName, user.NickName, user.Enable, user.PermissionLevel });
}
[EntryPoint("v1/manage/users/update")]
public async Task UpdateUserEntryPoint()
{
if (!await VerifyAdministrator()) return;
var id = GetString("id");
var user = UserDataProfile.UserTable.FirstOrDefault(item => item.Id == id);
if (user is null)
{
await CloseWithError("用户不存在。", HttpStatusCode.NotFound);
return;
}
var enabled = GetNullableBoolean("enabled");
var isAdministrator = GetNullableBoolean("isAdministrator");
var nickName = GetString("nickName");
var password = GetString("password", trim: false);
if (user.Id == User.Id && (enabled == false || isAdministrator == false))
{
await CloseWithError("不能禁用当前账号或移除自己的管理员权限。", HttpStatusCode.BadRequest);
return;
}
if (password is not null && password.Length is < 8 or > 128)
{
await CloseWithError("密码长度应为 8-128 个字符。", HttpStatusCode.BadRequest);
return;
}
if (enabled.HasValue) user.Enable = enabled.Value;
if (isAdministrator.HasValue)
user.Role = isAdministrator.Value ? ToolBoxUserRole.Administrator : ToolBoxUserRole.User;
if (nickName is not null) user.NickName = nickName;
if (password is not null)
{
user.Password = password;
foreach (var login in UserDataProfile.LoginTable
.Where(item => item.UserLoginModel.Uid == user.Id)
.ToArray())
UserDataProfile.LoginTable.Remove(login);
}
UserDataProfile.SaveProfile();
await Close(new { user.Id, user.UserName, user.NickName, user.Enable, user.PermissionLevel });
}
[EntryPoint("v1/manage/tools/list")]
public async Task GetToolsEntryPoint()
{
if (!await VerifyAdministrator() || !await VerifyRepository()) return;
var packages = await ToolPackageRepository!.ListAsync(publishedOnly: false);
await Close(packages
.OrderBy(package => package.Manifest.Id, StringComparer.OrdinalIgnoreCase)
.ThenByDescending(package => package.Manifest.Version, SemanticVersionComparer.Instance)
.Select(ToolPackageContractMapper.ToUploadResult)
.ToArray());
}
[EntryPoint("v1/manage/tools/upload")]
public async Task UploadToolEntryPoint()
{
if (!await VerifyAdministrator() || !await VerifyRepository()) return;
var packageBase64 = GetString("packageBase64", trim: false);
if (packageBase64 is null || packageBase64.Length > checked(MaxPackageBytes * 2))
{
await CloseWithError("工具包内容为空或超过限制。", HttpStatusCode.RequestEntityTooLarge);
return;
}
byte[] packageBytes;
try
{
packageBytes = Convert.FromBase64String(packageBase64);
}
catch (FormatException)
{
await CloseWithError("工具包 Base64 无效。", HttpStatusCode.BadRequest);
return;
}
if (packageBytes.LongLength > MaxPackageBytes)
{
await CloseWithError("工具包超过大小限制。", HttpStatusCode.RequestEntityTooLarge);
return;
}
try
{
using var stream = new MemoryStream(packageBytes, writable: false);
var package = await ToolPackageRepository!.SaveAsync(
stream,
GetNullableBoolean("published") ?? true,
GetNullableBoolean("overwrite") ?? false);
ReturnArgs.StatusCode = HttpStatusCode.Created;
await Close(ToolPackageContractMapper.ToUploadResult(package));
}
catch (ToolPackageValidationException exception)
{
await CloseWithError(exception.Message, HttpStatusCode.BadRequest);
}
catch (ToolPackageConflictException exception)
{
await CloseWithError(exception.Message, HttpStatusCode.Conflict);
}
}
[EntryPoint("v1/manage/tools/publication")]
public async Task SetPublicationEntryPoint()
{
if (!await VerifyAdministrator() || !await VerifyRepository()) return;
var toolId = GetString("toolId");
var version = GetString("version");
var published = GetNullableBoolean("published");
if (toolId is null || version is null || !published.HasValue)
{
await CloseWithError("toolId、version 和 published 均为必填项。", HttpStatusCode.BadRequest);
return;
}
try
{
var package = await ToolPackageRepository!.SetReviewStatusAsync(
toolId,
version,
published.Value ? ToolPackageReviewStatus.Approved : ToolPackageReviewStatus.Rejected,
User.Id,
User.UserName);
await Close(ToolPackageContractMapper.ToUploadResult(package));
}
catch (ToolPackageNotFoundException exception)
{
await CloseWithError(exception.Message, HttpStatusCode.NotFound);
}
}
[EntryPoint("v1/manage/tools/review")]
public async Task ReviewToolEntryPoint()
{
if (!await VerifyAdministrator() || !await VerifyRepository()) return;
var toolId = GetString("toolId");
var version = GetString("version");
var approved = GetNullableBoolean("approved");
if (toolId is null || version is null || !approved.HasValue)
{
await CloseWithError("toolId、version 和 approved 均为必填项。", HttpStatusCode.BadRequest);
return;
}
try
{
var package = await ToolPackageRepository!.SetReviewStatusAsync(
toolId,
version,
approved.Value ? ToolPackageReviewStatus.Approved : ToolPackageReviewStatus.Rejected,
User.Id,
User.UserName,
GetString("reviewMessage"));
await Close(ToolPackageContractMapper.ToUploadResult(package));
}
catch (ToolPackageNotFoundException exception)
{
await CloseWithError(exception.Message, HttpStatusCode.NotFound);
}
}
private async Task<bool> VerifyAdministrator()
{
if (User.PermissionLevel >= (int)ToolBoxUserRole.Administrator) return true;
await CloseWithError("需要管理员权限。", HttpStatusCode.Forbidden);
return false;
}
private async Task<bool> VerifyRepository()
{
if (ToolPackageRepository is not null) return true;
await CloseWithError("工具包仓库未初始化。", HttpStatusCode.InternalServerError);
return false;
}
private string? GetString(string propertyName, bool trim = true)
{
try
{
var value = Json?[propertyName]?.GetValue<string>();
if (string.IsNullOrWhiteSpace(value)) return null;
return trim ? value.Trim() : value;
}
catch (Exception exception) when (exception is InvalidOperationException or FormatException)
{
return null;
}
}
private bool GetBoolean(string propertyName) => GetNullableBoolean(propertyName) ?? false;
private bool? GetNullableBoolean(string propertyName)
{
try
{
return Json?[propertyName]?.GetValue<bool>();
}
catch (Exception exception) when (exception is InvalidOperationException or FormatException)
{
return null;
}
}
private static long GetDirectorySize(string path)
{
if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path)) return 0;
try
{
return Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)
.Sum(file => new FileInfo(file).Length);
}
catch (IOException) { return 0; }
catch (UnauthorizedAccessException) { return 0; }
}
private static SystemMemoryInfo GetSystemMemoryInfo()
{
if (OperatingSystem.IsWindows())
{
var status = new MemoryStatusEx { Length = (uint)Marshal.SizeOf<MemoryStatusEx>() };
if (GlobalMemoryStatusEx(ref status))
{
var total = (long)Math.Min(status.TotalPhysical, (ulong)long.MaxValue);
var available = (long)Math.Min(status.AvailablePhysical, (ulong)long.MaxValue);
return CreateMemoryInfo(total, available);
}
}
if (OperatingSystem.IsLinux() && File.Exists("/proc/meminfo"))
{
try
{
long totalKb = 0;
long availableKb = 0;
foreach (var line in File.ReadLines("/proc/meminfo"))
{
if (line.StartsWith("MemTotal:", StringComparison.Ordinal)) totalKb = ParseMemInfoKilobytes(line);
else if (line.StartsWith("MemAvailable:", StringComparison.Ordinal)) availableKb = ParseMemInfoKilobytes(line);
if (totalKb > 0 && availableKb > 0) break;
}
if (totalKb > 0) return CreateMemoryInfo(totalKb * 1024, availableKb * 1024);
}
catch (IOException) { }
catch (UnauthorizedAccessException) { }
}
var fallbackTotal = GC.GetGCMemoryInfo().TotalAvailableMemoryBytes;
var fallbackAvailable = Math.Max(0, fallbackTotal - Process.GetCurrentProcess().WorkingSet64);
return CreateMemoryInfo(fallbackTotal, fallbackAvailable);
}
private static SystemMemoryInfo CreateMemoryInfo(long total, long available)
{
total = Math.Max(0, total);
available = Math.Clamp(available, 0, total);
var used = Math.Max(0, total - available);
var percent = total == 0 ? 0 : used * 100d / total;
return new SystemMemoryInfo(total, used, available, Math.Clamp(percent, 0, 100));
}
private static long ParseMemInfoKilobytes(string line)
{
var parts = line.Split(' ', StringSplitOptions.RemoveEmptyEntries);
return parts.Length >= 2 && long.TryParse(parts[1], out var value) ? value : 0;
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GlobalMemoryStatusEx(ref MemoryStatusEx buffer);
[StructLayout(LayoutKind.Sequential)]
private struct MemoryStatusEx
{
public uint Length;
public uint MemoryLoad;
public ulong TotalPhysical;
public ulong AvailablePhysical;
public ulong TotalPageFile;
public ulong AvailablePageFile;
public ulong TotalVirtual;
public ulong AvailableVirtual;
public ulong AvailableExtendedVirtual;
}
private readonly record struct SystemMemoryInfo(long TotalBytes, long UsedBytes, long AvailableBytes, double UsagePercent);
}