mirror of
https://git.743378673.xyz/MeloNX/MeloNX.git
synced 2025-07-23 15:07:11 +02:00
Overhaul of string formatting/parsing/sorting logic for TimeSpans, DateTimes, and file sizes (#4956)
* Fixed formatting/parsing issues with ApplicationData properties TimePlayed, LastPlayed, and FileSize Replaced double-based TimePlayed property with TimeSpan?-based one in ApplicationData and ApplicationMetadata Added a migration for TimePlayed, just like in #4861 Consolidated ApplicationData's FileSize* properties into one FileSize property Added a formatting/parsing helper class ValueFormatUtils for TimeSpans, DateTimes, and file sizes Added new value converters for TimeSpans and file sizes for the Avalonia UI Added TimePlayedSortComparer Fixed sort order in LastPlayedSortComparer Fixed sort order for ApplicationData fields TimePlayed, LastPlayed, and FileSize Fixed crashes caused by SortHelper Replaced SystemInfo.ToMiBString with ToGiBString backed by ValueFormatUtils Replaced SaveModel.GetSizeString() with ValueFormatUtils * Additional ApplicationLibrary changes that got lost in the last commit * Removed unneeded usings * Removed converters as they are no longer needed * Updated comment on FormatDateTime * Removed base10 parameter from ValueFormatUtils FormatFileSize now always returns base 2 values with base 10 units Made ParseFileSize capable of parsing both base 2 and base 10 units * Removed nullable attribute from TimePlayed property Centralized TimePlayed update code into ApplicationMetadata * Changed UpdateTimePlayed() to use TimeSpan logic * Removed JsonIgnore attributes from ApplicationData * Implemented requested format changes * Fixed mistakes in method documentation comments * Made it so the Last Played value "Never" is localized in the Avalonia UI * Implemented suggestions * Remove unused import * Did a comment refinement pass in ValueFormatUtils.cs * Reordered ValueFormatUtils methods and sorted them into #regions * Integrated functionality from #5056 Also removed Logger print from last_played migration code * Implemented suggestions * Moved ValueFormatUtils and SystemInfo to namespace Ryujinx.Ui.Common * common: Respect proper value format convention and use base10 by default This could be discuss again in another issue/PR, for now revert to the previous behavior. Signed-off-by: Mary Guillemard <mary@mary.zone> --------- Signed-off-by: Mary Guillemard <mary@mary.zone> Co-authored-by: TSR Berry <20988865+TSRBerry@users.noreply.github.com> Co-authored-by: Mary Guillemard <mary@mary.zone>
This commit is contained in:
parent
617c5700ca
commit
623604c391
20 changed files with 398 additions and 243 deletions
|
@ -9,10 +9,9 @@ using LibHac.Tools.FsSystem;
|
|||
using LibHac.Tools.FsSystem.NcaUtils;
|
||||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.HLE.FileSystem;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ryujinx.Ui.App.Common
|
||||
{
|
||||
|
@ -24,29 +23,18 @@ namespace Ryujinx.Ui.App.Common
|
|||
public string TitleId { get; set; }
|
||||
public string Developer { get; set; }
|
||||
public string Version { get; set; }
|
||||
public string TimePlayed { get; set; }
|
||||
public double TimePlayedNum { get; set; }
|
||||
public TimeSpan TimePlayed { get; set; }
|
||||
public DateTime? LastPlayed { get; set; }
|
||||
public string FileExtension { get; set; }
|
||||
public string FileSize { get; set; }
|
||||
public double FileSizeBytes { get; set; }
|
||||
public long FileSize { get; set; }
|
||||
public string Path { get; set; }
|
||||
public BlitStruct<ApplicationControlProperty> ControlHolder { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public string LastPlayedString
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!LastPlayed.HasValue)
|
||||
{
|
||||
// TODO: maybe put localized string here instead of just "Never"
|
||||
return "Never";
|
||||
}
|
||||
public string TimePlayedString => ValueFormatUtils.FormatTimeSpan(TimePlayed);
|
||||
|
||||
return LastPlayed.Value.ToLocalTime().ToString(CultureInfo.CurrentCulture);
|
||||
}
|
||||
}
|
||||
public string LastPlayedString => ValueFormatUtils.FormatDateTime(LastPlayed);
|
||||
|
||||
public string FileSizeString => ValueFormatUtils.FormatFileSize(FileSize);
|
||||
|
||||
public static string GetApplicationBuildId(VirtualFileSystem virtualFileSystem, string titleFilePath)
|
||||
{
|
||||
|
|
|
@ -155,7 +155,7 @@ namespace Ryujinx.Ui.App.Common
|
|||
return;
|
||||
}
|
||||
|
||||
double fileSize = new FileInfo(applicationPath).Length * 0.000000000931;
|
||||
long fileSize = new FileInfo(applicationPath).Length;
|
||||
string titleName = "Unknown";
|
||||
string titleId = "0000000000000000";
|
||||
string developer = "Unknown";
|
||||
|
@ -425,25 +425,25 @@ namespace Ryujinx.Ui.App.Common
|
|||
{
|
||||
appMetadata.Title = titleName;
|
||||
|
||||
if (appMetadata.LastPlayedOld == default || appMetadata.LastPlayed.HasValue)
|
||||
// Only do the migration if time_played has a value and timespan_played hasn't been updated yet.
|
||||
if (appMetadata.TimePlayedOld != default && appMetadata.TimePlayed == TimeSpan.Zero)
|
||||
{
|
||||
// Don't do the migration if last_played doesn't exist or last_played_utc already has a value.
|
||||
return;
|
||||
appMetadata.TimePlayed = TimeSpan.FromSeconds(appMetadata.TimePlayedOld);
|
||||
appMetadata.TimePlayedOld = default;
|
||||
}
|
||||
|
||||
// Migrate from string-based last_played to DateTime-based last_played_utc.
|
||||
if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
|
||||
// Only do the migration if last_played has a value and last_played_utc doesn't exist yet.
|
||||
if (appMetadata.LastPlayedOld != default && !appMetadata.LastPlayed.HasValue)
|
||||
{
|
||||
Logger.Info?.Print(LogClass.Application, $"last_played found: \"{appMetadata.LastPlayedOld}\", migrating to last_played_utc");
|
||||
appMetadata.LastPlayed = lastPlayedOldParsed;
|
||||
// Migrate from string-based last_played to DateTime-based last_played_utc.
|
||||
if (DateTime.TryParse(appMetadata.LastPlayedOld, out DateTime lastPlayedOldParsed))
|
||||
{
|
||||
appMetadata.LastPlayed = lastPlayedOldParsed;
|
||||
|
||||
// Migration successful: deleting last_played from the metadata file.
|
||||
appMetadata.LastPlayedOld = default;
|
||||
}
|
||||
|
||||
// Migration successful: deleting last_played from the metadata file.
|
||||
appMetadata.LastPlayedOld = default;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Migration failed: emitting warning but leaving the unparsable value in the metadata file so the user can fix it.
|
||||
Logger.Warning?.Print(LogClass.Application, $"Last played string \"{appMetadata.LastPlayedOld}\" is invalid for current system culture, skipping (did current culture change?)");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -455,12 +455,10 @@ namespace Ryujinx.Ui.App.Common
|
|||
TitleId = titleId,
|
||||
Developer = developer,
|
||||
Version = version,
|
||||
TimePlayed = ConvertSecondsToFormattedString(appMetadata.TimePlayed),
|
||||
TimePlayedNum = appMetadata.TimePlayed,
|
||||
TimePlayed = appMetadata.TimePlayed,
|
||||
LastPlayed = appMetadata.LastPlayed,
|
||||
FileExtension = Path.GetExtension(applicationPath).ToUpper().Remove(0, 1),
|
||||
FileSize = (fileSize < 1) ? (fileSize * 1024).ToString("0.##") + " MiB" : fileSize.ToString("0.##") + " GiB",
|
||||
FileSizeBytes = fileSize,
|
||||
FileExtension = Path.GetExtension(applicationPath).TrimStart('.').ToUpper(),
|
||||
FileSize = fileSize,
|
||||
Path = applicationPath,
|
||||
ControlHolder = controlHolder,
|
||||
};
|
||||
|
@ -716,31 +714,6 @@ namespace Ryujinx.Ui.App.Common
|
|||
return applicationIcon ?? _ncaIcon;
|
||||
}
|
||||
|
||||
private static string ConvertSecondsToFormattedString(double seconds)
|
||||
{
|
||||
TimeSpan time = TimeSpan.FromSeconds(seconds);
|
||||
|
||||
string timeString;
|
||||
if (time.Days != 0)
|
||||
{
|
||||
timeString = $"{time.Days}d {time.Hours:D2}h {time.Minutes:D2}m";
|
||||
}
|
||||
else if (time.Hours != 0)
|
||||
{
|
||||
timeString = $"{time.Hours:D2}h {time.Minutes:D2}m";
|
||||
}
|
||||
else if (time.Minutes != 0)
|
||||
{
|
||||
timeString = $"{time.Minutes:D2}m";
|
||||
}
|
||||
else
|
||||
{
|
||||
timeString = "Never";
|
||||
}
|
||||
|
||||
return timeString;
|
||||
}
|
||||
|
||||
private void GetGameInformation(ref ApplicationControlProperty controlData, out string titleName, out string titleId, out string publisher, out string version)
|
||||
{
|
||||
_ = Enum.TryParse(_desiredTitleLanguage.ToString(), out TitleLanguage desiredTitleLanguage);
|
||||
|
|
|
@ -7,13 +7,45 @@ namespace Ryujinx.Ui.App.Common
|
|||
{
|
||||
public string Title { get; set; }
|
||||
public bool Favorite { get; set; }
|
||||
public double TimePlayed { get; set; }
|
||||
|
||||
[JsonPropertyName("timespan_played")]
|
||||
public TimeSpan TimePlayed { get; set; } = TimeSpan.Zero;
|
||||
|
||||
[JsonPropertyName("last_played_utc")]
|
||||
public DateTime? LastPlayed { get; set; } = null;
|
||||
|
||||
[JsonPropertyName("time_played")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public double TimePlayedOld { get; set; }
|
||||
|
||||
[JsonPropertyName("last_played")]
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public string LastPlayedOld { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="LastPlayed"/>. Call this before launching a game.
|
||||
/// </summary>
|
||||
public void UpdatePreGame()
|
||||
{
|
||||
LastPlayed = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates <see cref="LastPlayed"/> and <see cref="TimePlayed"/>. Call this after a game ends.
|
||||
/// </summary>
|
||||
public void UpdatePostGame()
|
||||
{
|
||||
DateTime? prevLastPlayed = LastPlayed;
|
||||
UpdatePreGame();
|
||||
|
||||
if (!prevLastPlayed.HasValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TimeSpan diff = DateTime.UtcNow - prevLastPlayed.Value;
|
||||
double newTotalSeconds = TimePlayed.Add(diff).TotalSeconds;
|
||||
TimePlayed = TimeSpan.FromSeconds(Math.Round(newTotalSeconds, MidpointRounding.AwayFromZero));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
219
src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs
Normal file
219
src/Ryujinx.Ui.Common/Helper/ValueFormatUtils.cs
Normal file
|
@ -0,0 +1,219 @@
|
|||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
||||
namespace Ryujinx.Ui.Common.Helper
|
||||
{
|
||||
public static class ValueFormatUtils
|
||||
{
|
||||
private static readonly string[] _fileSizeUnitStrings =
|
||||
{
|
||||
"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", // Base 10 units, used for formatting and parsing
|
||||
"KB", "MB", "GB", "TB", "PB", "EB", // Base 2 units, used for parsing legacy values
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Used by <see cref="FormatFileSize"/>.
|
||||
/// </summary>
|
||||
public enum FileSizeUnits
|
||||
{
|
||||
Auto = -1,
|
||||
Bytes = 0,
|
||||
Kibibytes = 1,
|
||||
Mebibytes = 2,
|
||||
Gibibytes = 3,
|
||||
Tebibytes = 4,
|
||||
Pebibytes = 5,
|
||||
Exbibytes = 6,
|
||||
Kilobytes = 7,
|
||||
Megabytes = 8,
|
||||
Gigabytes = 9,
|
||||
Terabytes = 10,
|
||||
Petabytes = 11,
|
||||
Exabytes = 12,
|
||||
}
|
||||
|
||||
private const double SizeBase10 = 1000;
|
||||
private const double SizeBase2 = 1024;
|
||||
private const int UnitEBIndex = 6;
|
||||
|
||||
#region Value formatters
|
||||
|
||||
/// <summary>
|
||||
/// Creates a human-readable string from a <see cref="TimeSpan"/>.
|
||||
/// </summary>
|
||||
/// <param name="timeSpan">The <see cref="TimeSpan"/> to be formatted.</param>
|
||||
/// <returns>A formatted string that can be displayed in the UI.</returns>
|
||||
public static string FormatTimeSpan(TimeSpan? timeSpan)
|
||||
{
|
||||
if (!timeSpan.HasValue || timeSpan.Value.TotalSeconds < 1)
|
||||
{
|
||||
// Game was never played
|
||||
return TimeSpan.Zero.ToString("c", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
if (timeSpan.Value.TotalDays < 1)
|
||||
{
|
||||
// Game was played for less than a day
|
||||
return timeSpan.Value.ToString("c", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
// Game was played for more than a day
|
||||
TimeSpan onlyTime = timeSpan.Value.Subtract(TimeSpan.FromDays(timeSpan.Value.Days));
|
||||
string onlyTimeString = onlyTime.ToString("c", CultureInfo.InvariantCulture);
|
||||
|
||||
return $"{timeSpan.Value.Days}d, {onlyTimeString}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a human-readable string from a <see cref="DateTime"/>.
|
||||
/// </summary>
|
||||
/// <param name="utcDateTime">The <see cref="DateTime"/> to be formatted. This is expected to be UTC-based.</param>
|
||||
/// <param name="culture">The <see cref="CultureInfo"/> that's used in formatting. Defaults to <see cref="CultureInfo.CurrentCulture"/>.</param>
|
||||
/// <returns>A formatted string that can be displayed in the UI.</returns>
|
||||
public static string FormatDateTime(DateTime? utcDateTime, CultureInfo culture = null)
|
||||
{
|
||||
culture ??= CultureInfo.CurrentCulture;
|
||||
|
||||
if (!utcDateTime.HasValue)
|
||||
{
|
||||
// In the Avalonia UI, this is turned into a localized version of "Never" by LocalizedNeverConverter.
|
||||
return "Never";
|
||||
}
|
||||
|
||||
return utcDateTime.Value.ToLocalTime().ToString(culture);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a human-readable file size string.
|
||||
/// </summary>
|
||||
/// <param name="size">The file size in bytes.</param>
|
||||
/// <param name="forceUnit">Formats the passed size value as this unit, bypassing the automatic unit choice.</param>
|
||||
/// <returns>A human-readable file size string.</returns>
|
||||
public static string FormatFileSize(long size, FileSizeUnits forceUnit = FileSizeUnits.Auto)
|
||||
{
|
||||
if (size <= 0)
|
||||
{
|
||||
return $"0 {_fileSizeUnitStrings[0]}";
|
||||
}
|
||||
|
||||
int unitIndex = (int)forceUnit;
|
||||
if (forceUnit == FileSizeUnits.Auto)
|
||||
{
|
||||
unitIndex = Convert.ToInt32(Math.Floor(Math.Log(size, SizeBase10)));
|
||||
|
||||
// Apply an upper bound so that exabytes are the biggest unit used when formatting.
|
||||
if (unitIndex > UnitEBIndex)
|
||||
{
|
||||
unitIndex = UnitEBIndex;
|
||||
}
|
||||
}
|
||||
|
||||
double sizeRounded;
|
||||
|
||||
if (unitIndex > UnitEBIndex)
|
||||
{
|
||||
sizeRounded = Math.Round(size / Math.Pow(SizeBase10, unitIndex - UnitEBIndex), 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
sizeRounded = Math.Round(size / Math.Pow(SizeBase2, unitIndex), 1);
|
||||
}
|
||||
|
||||
string sizeFormatted = sizeRounded.ToString(CultureInfo.InvariantCulture);
|
||||
|
||||
return $"{sizeFormatted} {_fileSizeUnitStrings[unitIndex]}";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Value parsers
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string generated by <see cref="FormatTimeSpan"/> and returns the original <see cref="TimeSpan"/>.
|
||||
/// </summary>
|
||||
/// <param name="timeSpanString">A string representing a <see cref="TimeSpan"/>.</param>
|
||||
/// <returns>A <see cref="TimeSpan"/> object. If the input string couldn't been parsed, <see cref="TimeSpan.Zero"/> is returned.</returns>
|
||||
public static TimeSpan ParseTimeSpan(string timeSpanString)
|
||||
{
|
||||
TimeSpan returnTimeSpan = TimeSpan.Zero;
|
||||
|
||||
// An input string can either look like "01:23:45" or "1d, 01:23:45" if the timespan represents a duration of more than a day.
|
||||
// Here, we split the input string to check if it's the former or the latter.
|
||||
var valueSplit = timeSpanString.Split(", ");
|
||||
if (valueSplit.Length > 1)
|
||||
{
|
||||
var dayPart = valueSplit[0].Split("d")[0];
|
||||
if (int.TryParse(dayPart, out int days))
|
||||
{
|
||||
returnTimeSpan = returnTimeSpan.Add(TimeSpan.FromDays(days));
|
||||
}
|
||||
}
|
||||
|
||||
if (TimeSpan.TryParse(valueSplit.Last(), out TimeSpan parsedTimeSpan))
|
||||
{
|
||||
returnTimeSpan = returnTimeSpan.Add(parsedTimeSpan);
|
||||
}
|
||||
|
||||
return returnTimeSpan;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string generated by <see cref="FormatDateTime"/> and returns the original <see cref="DateTime"/>.
|
||||
/// </summary>
|
||||
/// <param name="dateTimeString">The string representing a <see cref="DateTime"/>.</param>
|
||||
/// <returns>A <see cref="DateTime"/> object. If the input string couldn't be parsed, <see cref="DateTime.UnixEpoch"/> is returned.</returns>
|
||||
public static DateTime ParseDateTime(string dateTimeString)
|
||||
{
|
||||
if (!DateTime.TryParse(dateTimeString, CultureInfo.CurrentCulture, out DateTime parsedDateTime))
|
||||
{
|
||||
// Games that were never played are supposed to appear before the oldest played games in the list,
|
||||
// so returning DateTime.UnixEpoch here makes sense.
|
||||
return DateTime.UnixEpoch;
|
||||
}
|
||||
|
||||
return parsedDateTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string generated by <see cref="FormatFileSize"/> and returns a <see cref="long"/> representing a number of bytes.
|
||||
/// </summary>
|
||||
/// <param name="sizeString">A string representing a file size formatted with <see cref="FormatFileSize"/>.</param>
|
||||
/// <returns>A <see cref="long"/> representing a number of bytes.</returns>
|
||||
public static long ParseFileSize(string sizeString)
|
||||
{
|
||||
// Enumerating over the units backwards because otherwise, sizeString.EndsWith("B") would exit the loop in the first iteration.
|
||||
for (int i = _fileSizeUnitStrings.Length - 1; i >= 0; i--)
|
||||
{
|
||||
string unit = _fileSizeUnitStrings[i];
|
||||
if (!sizeString.EndsWith(unit))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string numberString = sizeString.Split(" ")[0];
|
||||
if (!double.TryParse(numberString, CultureInfo.InvariantCulture, out double number))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
double sizeBase = SizeBase2;
|
||||
|
||||
// If the unit index is one that points to a base 10 unit in the FileSizeUnitStrings array, subtract 6 to arrive at a usable power value.
|
||||
if (i > UnitEBIndex)
|
||||
{
|
||||
i -= UnitEBIndex;
|
||||
sizeBase = SizeBase10;
|
||||
}
|
||||
|
||||
number *= Math.Pow(sizeBase, i);
|
||||
|
||||
return Convert.ToInt64(number);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
85
src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs
Normal file
85
src/Ryujinx.Ui.Common/SystemInfo/LinuxSystemInfo.cs
Normal file
|
@ -0,0 +1,85 @@
|
|||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Ui.Common.SystemInfo
|
||||
{
|
||||
[SupportedOSPlatform("linux")]
|
||||
class LinuxSystemInfo : SystemInfo
|
||||
{
|
||||
internal LinuxSystemInfo()
|
||||
{
|
||||
string cpuName = GetCpuidCpuName();
|
||||
|
||||
if (cpuName == null)
|
||||
{
|
||||
var cpuDict = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["model name"] = null,
|
||||
["Processor"] = null,
|
||||
["Hardware"] = null,
|
||||
};
|
||||
|
||||
ParseKeyValues("/proc/cpuinfo", cpuDict);
|
||||
|
||||
cpuName = cpuDict["model name"] ?? cpuDict["Processor"] ?? cpuDict["Hardware"] ?? "Unknown";
|
||||
}
|
||||
|
||||
var memDict = new Dictionary<string, string>(StringComparer.Ordinal)
|
||||
{
|
||||
["MemTotal"] = null,
|
||||
["MemAvailable"] = null,
|
||||
};
|
||||
|
||||
ParseKeyValues("/proc/meminfo", memDict);
|
||||
|
||||
// Entries are in KiB
|
||||
ulong.TryParse(memDict["MemTotal"]?.Split(' ')[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong totalKiB);
|
||||
ulong.TryParse(memDict["MemAvailable"]?.Split(' ')[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out ulong availableKiB);
|
||||
|
||||
CpuName = $"{cpuName} ; {LogicalCoreCount} logical";
|
||||
RamTotal = totalKiB * 1024;
|
||||
RamAvailable = availableKiB * 1024;
|
||||
}
|
||||
|
||||
private static void ParseKeyValues(string filePath, Dictionary<string, string> itemDict)
|
||||
{
|
||||
if (!File.Exists(filePath))
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"File \"{filePath}\" not found");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int count = itemDict.Count;
|
||||
|
||||
using StreamReader file = new(filePath);
|
||||
|
||||
string line;
|
||||
while ((line = file.ReadLine()) != null)
|
||||
{
|
||||
string[] kvPair = line.Split(':', 2, StringSplitOptions.TrimEntries);
|
||||
|
||||
if (kvPair.Length < 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = kvPair[0];
|
||||
|
||||
if (itemDict.TryGetValue(key, out string value) && value == null)
|
||||
{
|
||||
itemDict[key] = kvPair[1];
|
||||
|
||||
if (--count <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
164
src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs
Normal file
164
src/Ryujinx.Ui.Common/SystemInfo/MacOSSystemInfo.cs
Normal file
|
@ -0,0 +1,164 @@
|
|||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Ui.Common.SystemInfo
|
||||
{
|
||||
[SupportedOSPlatform("macos")]
|
||||
partial class MacOSSystemInfo : SystemInfo
|
||||
{
|
||||
internal MacOSSystemInfo()
|
||||
{
|
||||
if (SysctlByName("kern.osversion", out string buildRevision) != 0)
|
||||
{
|
||||
buildRevision = "Unknown Build";
|
||||
}
|
||||
|
||||
OsDescription = $"macOS {Environment.OSVersion.Version} ({buildRevision}) ({RuntimeInformation.OSArchitecture})";
|
||||
|
||||
string cpuName = GetCpuidCpuName();
|
||||
|
||||
if (cpuName == null && SysctlByName("machdep.cpu.brand_string", out cpuName) != 0)
|
||||
{
|
||||
cpuName = "Unknown";
|
||||
}
|
||||
|
||||
ulong totalRAM = 0;
|
||||
|
||||
if (SysctlByName("hw.memsize", ref totalRAM) != 0) // Bytes
|
||||
{
|
||||
totalRAM = 0;
|
||||
}
|
||||
|
||||
CpuName = $"{cpuName} ; {LogicalCoreCount} logical";
|
||||
RamTotal = totalRAM;
|
||||
RamAvailable = GetVMInfoAvailableMemory();
|
||||
}
|
||||
|
||||
static ulong GetVMInfoAvailableMemory()
|
||||
{
|
||||
var port = mach_host_self();
|
||||
|
||||
uint pageSize = 0;
|
||||
var result = host_page_size(port, ref pageSize);
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to query Available RAM. host_page_size() error = {result}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int Flavor = 4; // HOST_VM_INFO64
|
||||
uint count = (uint)(Marshal.SizeOf<VMStatistics64>() / sizeof(int)); // HOST_VM_INFO64_COUNT
|
||||
VMStatistics64 stats = new();
|
||||
result = host_statistics64(port, Flavor, ref stats, ref count);
|
||||
|
||||
if (result != 0)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"Failed to query Available RAM. host_statistics64() error = {result}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (ulong)(stats.FreeCount + stats.InactiveCount) * pageSize;
|
||||
}
|
||||
|
||||
private const string SystemLibraryName = "libSystem.dylib";
|
||||
|
||||
[LibraryImport(SystemLibraryName, SetLastError = true)]
|
||||
private static partial int sysctlbyname([MarshalAs(UnmanagedType.LPStr)] string name, IntPtr oldValue, ref ulong oldSize, IntPtr newValue, ulong newValueSize);
|
||||
|
||||
private static int SysctlByName(string name, IntPtr oldValue, ref ulong oldSize)
|
||||
{
|
||||
if (sysctlbyname(name, oldValue, ref oldSize, IntPtr.Zero, 0) == -1)
|
||||
{
|
||||
int err = Marshal.GetLastWin32Error();
|
||||
|
||||
Logger.Error?.Print(LogClass.Application, $"Cannot retrieve '{name}'. Error Code {err}");
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int SysctlByName<T>(string name, ref T oldValue)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
ulong oldValueSize = (ulong)Unsafe.SizeOf<T>();
|
||||
|
||||
return SysctlByName(name, (IntPtr)Unsafe.AsPointer(ref oldValue), ref oldValueSize);
|
||||
}
|
||||
}
|
||||
|
||||
private static int SysctlByName(string name, out string oldValue)
|
||||
{
|
||||
oldValue = default;
|
||||
|
||||
ulong strSize = 0;
|
||||
|
||||
int res = SysctlByName(name, IntPtr.Zero, ref strSize);
|
||||
|
||||
if (res == 0)
|
||||
{
|
||||
byte[] rawData = new byte[strSize];
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* rawDataPtr = rawData)
|
||||
{
|
||||
res = SysctlByName(name, (IntPtr)rawDataPtr, ref strSize);
|
||||
}
|
||||
|
||||
if (res == 0)
|
||||
{
|
||||
oldValue = Encoding.ASCII.GetString(rawData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
[LibraryImport(SystemLibraryName, SetLastError = true)]
|
||||
private static partial uint mach_host_self();
|
||||
|
||||
[LibraryImport(SystemLibraryName, SetLastError = true)]
|
||||
private static partial int host_page_size(uint host, ref uint out_page_size);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 8)]
|
||||
struct VMStatistics64
|
||||
{
|
||||
public uint FreeCount;
|
||||
public uint ActiveCount;
|
||||
public uint InactiveCount;
|
||||
public uint WireCount;
|
||||
public ulong ZeroFillCount;
|
||||
public ulong Reactivations;
|
||||
public ulong Pageins;
|
||||
public ulong Pageouts;
|
||||
public ulong Faults;
|
||||
public ulong CowFaults;
|
||||
public ulong Lookups;
|
||||
public ulong Hits;
|
||||
public ulong Purges;
|
||||
public uint PurgeableCount;
|
||||
public uint SpeculativeCount;
|
||||
public ulong Decompressions;
|
||||
public ulong Compressions;
|
||||
public ulong Swapins;
|
||||
public ulong Swapouts;
|
||||
public uint CompressorPageCount;
|
||||
public uint ThrottledCount;
|
||||
public uint ExternalPageCount;
|
||||
public uint InternalPageCount;
|
||||
public ulong TotalUncompressedPagesInCompressor;
|
||||
}
|
||||
|
||||
[LibraryImport(SystemLibraryName, SetLastError = true)]
|
||||
private static partial int host_statistics64(uint hostPriv, int hostFlavor, ref VMStatistics64 hostInfo64Out, ref uint hostInfo64OutCnt);
|
||||
}
|
||||
}
|
79
src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs
Normal file
79
src/Ryujinx.Ui.Common/SystemInfo/SystemInfo.cs
Normal file
|
@ -0,0 +1,79 @@
|
|||
using Ryujinx.Common.Logging;
|
||||
using Ryujinx.Ui.Common.Helper;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using System.Text;
|
||||
|
||||
namespace Ryujinx.Ui.Common.SystemInfo
|
||||
{
|
||||
public class SystemInfo
|
||||
{
|
||||
public string OsDescription { get; protected set; }
|
||||
public string CpuName { get; protected set; }
|
||||
public ulong RamTotal { get; protected set; }
|
||||
public ulong RamAvailable { get; protected set; }
|
||||
protected static int LogicalCoreCount => Environment.ProcessorCount;
|
||||
|
||||
protected SystemInfo()
|
||||
{
|
||||
OsDescription = $"{RuntimeInformation.OSDescription} ({RuntimeInformation.OSArchitecture})";
|
||||
CpuName = "Unknown";
|
||||
}
|
||||
|
||||
private static string ToGBString(ulong bytesValue) => (bytesValue == 0) ? "Unknown" : ValueFormatUtils.FormatFileSize((long)bytesValue, ValueFormatUtils.FileSizeUnits.Gibibytes);
|
||||
|
||||
public void Print()
|
||||
{
|
||||
Logger.Notice.Print(LogClass.Application, $"Operating System: {OsDescription}");
|
||||
Logger.Notice.Print(LogClass.Application, $"CPU: {CpuName}");
|
||||
Logger.Notice.Print(LogClass.Application, $"RAM: Total {ToGBString(RamTotal)} ; Available {ToGBString(RamAvailable)}");
|
||||
}
|
||||
|
||||
public static SystemInfo Gather()
|
||||
{
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
return new WindowsSystemInfo();
|
||||
}
|
||||
else if (OperatingSystem.IsLinux())
|
||||
{
|
||||
return new LinuxSystemInfo();
|
||||
}
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
{
|
||||
return new MacOSSystemInfo();
|
||||
}
|
||||
|
||||
Logger.Error?.Print(LogClass.Application, "SystemInfo unsupported on this platform");
|
||||
|
||||
return new SystemInfo();
|
||||
}
|
||||
|
||||
// x86 exposes a 48 byte ASCII "CPU brand" string via CPUID leaves 0x80000002-0x80000004.
|
||||
internal static string GetCpuidCpuName()
|
||||
{
|
||||
if (!X86Base.IsSupported)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if CPU supports the query
|
||||
if ((uint)X86Base.CpuId(unchecked((int)0x80000000), 0).Eax < 0x80000004)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
int[] regs = new int[12];
|
||||
|
||||
for (uint i = 0; i < 3; ++i)
|
||||
{
|
||||
(regs[4 * i], regs[4 * i + 1], regs[4 * i + 2], regs[4 * i + 3]) = X86Base.CpuId((int)(0x80000002 + i), 0);
|
||||
}
|
||||
|
||||
string name = Encoding.ASCII.GetString(MemoryMarshal.Cast<int, byte>(regs)).Replace('\0', ' ').Trim();
|
||||
|
||||
return string.IsNullOrEmpty(name) ? null : name;
|
||||
}
|
||||
}
|
||||
}
|
87
src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs
Normal file
87
src/Ryujinx.Ui.Common/SystemInfo/WindowsSystemInfo.cs
Normal file
|
@ -0,0 +1,87 @@
|
|||
using Ryujinx.Common.Logging;
|
||||
using System;
|
||||
using System.Management;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
|
||||
namespace Ryujinx.Ui.Common.SystemInfo
|
||||
{
|
||||
[SupportedOSPlatform("windows")]
|
||||
partial class WindowsSystemInfo : SystemInfo
|
||||
{
|
||||
internal WindowsSystemInfo()
|
||||
{
|
||||
CpuName = $"{GetCpuidCpuName() ?? GetCpuNameWMI()} ; {LogicalCoreCount} logical"; // WMI is very slow
|
||||
(RamTotal, RamAvailable) = GetMemoryStats();
|
||||
}
|
||||
|
||||
private static (ulong Total, ulong Available) GetMemoryStats()
|
||||
{
|
||||
MemoryStatusEx memStatus = new();
|
||||
if (GlobalMemoryStatusEx(ref memStatus))
|
||||
{
|
||||
return (memStatus.TotalPhys, memStatus.AvailPhys); // Bytes
|
||||
}
|
||||
|
||||
Logger.Error?.Print(LogClass.Application, $"GlobalMemoryStatusEx failed. Error {Marshal.GetLastWin32Error():X}");
|
||||
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
private static string GetCpuNameWMI()
|
||||
{
|
||||
ManagementObjectCollection cpuObjs = GetWMIObjects("root\\CIMV2", "SELECT * FROM Win32_Processor");
|
||||
|
||||
if (cpuObjs != null)
|
||||
{
|
||||
foreach (var cpuObj in cpuObjs)
|
||||
{
|
||||
return cpuObj["Name"].ToString().Trim();
|
||||
}
|
||||
}
|
||||
|
||||
return Environment.GetEnvironmentVariable("PROCESSOR_IDENTIFIER").Trim();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MemoryStatusEx
|
||||
{
|
||||
public uint Length;
|
||||
public uint MemoryLoad;
|
||||
public ulong TotalPhys;
|
||||
public ulong AvailPhys;
|
||||
public ulong TotalPageFile;
|
||||
public ulong AvailPageFile;
|
||||
public ulong TotalVirtual;
|
||||
public ulong AvailVirtual;
|
||||
public ulong AvailExtendedVirtual;
|
||||
|
||||
public MemoryStatusEx()
|
||||
{
|
||||
Length = (uint)Marshal.SizeOf<MemoryStatusEx>();
|
||||
}
|
||||
}
|
||||
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static partial bool GlobalMemoryStatusEx(ref MemoryStatusEx lpBuffer);
|
||||
|
||||
private static ManagementObjectCollection GetWMIObjects(string scope, string query)
|
||||
{
|
||||
try
|
||||
{
|
||||
return new ManagementObjectSearcher(scope, query).Get();
|
||||
}
|
||||
catch (PlatformNotSupportedException ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"WMI isn't available : {ex.Message}");
|
||||
}
|
||||
catch (COMException ex)
|
||||
{
|
||||
Logger.Error?.Print(LogClass.Application, $"WMI isn't available : {ex.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue