improve Program startup, remove the OSTailoredCode duplication hack, use RuntimeInformation.IsOSPlatform for figuring out OS, use AppContext.BaseDirectory to get exe location (apparently even works with bundled assemblies in .net5+)

This commit is contained in:
CasualPokePlayer 2023-11-18 20:00:16 -08:00
parent eca0ca41a9
commit 859ea8e249
14 changed files with 339 additions and 267 deletions

View File

@ -22,7 +22,7 @@ namespace BizHawk.Bizware.Test
{
var firstAsm = Array.Find(AppDomain.CurrentDomain.GetAssemblies(), asm => asm.FullName == args.Name);
if (firstAsm is not null) return firstAsm;
var guessFilename = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "dll", $"{new AssemblyName(args.Name).Name}.dll");
var guessFilename = Path.Combine(AppContext.BaseDirectory, "dll", $"{new AssemblyName(args.Name).Name}.dll");
return File.Exists(guessFilename) ? Assembly.LoadFile(guessFilename) : null;
}
};

View File

@ -6,7 +6,6 @@
<PropertyGroup>
<ApplicationIcon>discohawk.ico</ApplicationIcon>
<Nullable>disable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Reference Include="System.Windows.Forms" />

View File

@ -3,74 +3,82 @@ using System.Runtime.InteropServices;
using System.Reflection;
using System.Collections.Generic;
using System.IO;
using BizHawk.Emulation.DiscSystem;
using System.Windows.Forms;
using OSTC = EXE_PROJECT.OSTailoredCode;
using BizHawk.Common;
using BizHawk.Emulation.DiscSystem;
namespace BizHawk.Client.DiscoHawk
{
internal static class Program
{
// Declared here instead of a more usual place to avoid dependencies on the more usual place
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetDllDirectoryW(string lpPathName);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeleteFileW(string lpFileName);
static Program()
{
if (OSTC.IsUnixHost)
// in case assembly resolution fails, such as if we moved them into the dll subdirectory, this event handler can reroute to them
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// for Unix, skip everything else and just wire up the event handler
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
// Windows needs extra considerations for the dll directory
// we can skip all this on non-Windows platforms
return;
}
// http://www.codeproject.com/Articles/310675/AppDomain-AssemblyResolve-Event-Tips
// this will look in subdirectory "dll" to load pinvoked stuff
string dllDir = Path.Combine(GetExeDirectoryAbsolute(), "dll");
SetDllDirectory(dllDir);
var dllDir = Path.Combine(AppContext.BaseDirectory, "dll");
SetDllDirectoryW(dllDir);
// in case assembly resolution fails, such as if we moved them into the dll subdirectory, this event handler can reroute to them
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
// but before we even try doing that, whack the MOTW from everything in that directory (that's a dll)
// otherwise, some people will have crashes at boot-up due to .net security disliking MOTW.
// some people are getting MOTW through a combination of browser used to download BizHawk, and program used to dearchive it
static void RemoveMOTW(string path) => DeleteFileW($"{path}:Zone.Identifier");
var todo = new Queue<DirectoryInfo>(new[] { new DirectoryInfo(dllDir) });
while (todo.Count != 0)
try
{
var di = todo.Dequeue();
foreach (var diSub in di.GetDirectories()) todo.Enqueue(diSub);
foreach (var fi in di.GetFiles("*.dll")) RemoveMOTW(fi.FullName);
foreach (var fi in di.GetFiles("*.exe")) RemoveMOTW(fi.FullName);
// but before we even try doing that, whack the MOTW from everything in that directory (that's a dll)
// otherwise, some people will have crashes at boot-up due to .net security disliking MOTW.
// some people are getting MOTW through a combination of browser used to download bizhawk, and program used to dearchive it
static void RemoveMOTW(string path) => DeleteFileW($"{path}:Zone.Identifier");
var todo = new Queue<DirectoryInfo>(new[] { new DirectoryInfo(dllDir) });
while (todo.Count != 0)
{
var di = todo.Dequeue();
foreach (var disub in di.GetDirectories()) todo.Enqueue(disub);
foreach (var fi in di.GetFiles("*.dll")) RemoveMOTW(fi.FullName);
foreach (var fi in di.GetFiles("*.exe")) RemoveMOTW(fi.FullName);
}
}
catch (Exception e)
{
Console.WriteLine($"MotW remover failed: {e}");
}
}
[STAThread]
private static void Main(string[] args)
{
SubMain(args);
}
// NoInlining should keep this code from getting jammed into Main() which would create dependencies on types which haven't been setup by the resolver yet... or something like that
[DllImport("user32.dll", SetLastError = true)]
public static extern bool ChangeWindowMessageFilterEx(IntPtr hWnd, uint msg, ChangeWindowMessageFilterExAction action, ref CHANGEFILTERSTRUCT changeInfo);
private static void SubMain(string[] args)
{
if (!OSTC.IsUnixHost)
if (!OSTailoredCode.IsUnixHost)
{
// MICROSOFT BROKE DRAG AND DROP IN WINDOWS 7. IT DOESN'T WORK ANYMORE
// WELL, OBVIOUSLY IT DOES SOMETIMES. I DON'T REMEMBER THE DETAILS OR WHY WE HAD TO DO THIS SHIT
// BUT THE FUNCTION WE NEED DOESN'T EXIST UNTIL WINDOWS 7, CONVENIENTLY
// SO CHECK FOR IT
IntPtr lib = OSTC.LinkedLibManager.LoadOrThrow("user32.dll");
IntPtr proc = OSTC.LinkedLibManager.GetProcAddrOrZero(lib, "ChangeWindowMessageFilterEx");
if (proc != IntPtr.Zero)
{
ChangeWindowMessageFilter(WM_DROPFILES, ChangeWindowMessageFilterFlags.Add);
ChangeWindowMessageFilter(WM_COPYDATA, ChangeWindowMessageFilterFlags.Add);
ChangeWindowMessageFilter(0x0049, ChangeWindowMessageFilterFlags.Add);
}
OSTC.LinkedLibManager.FreeByPtr(lib);
const uint WM_DROPFILES = 0x0233;
const uint WM_COPYDATA = 0x004A;
const uint WM_COPYGLOBALDATA = 0x0049;
WmImports.ChangeWindowMessageFilter(WM_DROPFILES, WmImports.ChangeWindowMessageFilterFlags.Add);
WmImports.ChangeWindowMessageFilter(WM_COPYDATA, WmImports.ChangeWindowMessageFilterFlags.Add);
WmImports.ChangeWindowMessageFilter(WM_COPYGLOBALDATA, WmImports.ChangeWindowMessageFilterFlags.Add);
}
// Do something for visuals, I guess
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
if (args.Length == 0)
{
using var dialog = new MainDiscoForm();
@ -88,61 +96,26 @@ namespace BizHawk.Client.DiscoHawk
}
}
public static string GetExeDirectoryAbsolute()
{
return Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
}
/// <remarks>http://www.codeproject.com/Articles/310675/AppDomain-AssemblyResolve-Event-Tips</remarks>
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
var requested = args.Name;
lock (AppDomain.CurrentDomain)
{
var asms = AppDomain.CurrentDomain.GetAssemblies();
foreach (var asm in asms)
if (asm.FullName == args.Name)
return asm;
//load missing assemblies by trying to find them in the dll directory
string dllName = $"{new AssemblyName(args.Name).Name}.dll";
string directory = Path.Combine(GetExeDirectoryAbsolute(), "dll");
string fname = Path.Combine(directory, dllName);
return File.Exists(fname) ? Assembly.LoadFile(fname) : null;
var firstAsm = Array.Find(AppDomain.CurrentDomain.GetAssemblies(), asm => asm.FullName == requested);
if (firstAsm != null)
{
return firstAsm;
}
// load missing assemblies by trying to find them in the dll directory
var dllname = $"{new AssemblyName(requested).Name}.dll";
var directory = Path.Combine(AppContext.BaseDirectory, "dll");
var fname = Path.Combine(directory, dllname);
// it is important that we use LoadFile here and not load from a byte array; otherwise mixed (managed/unmanaged) assemblies can't load
return File.Exists(fname) ? Assembly.LoadFile(fname) : null;
}
}
//declared here instead of a more usual place to avoid dependencies on the more usual place
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetDllDirectory(string lpPathName);
[DllImport("kernel32.dll", EntryPoint = "DeleteFileW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern bool DeleteFileW([MarshalAs(UnmanagedType.LPWStr)]string lpFileName);
private const uint WM_DROPFILES = 0x0233;
private const uint WM_COPYDATA = 0x004A;
[DllImport("user32")]
public static extern bool ChangeWindowMessageFilter(uint msg, ChangeWindowMessageFilterFlags flags);
public enum ChangeWindowMessageFilterFlags : uint
{
Add = 1, Remove = 2
}
public enum MessageFilterInfo : uint
{
None = 0, AlreadyAllowed = 1, AlreadyDisAllowed = 2, AllowedHigher = 3
}
public enum ChangeWindowMessageFilterExAction : uint
{
Reset = 0, Allow = 1, DisAllow = 2
}
[StructLayout(LayoutKind.Sequential)]
public struct CHANGEFILTERSTRUCT
{
public uint size;
public MessageFilterInfo info;
}
}
}

View File

@ -41,7 +41,7 @@ namespace BizHawk.Client.EmuHawk
{
try
{
string execPath = Assembly.GetEntryAssembly().Location;
var execPath = AppContext.BaseDirectory;
dynamic ji = Activator.CreateInstance(JumpTask);
ji.ApplicationPath = execPath;

View File

@ -2,6 +2,7 @@
using System.Diagnostics;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;
@ -13,60 +14,54 @@ using BizHawk.Common.PathExtensions;
using BizHawk.Client.Common;
using BizHawk.Client.EmuHawk.CustomControls;
using OSTC = EXE_PROJECT.OSTailoredCode;
namespace BizHawk.Client.EmuHawk
{
internal static class Program
{
// Declared here instead of a more usual place to avoid dependencies on the more usual place
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetDllDirectoryW(string lpPathName);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeleteFileW(string lpFileName);
static Program()
{
//this needs to be done before the warnings/errors show up
// This needs to be done before the warnings/errors show up
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// quickly check if the user is running this as a 32 bit process somehow
// Quickly check if the user is running this as a 32 bit process somehow
// TODO: We may want to remove this sometime, EmuHawk should be able to run somewhat as 32 bit if the user really wants to
// (There are no longer any hard 64 bit deps, i.e. SlimDX is no longer around)
if (!Environment.Is64BitProcess)
{
using (var box = new ExceptionBox($"EmuHawk requires a 64 bit environment in order to run! EmuHawk will now close.")) box.ShowDialog();
Process.GetCurrentProcess().Kill();
return;
}
if (OSTC.IsUnixHost)
{
// for Unix, skip everything else and just wire up the event handler
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
return;
}
foreach (var (dllToLoad, desc) in new[]
{
("vcruntime140_1.dll", "Microsoft Visual C++ Redistributable for Visual Studio 2015, 2017 and 2019 (x64)"),
("msvcr100.dll", "Microsoft Visual C++ 2010 SP1 Runtime (x64)"), // for Mupen64Plus, and some others
})
{
var p = OSTC.LinkedLibManager.LoadOrZero(dllToLoad);
if (p != IntPtr.Zero)
{
OSTC.LinkedLibManager.FreeByPtr(p);
continue;
}
// else it's missing or corrupted
using (ExceptionBox box = new($"EmuHawk needs {desc} in order to run! See the readme on GitHub for more info. (EmuHawk will now close.) Internal error message: {OSTC.LinkedLibManager.GetErrorMessage()}"))
using (var box = new ExceptionBox(
"EmuHawk requires a 64 bit environment in order to run! EmuHawk will now close."))
{
box.ShowDialog();
}
Process.GetCurrentProcess().Kill();
return;
}
// In case assembly resolution fails, such as if we moved them into the dll subdiretory, this event handler can reroute to them
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
// Windows needs extra considerations for the dll directory
// we can skip all this on non-Windows platforms
return;
}
// this will look in subdirectory "dll" to load pinvoked stuff
var dllDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "dll");
_ = SetDllDirectory(dllDir);
//in case assembly resolution fails, such as if we moved them into the dll subdiretory, this event handler can reroute to them
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
var dllDir = Path.Combine(AppContext.BaseDirectory, "dll");
_ = SetDllDirectoryW(dllDir);
try
{
@ -94,7 +89,7 @@ namespace BizHawk.Client.EmuHawk
private static int Main(string[] args)
{
var exitCode = SubMain(args);
if (OSTC.IsUnixHost)
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Console.WriteLine("BizHawk has completed its shutdown routines, killing process...");
Process.GetCurrentProcess().Kill();
@ -102,37 +97,60 @@ namespace BizHawk.Client.EmuHawk
return exitCode;
}
//NoInlining should keep this code from getting jammed into Main() which would create dependencies on types which havent been setup by the resolver yet... or something like that
// NoInlining should keep this code from getting jammed into Main() which would create dependencies on types which havent been setup by the resolver yet... or something like that
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
private static int SubMain(string[] args)
{
// this check has to be done VERY early. i stepped through a debug build with wrong .dll versions purposely used,
// and there was a TypeLoadException before the first line of SubMain was reached (some static ColorType init?)
var thisAsmVer = EmuHawk.ReflectionCache.AsmVersion;
foreach (var asmVer in new[]
{
BizInvoke.ReflectionCache.AsmVersion,
Bizware.Audio.ReflectionCache.AsmVersion,
Bizware.BizwareGL.ReflectionCache.AsmVersion,
Bizware.Graphics.ReflectionCache.AsmVersion,
Bizware.Graphics.Controls.ReflectionCache.AsmVersion,
Bizware.Input.ReflectionCache.AsmVersion,
Client.Common.ReflectionCache.AsmVersion,
Common.ReflectionCache.AsmVersion,
Emulation.Common.ReflectionCache.AsmVersion,
Emulation.Cores.ReflectionCache.AsmVersion,
Emulation.DiscSystem.ReflectionCache.AsmVersion,
WinForms.Controls.ReflectionCache.AsmVersion,
})
{
if (asmVer != thisAsmVer)
var thisAsmVer = ReflectionCache.AsmVersion;
if (new[]
{
MessageBox.Show("One or more of the BizHawk.* assemblies have the wrong version!\n(Did you attempt to update by overwriting an existing install?)");
BizInvoke.ReflectionCache.AsmVersion,
Bizware.Audio.ReflectionCache.AsmVersion,
Bizware.BizwareGL.ReflectionCache.AsmVersion,
Bizware.Graphics.ReflectionCache.AsmVersion,
Bizware.Graphics.Controls.ReflectionCache.AsmVersion,
Bizware.Input.ReflectionCache.AsmVersion,
Client.Common.ReflectionCache.AsmVersion,
Common.ReflectionCache.AsmVersion,
Emulation.Common.ReflectionCache.AsmVersion,
Emulation.Cores.ReflectionCache.AsmVersion,
Emulation.DiscSystem.ReflectionCache.AsmVersion,
WinForms.Controls.ReflectionCache.AsmVersion,
}.Any(asmVer => asmVer != thisAsmVer))
{
MessageBox.Show("One or more of the BizHawk.* assemblies have the wrong version!\n(Did you attempt to update by overwriting an existing install?)");
return -1;
}
if (!OSTailoredCode.IsUnixHost)
{
// TODO: We optimally should only require the new VS2015-2022 all in one redist be installed
// None of this old 2010 runtime garbage
// (It's probably as simple as just recompiling old mupen .dlls against newer VS and probably just removing speex completely)
foreach (var (dllToLoad, desc) in new[]
{
("vcruntime140_1.dll", "Microsoft Visual C++ Redistributable for Visual Studio 2015, 2017, 2019, and 2022 (x64)"),
// for libspeexdsp.dll, mupen64plus-audio-bkm.dll, mupen64plus-video-glide64.dll, mupen64plus-video-glide64mk2.dll, mupen64plus-video-rice.dll
("msvcr100.dll", "Microsoft Visual C++ 2010 SP1 Runtime (x64)"),
})
{
var p = OSTailoredCode.LinkedLibManager.LoadOrZero(dllToLoad);
if (p != IntPtr.Zero)
{
OSTailoredCode.LinkedLibManager.FreeByPtr(p);
continue;
}
// else it's missing or corrupted
MessageBox.Show($"EmuHawk needs {desc} in order to run! See the readme on GitHub for more info. (EmuHawk will now close.) " +
$"Internal error message: {OSTailoredCode.LinkedLibManager.GetErrorMessage()}");
return -1;
}
}
typeof(Form).GetField(OSTC.IsUnixHost ? "default_icon" : "defaultIcon", BindingFlags.NonPublic | BindingFlags.Static)
typeof(Form).GetField(OSTailoredCode.IsUnixHost ? "default_icon" : "defaultIcon", BindingFlags.NonPublic | BindingFlags.Static)!
.SetValue(null, Properties.Resources.Logo);
TempFileManager.Start();
@ -195,8 +213,8 @@ namespace BizHawk.Client.EmuHawk
{
// try to fallback on the faster option on Windows
// if we're on a Unix platform, there's only 1 fallback here...
1 when OSTC.IsUnixHost => (EDispMethod.GdiPlus, "GDI+"),
1 or 2 when !OSTC.IsUnixHost => dispMethod == EDispMethod.D3D9
1 when OSTailoredCode.IsUnixHost => (EDispMethod.GdiPlus, "GDI+"),
1 or 2 when !OSTailoredCode.IsUnixHost => dispMethod == EDispMethod.D3D9
? (EDispMethod.OpenGL, "OpenGL")
: (EDispMethod.D3D9, "Direct3D9"),
_ => (EDispMethod.GdiPlus, "GDI+")
@ -210,16 +228,16 @@ namespace BizHawk.Client.EmuHawk
}
catch (Exception ex)
{
var fallback = ChooseFallback();
new ExceptionBox(new Exception($"Initialization of Display Method failed; falling back to {fallback.Name}", ex)).ShowDialog();
return TryInitIGL(initialConfig.DispMethod = fallback.Method);
var (method, name) = ChooseFallback();
new ExceptionBox(new Exception($"Initialization of Display Method failed; falling back to {name}", ex)).ShowDialog();
return TryInitIGL(initialConfig.DispMethod = method);
}
}
switch (dispMethod)
{
case EDispMethod.D3D9:
if (OSTC.IsUnixHost || OSTC.IsWine)
if (OSTailoredCode.IsUnixHost || OSTailoredCode.IsWine)
{
// possibly sharing config w/ Windows, assume the user wants the not-slow method (but don't change the config)
return TryInitIGL(EDispMethod.OpenGL);
@ -230,17 +248,17 @@ namespace BizHawk.Client.EmuHawk
}
catch (Exception ex)
{
var fallback = ChooseFallback();
new ExceptionBox(new Exception($"Initialization of Direct3D9 Display Method failed; falling back to {fallback.Name}", ex)).ShowDialog();
return TryInitIGL(initialConfig.DispMethod = fallback.Method);
var (method, name) = ChooseFallback();
new ExceptionBox(new Exception($"Initialization of Direct3D9 Display Method failed; falling back to {name}", ex)).ShowDialog();
return TryInitIGL(initialConfig.DispMethod = method);
}
case EDispMethod.OpenGL:
if (!IGL_OpenGL.Available)
{
// too old to use, need to fallback to something else
var fallback = ChooseFallback();
new ExceptionBox(new Exception($"Initialization of OpenGL Display Method failed; falling back to {fallback.Name}")).ShowDialog();
return TryInitIGL(initialConfig.DispMethod = fallback.Method);
var (method, name) = ChooseFallback();
new ExceptionBox(new Exception($"Initialization of OpenGL Display Method failed; falling back to {name}")).ShowDialog();
return TryInitIGL(initialConfig.DispMethod = method);
}
var igl = new IGL_OpenGL();
// need to have a context active for checking renderer, will be disposed afterwards
@ -265,24 +283,25 @@ namespace BizHawk.Client.EmuHawk
Sound globalSound = null;
if (!OSTC.IsUnixHost)
if (!OSTailoredCode.IsUnixHost)
{
//WHY do we have to do this? some intel graphics drivers (ig7icd64.dll 10.18.10.3304 on an unknown chip on win8.1) are calling SetDllDirectory() for the process, which ruins stuff.
//The relevant initialization happened just before in "create IGL context".
//It isn't clear whether we need the earlier SetDllDirectory(), but I think we do.
//note: this is pasted instead of being put in a static method due to this initialization code being sensitive to things like that, and not wanting to cause it to break
//pasting should be safe (not affecting the jit order of things)
var dllDir = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "dll");
_ = SetDllDirectory(dllDir);
// WHY do we have to do this? some intel graphics drivers (ig7icd64.dll 10.18.10.3304 on an unknown chip on win8.1) are calling SetDllDirectory() for the process, which ruins stuff.
// The relevant initialization happened just before in "create IGL context".
// It isn't clear whether we need the earlier SetDllDirectory(), but I think we do.
// note: this is pasted instead of being put in a static method due to this initialization code being sensitive to things like that, and not wanting to cause it to break
// pasting should be safe (not affecting the jit order of things)
var dllDir = Path.Combine(AppContext.BaseDirectory, "dll");
_ = SetDllDirectoryW(dllDir);
}
if (OSTC.HostWindowsVersion is null || OSTC.HostWindowsVersion.Value.Version >= OSTC.WindowsVersion._10) // "windows isn't capable of being useful for non-administrators until windows 10" --zeromus
if (OSTailoredCode.HostWindowsVersion is null ||
OSTailoredCode.HostWindowsVersion.Value.Version >= OSTailoredCode.WindowsVersion._10) // "windows isn't capable of being useful for non-administrators until windows 10" --zeromus
{
if (EmuHawkUtil.CLRHostHasElevatedPrivileges)
{
using MsgBox dialog = new(
title: "This EmuHawk is privileged",
message: $"EmuHawk detected it {(OSTC.IsUnixHost ? "is running as root (Superuser)" : "has Administrator privileges")}.\n"
message: $"EmuHawk detected it {(OSTailoredCode.IsUnixHost ? "is running as root (Superuser)" : "has Administrator privileges")}.\n"
+ "This is a bad idea.",
boxIcon: MessageBoxIcon.Warning);
dialog.ShowDialog();
@ -321,11 +340,10 @@ namespace BizHawk.Client.EmuHawk
}
initialConfig = ConfigService.Load<Config>(iniPath);
initialConfig.ResolveDefaults();
// ReSharper disable once AccessToDisposedClosure
mf.Config = initialConfig;
};
// var title = mf.Text;
mf.Show();
// mf.Text = title;
try
{
exitCode = mf.ProgramRunLoop();
@ -357,17 +375,9 @@ namespace BizHawk.Client.EmuHawk
Input.Instance?.Adapter?.DeInitAll();
}
//return 0 assuming things have gone well, non-zero values could be used as error codes or for scripting purposes
// return 0 assuming things have gone well, non-zero values could be used as error codes or for scripting purposes
return exitCode;
} //SubMain
//declared here instead of a more usual place to avoid dependencies on the more usual place
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint SetDllDirectory(string lpPathName);
[DllImport("kernel32.dll", EntryPoint = "DeleteFileW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern bool DeleteFileW([MarshalAs(UnmanagedType.LPWStr)]string lpFileName);
}
/// <remarks>http://www.codeproject.com/Articles/310675/AppDomain-AssemblyResolve-Event-Tips</remarks>
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
@ -377,14 +387,16 @@ namespace BizHawk.Client.EmuHawk
lock (AppDomain.CurrentDomain)
{
var firstAsm = Array.Find(AppDomain.CurrentDomain.GetAssemblies(), asm => asm.FullName == requested);
if (firstAsm != null) return firstAsm;
if (firstAsm != null)
{
return firstAsm;
}
//load missing assemblies by trying to find them in the dll directory
// load missing assemblies by trying to find them in the dll directory
var dllname = $"{new AssemblyName(requested).Name}.dll";
var directory = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "dll");
var simpleName = new AssemblyName(requested).Name;
var directory = Path.Combine(AppContext.BaseDirectory, "dll");
var fname = Path.Combine(directory, dllname);
//it is important that we use LoadFile here and not load from a byte array; otherwise mixed (managed/unmanaged) assemblies can't load
// it is important that we use LoadFile here and not load from a byte array; otherwise mixed (managed/unmanaged) assemblies can't load
return File.Exists(fname) ? Assembly.LoadFile(fname) : null;
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.IO;
using System.Reflection;
using BizHawk.Common.StringExtensions;
@ -200,7 +199,7 @@ namespace BizHawk.Common.PathExtensions
}
if (OSTailoredCode.IsUnixHost)
{
var dirPath = ReadPathFromEnvVar("BIZHAWK_HOME") ?? Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
var dirPath = ReadPathFromEnvVar("BIZHAWK_HOME") ?? AppContext.BaseDirectory;
ExeDirectoryPath = string.IsNullOrEmpty(dirPath) || dirPath == "/" ? string.Empty : dirPath;
DllDirectoryPath = Path.Combine(ExeDirectoryPath == string.Empty ? "/" : ExeDirectoryPath, "dll");
// yes, this is a lot of extra code to make sure BizHawk can run in `/` on Unix, but I've made up for it by caching these for the program lifecycle --yoshi
@ -208,8 +207,8 @@ namespace BizHawk.Common.PathExtensions
}
else
{
var dirPath = Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location);
DataDirectoryPath = ExeDirectoryPath = string.IsNullOrEmpty(dirPath) ? throw new Exception("failed to get location of executable, very bad things must have happened") : dirPath.RemoveSuffix('\\');
var dirPath = AppContext.BaseDirectory;
DataDirectoryPath = ExeDirectoryPath = string.IsNullOrEmpty(dirPath) ? throw new("failed to get location of executable, very bad things must have happened") : dirPath.RemoveSuffix('\\');
DllDirectoryPath = Path.Combine(ExeDirectoryPath, "dll");
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Runtime.InteropServices;
namespace BizHawk.Common
{
/// <summary>
/// Imports of functions in dlfcn.h
/// For Linux, these come from libdl historically
/// (Although since glibc 2.34 these are just in libc, with libdl just calling into libc)
/// </summary>
public static class LinuxDlfcnImports
{
public const int RTLD_NOW = 2;
[DllImport("libdl.so.2")]
public static extern IntPtr dlopen(string fileName, int flags);
[DllImport("libdl.so.2")]
public static extern int dlclose(IntPtr handle);
[DllImport("libdl.so.2")]
public static extern IntPtr dlsym(IntPtr handle, string symbol);
[DllImport("libdl.so.2")]
public static extern IntPtr dlerror();
}
}

View File

@ -1,25 +1,43 @@
#pragma warning disable IDE0240
#nullable enable
#pragma warning restore IDE0240
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using BizHawk.Common.StringExtensions;
#if EXE_PROJECT
namespace EXE_PROJECT // Use a different namespace so the executable can still use this class' members without an implicit dependency on the BizHawk.Common library, and without resorting to code duplication.
#else
using static BizHawk.Common.LoaderApiImports;
namespace BizHawk.Common
#endif
{
public static class OSTailoredCode
{
/// <remarks>macOS doesn't use <see cref="PlatformID.MacOSX">PlatformID.MacOSX</see></remarks>
public static readonly DistinctOS CurrentOS = Environment.OSVersion.Platform == PlatformID.Unix
? SimpleSubshell("uname", "-s", "Can't determine OS") == "Darwin" ? DistinctOS.macOS : DistinctOS.Linux
: DistinctOS.Windows;
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(() =>
{
@ -65,8 +83,6 @@ namespace BizHawk.Common
public static (WindowsVersion Version, Version? Win10PlusVersion)? HostWindowsVersion => _HostWindowsVersion.Value;
public static readonly bool IsUnixHost = CurrentOS != DistinctOS.Windows;
public static bool IsWSL => _isWSL.Value;
private static readonly Lazy<bool> _isWine = new(() =>
@ -89,11 +105,13 @@ namespace BizHawk.Common
public static bool IsWine => _isWine.Value;
private static readonly Lazy<ILinkedLibManager> _LinkedLibManager = new Lazy<ILinkedLibManager>(() => CurrentOS switch
private static readonly Lazy<ILinkedLibManager> _LinkedLibManager = new(() => CurrentOS switch
{
DistinctOS.Linux => new UnixMonoLLManager(),
DistinctOS.macOS => new UnixMonoLLManager(),
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()
});
@ -117,65 +135,72 @@ namespace BizHawk.Common
string GetErrorMessage();
}
private class UnixMonoLLManager : ILinkedLibManager
private class LinuxLLManager : ILinkedLibManager
{
private const int RTLD_NOW = 2;
public int FreeByPtr(IntPtr hModule) => LinuxDlfcnImports.dlclose(hModule);
[DllImport("libdl.so.2")]
private static extern int dlclose(IntPtr handle);
[DllImport("libdl.so.2")]
private static extern IntPtr dlerror();
[DllImport("libdl.so.2")]
private static extern IntPtr dlopen(string fileName, int flags);
[DllImport("libdl.so.2")]
private static extern IntPtr dlsym(IntPtr handle, string symbol);
public int FreeByPtr(IntPtr hModule) => dlclose(hModule);
public IntPtr GetProcAddrOrZero(IntPtr hModule, string procName) => dlsym(hModule, procName);
public IntPtr GetProcAddrOrZero(IntPtr hModule, string procName) => LinuxDlfcnImports.dlsym(hModule, procName);
public IntPtr GetProcAddrOrThrow(IntPtr hModule, string procName)
{
_ = dlerror(); // the Internet said to do this
_ = LinuxDlfcnImports.dlerror(); // the Internet said to do this
var p = GetProcAddrOrZero(hModule, procName);
if (p != IntPtr.Zero) return p;
var errCharPtr = dlerror();
throw new InvalidOperationException($"error in {nameof(dlsym)}{(errCharPtr == IntPtr.Zero ? string.Empty : $": {Marshal.PtrToStringAnsi(errCharPtr)}")}");
throw new InvalidOperationException($"error in dlsym: {GetErrorMessage()}");
}
public IntPtr LoadOrZero(string dllToLoad) => dlopen(dllToLoad, RTLD_NOW);
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 {nameof(dlopen)}, error: {Marshal.PtrToStringAnsi(dlerror())}");
return ret != IntPtr.Zero
? ret
: throw new InvalidOperationException($"got null pointer from dlopen, error: {GetErrorMessage()}");
}
public string GetErrorMessage()
{
var errCharPtr = dlerror();
return errCharPtr == IntPtr.Zero ? "No error present" : Marshal.PtrToStringAnsi(errCharPtr)!;
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
{
// functions taken from LoaderApiImports
// TODO: Should we apply the same EXE_PROJECT hack to that file?
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
private static extern IntPtr LoadLibraryW(string lpLibFileName);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FreeLibrary(IntPtr hLibModule);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
private static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
public int FreeByPtr(IntPtr hModule) => FreeLibrary(hModule) ? 0 : 1;
public IntPtr GetProcAddrOrZero(IntPtr hModule, string procName) => GetProcAddress(hModule, procName);
@ -194,18 +219,12 @@ namespace BizHawk.Common
return ret != IntPtr.Zero ? ret : throw new InvalidOperationException($"got null pointer from {nameof(LoadLibraryW)}, {GetErrorMessage()}");
}
[DllImport("kernel32.dll", ExactSpelling = true)]
private static extern uint GetLastError();
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
private static extern unsafe int FormatMessageW(int flags, IntPtr source, uint messageId, uint languageId, char* outMsg, int size, IntPtr args);
public unsafe string GetErrorMessage()
{
var errCode = GetLastError();
var errCode = Win32Imports.GetLastError();
var buffer = stackalloc char[1024];
const int FORMAT_MESSAGE_FROM_SYSTEM = 0x1000;
var sz = FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM, IntPtr.Zero, errCode, 0, buffer, 1024, IntPtr.Zero);
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)}";
}
}
@ -214,7 +233,9 @@ namespace BizHawk.Common
{
Linux,
macOS,
Windows
Windows,
BSD, // covering all the *BSDs
Unknown,
}
public enum WindowsVersion
@ -234,8 +255,10 @@ namespace BizHawk.Common
/// <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 Process {
StartInfo = new ProcessStartInfo {
=> new()
{
StartInfo = new()
{
Arguments = args,
CreateNoWindow = true,
FileName = cmd,
@ -257,7 +280,7 @@ namespace BizHawk.Common
using var proc = ConstructSubshell(cmd, args);
proc.Start();
var stdout = proc.StandardOutput;
if (stdout.EndOfStream) throw new Exception($"{noOutputMsg} ({cmd} wrote nothing to stdout)");
if (stdout.EndOfStream) throw new($"{noOutputMsg} ({cmd} wrote nothing to stdout)");
return stdout.ReadLine()!;
}
}

View File

@ -0,0 +1,28 @@
using System;
using System.Runtime.InteropServices;
namespace BizHawk.Common
{
/// <summary>
/// Imports of functions in dlfcn.h
/// For most POSIX systems, these come from libc
/// Linux is a partial exception, as they weren't in libc until glibc 2.34
/// (For reference, Debian 10, the current Debian oldoldstable, is on glibc 2.28)
/// </summary>
public static class PosixDlfcnImports
{
public const int RTLD_NOW = 2;
[DllImport("libc")]
public static extern IntPtr dlopen(string fileName, int flags);
[DllImport("libc")]
public static extern int dlclose(IntPtr handle);
[DllImport("libc")]
public static extern IntPtr dlsym(IntPtr handle, string symbol);
[DllImport("libc")]
public static extern IntPtr dlerror();
}
}

View File

@ -14,13 +14,13 @@ namespace BizHawk.Common
Execute = 0x4
}
[DllImport("libc.so.6")]
[DllImport("libc")]
public static extern IntPtr mmap(IntPtr addr, UIntPtr length, MemoryProtection prot, int flags, int fd, IntPtr offset);
[DllImport("libc.so.6")]
[DllImport("libc")]
public static extern int mprotect(IntPtr addr, UIntPtr len, MemoryProtection prot);
[DllImport("libc.so.6")]
[DllImport("libc")]
public static extern int munmap(IntPtr addr, UIntPtr length);
}
}

View File

@ -1,5 +1,5 @@
using System;
using System.IO;
using System.Reflection;
using BizHawk.Common.StringExtensions;
@ -24,7 +24,7 @@ namespace BizHawk.Common
static VersionInfo()
{
var path = Path.Combine(
Path.GetDirectoryName(Assembly.GetEntryAssembly()?.Location)?.RemoveSuffix(Path.DirectorySeparatorChar) ?? string.Empty,
AppContext.BaseDirectory.RemoveSuffix(Path.DirectorySeparatorChar),
"dll",
"custombuild.txt"
);

View File

@ -51,6 +51,12 @@ namespace BizHawk.Common
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool DestroyMenu(IntPtr hMenu);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern unsafe int FormatMessageW(int flags, IntPtr source, uint messageId, uint languageId, char* outMsg, int size, IntPtr args);
[DllImport("kernel32.dll", ExactSpelling = true)]
public static extern uint GetLastError();
[DllImport("user32.dll", CharSet = CharSet.Unicode, ExactSpelling = true)]
public static extern uint MapVirtualKeyW(uint uCode, uint uMapType);

View File

@ -13,6 +13,12 @@ namespace BizHawk.Common
public static readonly IntPtr HWND_MESSAGE = new(-3);
public const int GWLP_USERDATA = -21;
public enum ChangeWindowMessageFilterFlags : uint
{
Add = 1,
Remove = 2,
}
[StructLayout(LayoutKind.Sequential)]
public struct MSG
{
@ -44,6 +50,9 @@ namespace BizHawk.Common
public string lpszClassName;
}
[DllImport("user32.dll", ExactSpelling = true)]
public static extern bool ChangeWindowMessageFilter(uint msg, ChangeWindowMessageFilterFlags flags);
[DllImport("user32.dll", CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)]
public static extern IntPtr CreateWindowExW(int dwExStyle, IntPtr lpClassName, string lpWindowName,
int dwStyle, int X, int Y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);

View File

@ -1,7 +1,6 @@
<Project>
<Import Project="MainSlnCommon.props" />
<PropertyGroup>
<DefineConstants>$(DefineConstants);EXE_PROJECT</DefineConstants>
<OutputPath>$(MSBuildProjectDirectory)/../../output</OutputPath>
<AssemblyName>$(MSBuildProjectName.Substring($([MSBuild]::Add($(MSBuildProjectName.LastIndexOf('.')), 1))))</AssemblyName>
</PropertyGroup>
@ -11,9 +10,6 @@
<PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
<OutputType>WinExe</OutputType>
</PropertyGroup>
<ItemGroup>
<Compile Include="$(MSBuildProjectDirectory)/../BizHawk.Common/OSTailoredCode.cs" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<ItemGroup>
<NotExecFilesFromExecProj Include="$(OutputPath)*.deps.json" />