Add OS-specific interop

This commit is contained in:
Robert McRackan 2022-08-12 13:49:51 -04:00
parent 86c7f89788
commit aea8c11dc4
33 changed files with 1083 additions and 13 deletions

View file

@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\bin\Debug</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\bin\Release</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dinah.Core" Version="5.1.0.1" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,12 @@
using System;
namespace CrossPlatformClientExe
{
public interface IInteropFunctions
{
public string TransformInit1();
public int TransformInit2();
public void CopyTextToClipboard(string text);
public void ShowForm();
}
}

View file

@ -0,0 +1,14 @@
using System;
namespace CrossPlatformClientExe
{
internal class NullInteropFunctions : IInteropFunctions
{
public NullInteropFunctions(params object[] values) { }
public string TransformInit1() => throw new PlatformNotSupportedException();
public int TransformInit2() => throw new PlatformNotSupportedException();
public void CopyTextToClipboard(string text) => throw new PlatformNotSupportedException();
public void ShowForm() => throw new PlatformNotSupportedException();
}
}

View file

@ -0,0 +1,29 @@
using System;
namespace CrossPlatformClientExe
{
public abstract class OSConfigBase
{
public abstract Type InteropFunctionsType { get; }
public virtual Type[] ReferencedTypes { get; } = new Type[0];
public void Run()
{
//Each of these types belongs to a different windows-only assembly that's needed by
//the WinInterop methods. By referencing these types in main we force the runtime to
//load their assemblies before execution reaches inside main. This allows the calling
//process to find these assemblies in its module list.
_ = ReferencedTypes;
_ = InteropFunctionsType;
//Wait for the calling process to be ready to read the WriteLine()
Console.ReadLine();
// Signal the calling process that execution has reached inside main, and that all referenced assemblies have been loaded.
Console.WriteLine();
// Wait for the calling process to finish reading the process module list, then exit.
Console.ReadLine();
}
}
}

View file

@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using Dinah.Core;
namespace CrossPlatformClientExe
{
public class OSInteropProxy : IInteropFunctions
{
public static bool IsWindows { get; } = OperatingSystem.IsWindows();
public static bool IsLinux { get; } = OperatingSystem.IsLinux();
public static bool IsMacOs { get; } = OperatingSystem.IsMacOS();
public static Func<string, bool> MatchesOS { get; }
= IsWindows ? a => Path.GetFileName(a).StartsWithInsensitive("win")
: IsLinux ? a => Path.GetFileName(a).StartsWithInsensitive("linux")
: IsMacOs ? a => Path.GetFileName(a).StartsWithInsensitive("mac") || a.StartsWithInsensitive("osx")
: _ => false;
private IInteropFunctions InteropFunctions { get; } = new NullInteropFunctions();
#region Singleton Stuff
private const string CONFIG_APP_ENDING = "ConfigApp.exe";
private static List<ProcessModule> ModuleList { get; } = new();
private static Type InteropFunctionsType { get; }
static OSInteropProxy()
{
// searches file names for potential matches; doesn't run anything
var configApp = getOSConfigApp();
// nothing to load
if (configApp is null)
return;
// runs the exe and gets the exe's loaded modules
ModuleList = LoadModuleList(Path.GetFileNameWithoutExtension(configApp))
.OrderBy(x => x.ModuleName)
.ToList();
AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;
var configAppAssembly = Assembly.LoadFrom(Path.ChangeExtension(configApp, "dll"));
var type = typeof(IInteropFunctions);
InteropFunctionsType = configAppAssembly
.GetTypes()
.FirstOrDefault(t => type.IsAssignableFrom(t));
}
private static string getOSConfigApp()
{
var here = Path.GetDirectoryName(Environment.ProcessPath);
// find '*ConfigApp.exe' files
var exes =
Directory.EnumerateFiles(here, $"*{CONFIG_APP_ENDING}", SearchOption.TopDirectoryOnly)
// sanity check. shouldn't ever be true
.Except(new[] { Environment.ProcessPath })
.Where(exe =>
// has a corresponding dll
File.Exists(Path.ChangeExtension(exe, "dll"))
&& MatchesOS(exe)
)
.ToList();
var exeName = exes.FirstOrDefault();
return exeName;
}
private static List<ProcessModule> LoadModuleList(string exeName)
{
var proc = new Process
{
StartInfo = new()
{
FileName = exeName,
RedirectStandardInput = true,
RedirectStandardOutput = true,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
UseShellExecute = false
}
};
var waitHandle = new EventWaitHandle(false, EventResetMode.ManualReset);
proc.OutputDataReceived += (_, _) => waitHandle.Set();
proc.Start();
proc.BeginOutputReadLine();
//Let the win process know we're ready to receive its standard output
proc.StandardInput.WriteLine();
if (!waitHandle.WaitOne(2000))
throw new Exception("Failed to start program");
//The win process has finished loading and is now waiting inside Main().
//Copy it process module list.
var modules = proc.Modules.Cast<ProcessModule>().ToList();
//Let the win process know we're done reading its module list
proc.StandardInput.WriteLine();
return modules;
}
private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
// e.g. "System.Windows.Forms, Version=6.0.2.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
var asmName = args.Name.Split(',')[0];
// `First` instead of `FirstOrDefault`. If it's not present we're going to fail anyway. May as well be here
var module = ModuleList.First(m => m.ModuleName.StartsWith(asmName));
return Assembly.LoadFrom(module.FileName);
}
#endregion
public OSInteropProxy(string str, int i) : this(new object[] { str, i }) { }
private OSInteropProxy(params object[] values)
{
InteropFunctions =
values is null || values.Length == 0
? Activator.CreateInstance(InteropFunctionsType) as IInteropFunctions
: Activator.CreateInstance(InteropFunctionsType, values) as IInteropFunctions;
}
#region Interface Members
public void CopyTextToClipboard(string text) => InteropFunctions.CopyTextToClipboard(text);
public void ShowForm() => InteropFunctions.ShowForm();
public string TransformInit1() => InteropFunctions.TransformInit1();
public int TransformInit2() => InteropFunctions.TransformInit2();
#endregion
}
}

View file

@ -0,0 +1,19 @@
using System;
namespace CrossPlatformClientExe
{
class Program
{
[STAThread]
public static void Main()
{
var interopInstance = new OSInteropProxy("this IS SOME text", 42);
Console.WriteLine("X-Formed Value 1: {0}", interopInstance.TransformInit1());
Console.WriteLine("X-Formed Value 2: {0}", interopInstance.TransformInit2());
interopInstance.ShowForm();
interopInstance.CopyTextToClipboard("This is copied text!");
}
}
}