forked from TASEmulators/BizHawk
-
Notifications
You must be signed in to change notification settings - Fork 0
/
OSTailoredCode.cs
287 lines (244 loc) · 10.3 KB
/
OSTailoredCode.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using BizHawk.Common.StringExtensions;
using static BizHawk.Common.LoaderApiImports;
namespace BizHawk.Common
{
public static class OSTailoredCode
{
public static readonly DistinctOS CurrentOS;
public static readonly bool IsUnixHost;
static OSTailoredCode()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
CurrentOS = DistinctOS.Linux;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
CurrentOS = DistinctOS.macOS;
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
CurrentOS = DistinctOS.Windows;
}
else if (RuntimeInformation.OSDescription.ToUpperInvariant().Contains("BSD", StringComparison.Ordinal))
{
CurrentOS = DistinctOS.BSD;
}
else
{
CurrentOS = DistinctOS.Unknown;
}
IsUnixHost = CurrentOS != DistinctOS.Windows;
}
private static readonly Lazy<(WindowsVersion, Version?)?> _HostWindowsVersion = new(() =>
{
static string? GetRegValue(string key)
{
try
{
using var proc = ConstructSubshell("REG", $@"QUERY ""HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion"" /V {key}");
proc.Start();
return proc.StandardOutput.ReadToEnd().Split(new[] { "\r\n", "\n" }, StringSplitOptions.RemoveEmptyEntries)[1].Split(new[] { "\t", " " }, StringSplitOptions.RemoveEmptyEntries)[2];
}
catch (Exception)
{
// Education edition? Poor Group Policy setup? https://github.com/TASEmulators/BizHawk/issues/2972
return null;
}
}
if (CurrentOS != DistinctOS.Windows) return null;
Version rawWinVer = new(GetRegValue("CurrentVersion") ?? "6.3");
WindowsVersion winVer;
if (rawWinVer >= new Version(6, 3))
{
// Win8.1, Win10, and Win11 all have CurrentVersion == "6.3"
if ((GetRegValue("ProductName") ?? "Windows 10").Contains("Windows 10"))
{
// Win11 has ProductName == "Windows 10 Pro" MICROSOFT WHY https://stackoverflow.com/a/69922526 https://stackoverflow.com/a/70456554
// Version win10PlusVer = new(FileVersionInfo.GetVersionInfo(@"C:\Windows\System32\kernel32.dll").FileVersion.SubstringBefore(' ')); // bonus why: this doesn't work because the file's metadata wasn't updated
Version win10PlusVer = new(10, 0, int.TryParse(GetRegValue("CurrentBuild") ?? "19044", out var i) ? i : 19044); // still, leaving the Version wrapper here for when they inevitably do something stupid like decide to call Win11 11.0.x (or 10.1.x because they're incapable of doing anything sensible)
return (win10PlusVer < new Version(10, 0, 22000) ? WindowsVersion._10 : WindowsVersion._11, win10PlusVer);
}
// ...else we're on 8.1. Can't be bothered writing code for KB installed check, not that I have a Win8.1 machine to test on anyway, so it gets a free pass (though I suspect `CurrentBuild` would work here too). --yoshi
winVer = WindowsVersion._8_1;
}
else if (rawWinVer == new Version(6, 2)) winVer = WindowsVersion._8;
else if (rawWinVer == new Version(6, 1)) winVer = WindowsVersion._7;
// in reality, EmuHawk will not run on these OSes, but here they are for posterity
else if (rawWinVer == new Version(6, 0)) winVer = WindowsVersion.Vista;
else /*if (rawWinVer < new Version(6, 0))*/ winVer = WindowsVersion.XP;
return (winVer, null);
});
private static readonly Lazy<bool> _isWSL = new(() => IsUnixHost && SimpleSubshell("uname", "-r", "missing uname?").Contains("microsoft", StringComparison.InvariantCultureIgnoreCase));
public static (WindowsVersion Version, Version? Win10PlusVersion)? HostWindowsVersion => _HostWindowsVersion.Value;
public static bool IsWSL => _isWSL.Value;
private static readonly Lazy<bool> _isWine = new(() =>
{
if (IsUnixHost)
{
return false;
}
var ntdll = LinkedLibManager.LoadOrZero("ntdll.dll");
if (ntdll == IntPtr.Zero)
{
return false;
}
var isWine = LinkedLibManager.GetProcAddrOrZero(ntdll, "wine_get_version") != IntPtr.Zero;
LinkedLibManager.FreeByPtr(ntdll);
return isWine;
});
public static bool IsWine => _isWine.Value;
private static readonly Lazy<ILinkedLibManager> _LinkedLibManager = new(() => CurrentOS switch
{
DistinctOS.Linux => new LinuxLLManager(),
DistinctOS.macOS => new PosixLLManager(),
DistinctOS.Windows => new WindowsLLManager(),
DistinctOS.BSD => new PosixLLManager(),
DistinctOS.Unknown => throw new NotSupportedException("Cannot link libraries with Unknown OS"),
_ => throw new InvalidOperationException()
});
public static ILinkedLibManager LinkedLibManager => _LinkedLibManager.Value;
/// <remarks>this interface's inheritors hide OS-specific implementation details</remarks>
public interface ILinkedLibManager
{
int FreeByPtr(IntPtr hModule);
IntPtr GetProcAddrOrZero(IntPtr hModule, string procName);
/// <exception cref="InvalidOperationException">could not find symbol</exception>
IntPtr GetProcAddrOrThrow(IntPtr hModule, string procName);
IntPtr LoadOrZero(string dllToLoad);
/// <exception cref="InvalidOperationException">could not find library</exception>
IntPtr LoadOrThrow(string dllToLoad);
string GetErrorMessage();
}
private class LinuxLLManager : ILinkedLibManager
{
public int FreeByPtr(IntPtr hModule) => LinuxDlfcnImports.dlclose(hModule);
public IntPtr GetProcAddrOrZero(IntPtr hModule, string procName) => LinuxDlfcnImports.dlsym(hModule, procName);
public IntPtr GetProcAddrOrThrow(IntPtr hModule, string procName)
{
_ = LinuxDlfcnImports.dlerror(); // the Internet said to do this
var p = GetProcAddrOrZero(hModule, procName);
if (p != IntPtr.Zero) return p;
throw new InvalidOperationException($"error in dlsym: {GetErrorMessage()}");
}
public IntPtr LoadOrZero(string dllToLoad) => LinuxDlfcnImports.dlopen(dllToLoad, LinuxDlfcnImports.RTLD_NOW);
public IntPtr LoadOrThrow(string dllToLoad)
{
var ret = LoadOrZero(dllToLoad);
return ret != IntPtr.Zero
? ret
: throw new InvalidOperationException($"got null pointer from dlopen, error: {GetErrorMessage()}");
}
public string GetErrorMessage()
{
var errCharPtr = LinuxDlfcnImports.dlerror();
return errCharPtr == IntPtr.Zero ? "dlerror reported no error" : Marshal.PtrToStringAnsi(errCharPtr)!;
}
}
// this is just a copy paste of LinuxLLManager using PosixDlfcnImports instead of LinuxDlfcnImports
// TODO: probably could do some OOP magic so there isn't just a copy paste here
private class PosixLLManager : ILinkedLibManager
{
public int FreeByPtr(IntPtr hModule) => PosixDlfcnImports.dlclose(hModule);
public IntPtr GetProcAddrOrZero(IntPtr hModule, string procName) => PosixDlfcnImports.dlsym(hModule, procName);
public IntPtr GetProcAddrOrThrow(IntPtr hModule, string procName)
{
_ = PosixDlfcnImports.dlerror(); // the Internet said to do this
var p = GetProcAddrOrZero(hModule, procName);
if (p != IntPtr.Zero) return p;
throw new InvalidOperationException($"error in dlsym: {GetErrorMessage()}");
}
public IntPtr LoadOrZero(string dllToLoad) => PosixDlfcnImports.dlopen(dllToLoad, PosixDlfcnImports.RTLD_NOW);
public IntPtr LoadOrThrow(string dllToLoad)
{
var ret = LoadOrZero(dllToLoad);
return ret != IntPtr.Zero
? ret
: throw new InvalidOperationException($"got null pointer from dlopen, error: {GetErrorMessage()}");
}
public string GetErrorMessage()
{
var errCharPtr = PosixDlfcnImports.dlerror();
return errCharPtr == IntPtr.Zero ? "dlerror reported no error" : Marshal.PtrToStringAnsi(errCharPtr)!;
}
}
private class WindowsLLManager : ILinkedLibManager
{
public int FreeByPtr(IntPtr hModule) => FreeLibrary(hModule) ? 0 : 1;
public IntPtr GetProcAddrOrZero(IntPtr hModule, string procName) => GetProcAddress(hModule, procName);
public IntPtr GetProcAddrOrThrow(IntPtr hModule, string procName)
{
var ret = GetProcAddrOrZero(hModule, procName);
return ret != IntPtr.Zero ? ret : throw new InvalidOperationException($"got null pointer from {nameof(GetProcAddress)}, {GetErrorMessage()}");
}
public IntPtr LoadOrZero(string dllToLoad) => LoadLibraryW(dllToLoad);
public IntPtr LoadOrThrow(string dllToLoad)
{
var ret = LoadOrZero(dllToLoad);
return ret != IntPtr.Zero ? ret : throw new InvalidOperationException($"got null pointer from {nameof(LoadLibraryW)}, {GetErrorMessage()}");
}
public unsafe string GetErrorMessage()
{
var errCode = Win32Imports.GetLastError();
var buffer = stackalloc char[1024];
const int FORMAT_MESSAGE_FROM_SYSTEM = 0x1000;
var sz = Win32Imports.FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, IntPtr.Zero, errCode, 0, buffer, 1024, IntPtr.Zero);
return $"error code: 0x{errCode:X8}, error message: {new string(buffer, 0, sz)}";
}
}
public enum DistinctOS : byte
{
Linux,
macOS,
Windows,
BSD, // covering all the *BSDs
Unknown,
}
public enum WindowsVersion
{
XP,
Vista,
_7,
_8,
_8_1,
_10,
_11,
}
/// <param name="cmd">POSIX <c>$0</c></param>
/// <param name="args">POSIX <c>$*</c> (space-delimited)</param>
/// <param name="checkStdout">stdout is discarded if false</param>
/// <param name="checkStderr">stderr is discarded if false</param>
/// <remarks>OS is implicit and needs to be checked at callsite. Returned <see cref="Process"/> has not been started.</remarks>
public static Process ConstructSubshell(string cmd, string args, bool checkStdout = true, bool checkStderr = false)
=> new()
{
StartInfo = new()
{
Arguments = args,
CreateNoWindow = true,
FileName = cmd,
RedirectStandardError = checkStderr,
RedirectStandardInput = true,
RedirectStandardOutput = checkStdout,
UseShellExecute = false
}
};
/// <param name="cmd">POSIX <c>$0</c></param>
/// <param name="args">POSIX <c>$*</c> (space-delimited)</param>
/// <param name="noOutputMsg">used in exception</param>
/// <returns>first line of stdout</returns>
/// <exception cref="Exception">stdout is empty</exception>
/// <remarks>OS is implicit and needs to be checked at callsite</remarks>
public static string SimpleSubshell(string cmd, string args, string noOutputMsg)
{
using var proc = ConstructSubshell(cmd, args);
proc.Start();
var stdout = proc.StandardOutput;
if (stdout.EndOfStream) throw new($"{noOutputMsg} ({cmd} wrote nothing to stdout)");
return stdout.ReadLine()!;
}
}
}