Add OS-specific interop
This commit is contained in:
parent
86c7f89788
commit
aea8c11dc4
33 changed files with 1083 additions and 13 deletions
|
|
@ -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>
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
138
Source/_Demos/LoadByOS/CrossPlatformClientExe/OSInteropProxy.cs
Normal file
138
Source/_Demos/LoadByOS/CrossPlatformClientExe/OSInteropProxy.cs
Normal 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
|
||||
}
|
||||
}
|
||||
19
Source/_Demos/LoadByOS/CrossPlatformClientExe/Program.cs
Normal file
19
Source/_Demos/LoadByOS/CrossPlatformClientExe/Program.cs
Normal 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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue