using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; #if SMAPI_FOR_WINDOWS using System.Management; #endif using System.Runtime.InteropServices; using StardewModdingAPI.Toolkit.Utilities; namespace StardewModdingAPI.Toolkit.Framework { /// Provides low-level methods for fetching environment information. /// This is used by the SMAPI core before the toolkit DLL is available; most code should use instead. internal static class LowLevelEnvironmentUtility { /********* ** Fields *********/ /// Get the OS name from the system uname command. /// The buffer to fill with the resulting string. [DllImport("libc")] static extern int uname(IntPtr buffer); /********* ** Public methods *********/ /// Detect the current OS. public static string DetectPlatform() { switch (Environment.OSVersion.Platform) { case PlatformID.MacOSX: return nameof(Platform.Mac); case PlatformID.Unix when LowLevelEnvironmentUtility.IsRunningAndroid(): return nameof(Platform.Android); case PlatformID.Unix when LowLevelEnvironmentUtility.IsRunningMac(): return nameof(Platform.Mac); case PlatformID.Unix: return nameof(Platform.Linux); default: return nameof(Platform.Windows); } } /// Get the human-readable OS name and version. /// The current platform. [SuppressMessage("ReSharper", "EmptyGeneralCatchClause", Justification = "Error suppressed deliberately to fallback to default behaviour.")] public static string GetFriendlyPlatformName(string platform) { #if SMAPI_FOR_WINDOWS try { string? result = new ManagementObjectSearcher("SELECT Caption FROM Win32_OperatingSystem") .Get() .Cast() .Select(entry => entry.GetPropertyValue("Caption").ToString()) .FirstOrDefault(); return result ?? "Windows"; } catch { } #endif string name = Environment.OSVersion.ToString(); switch (platform) { case nameof(Platform.Android): name = $"Android {name}"; break; case nameof(Platform.Mac): name = $"macOS {name}"; break; } return name; } /// Get whether an executable is 64-bit. /// The absolute path to the assembly file. public static bool Is64BitAssembly(string path) { return AssemblyName.GetAssemblyName(path).ProcessorArchitecture != ProcessorArchitecture.X86; } /********* ** Private methods *********/ /// Detect whether the code is running on Android. /// /// This code is derived from https://stackoverflow.com/a/47521647/262123. It detects Android by calling the /// getprop system command to check for an Android-specific property. /// private static bool IsRunningAndroid() { using Process process = new() { StartInfo = { FileName = "getprop", Arguments = "ro.build.user", RedirectStandardOutput = true, UseShellExecute = false, CreateNoWindow = true } }; try { process.Start(); string output = process.StandardOutput.ReadToEnd(); return !string.IsNullOrWhiteSpace(output); } catch { return false; } } /// Detect whether the code is running on macOS. /// /// This code is derived from the Mono project (see System.Windows.Forms/System.Windows.Forms/XplatUI.cs). It detects macOS by calling the /// uname system command and checking the response, which is always 'Darwin' for macOS. /// private static bool IsRunningMac() { IntPtr buffer = IntPtr.Zero; try { buffer = Marshal.AllocHGlobal(8192); if (LowLevelEnvironmentUtility.uname(buffer) == 0) { string? os = Marshal.PtrToStringAnsi(buffer); return os == "Darwin"; } return false; } catch { return false; // default to Linux } finally { if (buffer != IntPtr.Zero) Marshal.FreeHGlobal(buffer); } } } }