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

@ -4,6 +4,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using ApplicationServices; using ApplicationServices;
using AppScaffolding.OSInterop;
using AudibleUtilities; using AudibleUtilities;
using Dinah.Core.Collections.Generic; using Dinah.Core.Collections.Generic;
using Dinah.Core.IO; using Dinah.Core.IO;
@ -59,6 +60,8 @@ namespace AppScaffolding
??= new[] { ExecutingAssembly.GetName(), EntryAssembly.GetName() } ??= new[] { ExecutingAssembly.GetName(), EntryAssembly.GetName() }
.Max(a => a.Version); .Max(a => a.Version);
public static OSInteropProxy InteropInstance { get; private set; }
/// <summary>Run migrations before loading Configuration for the first time. Then load and return Configuration</summary> /// <summary>Run migrations before loading Configuration for the first time. Then load and return Configuration</summary>
public static Configuration RunPreConfigMigrations() public static Configuration RunPreConfigMigrations()
{ {
@ -187,6 +190,9 @@ namespace AppScaffolding
configureLogging(config); configureLogging(config);
logStartupState(config); logStartupState(config);
// all else should occur after logging
loadOSInterop(config);
wireUpSystemEvents(config); wireUpSystemEvents(config);
} }
@ -337,6 +343,19 @@ namespace AppScaffolding
}); });
} }
private static void loadOSInterop(Configuration configuration)
{
InteropInstance = new OSInteropProxy();
Serilog.Log.Logger.Information("InteropInstance:{@DebugInfo}", new
{
type = OSInteropProxy.InteropFunctionsType,
instance = InteropInstance.InteropFunctions
});
if (OSInteropProxy.InteropFunctionsType is null)
Serilog.Log.Logger.Warning("WARNING: OSInteropProxy.InteropFunctionsType is null");
}
private static void wireUpSystemEvents(Configuration configuration) private static void wireUpSystemEvents(Configuration configuration)
{ {
LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex(); LibraryCommands.LibrarySizeChanged += (_, __) => SearchEngineCommands.FullReIndex();

View File

@ -0,0 +1,15 @@
using System;
namespace AppScaffolding.OSInterop
{
public interface IInteropFunctions
{
// examples until the real interface is filled out
/*
public string TransformInit1();
public int TransformInit2();
public void CopyTextToClipboard(string text);
public void ShowForm();
*/
}
}

View File

@ -0,0 +1,16 @@
using System;
namespace AppScaffolding.OSInterop
{
internal class NullInteropFunctions : IInteropFunctions
{
public NullInteropFunctions() { }
public NullInteropFunctions(params object[] values) { }
// examples until the real interface is filled out
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 AppScaffolding.OSInterop
{
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,147 @@
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 AppScaffolding.OSInterop
{
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;
public IInteropFunctions InteropFunctions { get; } = new NullInteropFunctions();
#region Singleton Stuff
private const string CONFIG_APP_ENDING = "ConfigApp.exe";
private static List<ProcessModule> ModuleList { get; } = new();
public 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() : this(new object[0]) { }
//// example of the pattern which could be useful later
//public OSInteropProxy(string str, int i) : this(new object[] { str, i }) { }
private OSInteropProxy(params object[] values)
{
if (InteropFunctionsType is null)
return;
InteropFunctions =
values is null || values.Length == 0
? Activator.CreateInstance(InteropFunctionsType) as IInteropFunctions
: Activator.CreateInstance(InteropFunctionsType, values) as IInteropFunctions;
}
// Interface Members
/*
// examples until the real interface is filled out
public void CopyTextToClipboard(string text) => InteropFunctions.CopyTextToClipboard(text);
public void ShowForm() => InteropFunctions.ShowForm();
public string TransformInit1() => InteropFunctions.TransformInit1();
public int TransformInit2() => InteropFunctions.TransformInit2();
*/
}
}

View File

@ -7,8 +7,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Solution Items", "_Solutio
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
REFERENCE.txt = REFERENCE.txt REFERENCE.txt = REFERENCE.txt
_ARCHITECTURE NOTES.txt = _ARCHITECTURE NOTES.txt _ARCHITECTURE NOTES.txt = _ARCHITECTURE NOTES.txt
_DB_NOTES.txt = _DB_NOTES.txt
_AvaloniaUI Primer.txt = _AvaloniaUI Primer.txt _AvaloniaUI Primer.txt = _AvaloniaUI Primer.txt
_DB_NOTES.txt = _DB_NOTES.txt
__README - COLLABORATORS.txt = __README - COLLABORATORS.txt __README - COLLABORATORS.txt = __README - COLLABORATORS.txt
EndProjectSection EndProjectSection
EndProject EndProject
@ -72,6 +72,27 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HangoverAvalonia", "Hangove
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HangoverBase", "HangoverBase\HangoverBase.csproj", "{5C7005BA-7D83-4E99-8073-D970943A7D61}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HangoverBase", "HangoverBase\HangoverBase.csproj", "{5C7005BA-7D83-4E99-8073-D970943A7D61}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_Demos", "_Demos", "{185AC9FF-381E-4AA1-B649-9771F4917214}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LoadByOS", "LoadByOS", "{59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CrossPlatformClientExe", "_Demos\LoadByOS\CrossPlatformClientExe\CrossPlatformClientExe.csproj", "{CC275937-DFE4-4383-B1BF-1D5D42B70C98}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxConfigApp", "_Demos\LoadByOS\LinuxConfigApp\LinuxConfigApp.csproj", "{47325742-5B38-48E7-95FB-CD94E6E07332}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsConfigApp", "_Demos\LoadByOS\WindowsConfigApp\WindowsConfigApp.csproj", "{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LoadByOS", "LoadByOS", "{9B906374-1142-4D69-86FF-B384806CA5FE}"
ProjectSection(SolutionItems) = preProject
LoadByOS\README.txt = LoadByOS\README.txt
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxConfigApp", "LoadByOS\LinuxConfigApp\LinuxConfigApp.csproj", "{357DF797-4EC2-4DBD-A4BD-D045277F2666}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MacOSConfigApp", "LoadByOS\MacOSConfigApp\MacOSConfigApp.csproj", "{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WindowsConfigApp", "LoadByOS\WindowsConfigApp\WindowsConfigApp.csproj", "{5F65A509-26E3-4B02-B403-EEB6F0EF391F}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -162,6 +183,30 @@ Global
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Debug|Any CPU.Build.0 = Debug|Any CPU {5C7005BA-7D83-4E99-8073-D970943A7D61}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Release|Any CPU.ActiveCfg = Release|Any CPU {5C7005BA-7D83-4E99-8073-D970943A7D61}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5C7005BA-7D83-4E99-8073-D970943A7D61}.Release|Any CPU.Build.0 = Release|Any CPU {5C7005BA-7D83-4E99-8073-D970943A7D61}.Release|Any CPU.Build.0 = Release|Any CPU
{CC275937-DFE4-4383-B1BF-1D5D42B70C98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC275937-DFE4-4383-B1BF-1D5D42B70C98}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC275937-DFE4-4383-B1BF-1D5D42B70C98}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CC275937-DFE4-4383-B1BF-1D5D42B70C98}.Release|Any CPU.Build.0 = Release|Any CPU
{47325742-5B38-48E7-95FB-CD94E6E07332}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{47325742-5B38-48E7-95FB-CD94E6E07332}.Debug|Any CPU.Build.0 = Debug|Any CPU
{47325742-5B38-48E7-95FB-CD94E6E07332}.Release|Any CPU.ActiveCfg = Release|Any CPU
{47325742-5B38-48E7-95FB-CD94E6E07332}.Release|Any CPU.Build.0 = Release|Any CPU
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9}.Release|Any CPU.Build.0 = Release|Any CPU
{357DF797-4EC2-4DBD-A4BD-D045277F2666}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{357DF797-4EC2-4DBD-A4BD-D045277F2666}.Debug|Any CPU.Build.0 = Debug|Any CPU
{357DF797-4EC2-4DBD-A4BD-D045277F2666}.Release|Any CPU.ActiveCfg = Release|Any CPU
{357DF797-4EC2-4DBD-A4BD-D045277F2666}.Release|Any CPU.Build.0 = Release|Any CPU
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C}.Release|Any CPU.Build.0 = Release|Any CPU
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F65A509-26E3-4B02-B403-EEB6F0EF391F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@ -188,6 +233,14 @@ Global
{F612D06F-3134-4B9B-95CD-EB3FC798AE60} = {8679CAC8-9164-4007-BDD2-F004810EDA14} {F612D06F-3134-4B9B-95CD-EB3FC798AE60} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{8A7B01D3-9830-44FD-91A1-D8D010996BEB} = {8679CAC8-9164-4007-BDD2-F004810EDA14} {8A7B01D3-9830-44FD-91A1-D8D010996BEB} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{5C7005BA-7D83-4E99-8073-D970943A7D61} = {8679CAC8-9164-4007-BDD2-F004810EDA14} {5C7005BA-7D83-4E99-8073-D970943A7D61} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F} = {185AC9FF-381E-4AA1-B649-9771F4917214}
{CC275937-DFE4-4383-B1BF-1D5D42B70C98} = {59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}
{47325742-5B38-48E7-95FB-CD94E6E07332} = {59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}
{0520760A-9CFB-48A8-BCE4-6E951AFD6BE9} = {59DF46F3-ECD0-43CA-AD12-3FEE8FCF9E4F}
{9B906374-1142-4D69-86FF-B384806CA5FE} = {8679CAC8-9164-4007-BDD2-F004810EDA14}
{357DF797-4EC2-4DBD-A4BD-D045277F2666} = {9B906374-1142-4D69-86FF-B384806CA5FE}
{ECED4E13-B676-4277-8A8F-C8B2507B7D8C} = {9B906374-1142-4D69-86FF-B384806CA5FE}
{5F65A509-26E3-4B02-B403-EEB6F0EF391F} = {9B906374-1142-4D69-86FF-B384806CA5FE}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9} SolutionGuid = {615E00ED-BAEF-4E8E-A92A-9B82D87942A9}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\..\bin\Avalonia\Debug</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\..\bin\Avalonia\Release</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using AppScaffolding.OSInterop;
namespace LinuxConfigApp
{
internal class LinuxInterop : IInteropFunctions
{
public LinuxInterop() { }
public LinuxInterop(params object[] values) { }
// examples until the real interface is filled out
private string InitValue1 { get; }
private int InitValue2 { get; }
public LinuxInterop(string initValue1, int initValue2)
{
InitValue1 = initValue1;
InitValue2 = initValue2;
}
public string TransformInit1() => InitValue1.ToLower();
public int TransformInit2() => InitValue2 + InitValue2;
public void CopyTextToClipboard(string text) => throw new PlatformNotSupportedException();
public void ShowForm() => throw new PlatformNotSupportedException();
}
}

View File

@ -0,0 +1,11 @@
using AppScaffolding.OSInterop;
namespace LinuxConfigApp
{
class Program : OSConfigBase
{
public override Type InteropFunctionsType => typeof(LinuxInterop);
static void Main() => new Program().Run();
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<OutputPath>..\..\bin\Avalonia\Debug</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<OutputPath>..\..\bin\Avalonia\Release</OutputPath>
<DebugType>embedded</DebugType>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using AppScaffolding.OSInterop;
namespace MacOSConfigApp
{
internal class MacOSInterop : IInteropFunctions
{
public MacOSInterop() { }
public MacOSInterop(params object[] values) { }
// examples until the real interface is filled out
private string InitValue1 { get; }
private int InitValue2 { get; }
public MacOSInterop(string initValue1, int initValue2)
{
InitValue1 = initValue1;
InitValue2 = initValue2;
}
public string TransformInit1() => InitValue1.ToLower();
public int TransformInit2() => InitValue2 + InitValue2;
public void CopyTextToClipboard(string text) => throw new PlatformNotSupportedException();
public void ShowForm() => throw new PlatformNotSupportedException();
}
}

View File

@ -0,0 +1,11 @@
using AppScaffolding.OSInterop;
namespace MacOSConfigApp
{
class Program : OSConfigBase
{
public override Type InteropFunctionsType => typeof(MacOSInterop);
static void Main() => new Program().Run();
}
}

View File

@ -0,0 +1 @@
Streamlined example is in \Source\_Demos\LoadByOS

View File

@ -0,0 +1,39 @@
namespace WindowsConfigApp
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Text = "Form1";
}
#endregion
}
}

View File

@ -0,0 +1,10 @@
namespace WindowsConfigApp
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,18 @@
using AppScaffolding.OSInterop;
namespace WindowsConfigApp
{
class Program : OSConfigBase
{
public override Type InteropFunctionsType => typeof(WinInterop);
public override Type[] ReferencedTypes => new Type[]
{
typeof(Form1),
typeof(Bitmap),
typeof(Accessibility.IAccIdentity),
typeof(Microsoft.Win32.SystemEvents)
};
static void Main() => new Program().Run();
}
}

View File

@ -0,0 +1,33 @@
using AppScaffolding.OSInterop;
namespace WindowsConfigApp
{
internal class WinInterop : IInteropFunctions
{
public WinInterop() { }
public WinInterop(params object[] values) { }
// examples until the real interface is filled out
private string InitValue1 { get; }
private int InitValue2 { get; }
public WinInterop(string initValue1, int initValue2)
{
InitValue1 = initValue1;
InitValue2 = initValue2;
}
public void CopyTextToClipboard(string text) => Clipboard.SetDataObject(text, true, 5, 150);
public void ShowForm()
{
ApplicationConfiguration.Initialize();
Application.Run(new Form1());
}
public string TransformInit1() => InitValue1.ToUpper();
public int TransformInit2() => InitValue2 * InitValue2;
}
}

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<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>
<ProjectReference Include="..\..\AppScaffolding\AppScaffolding.csproj" />
</ItemGroup>
</Project>

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!");
}
}
}

View File

@ -0,0 +1,25 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<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>
<ProjectReference Include="..\CrossPlatformClientExe\CrossPlatformClientExe.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,23 @@
using CrossPlatformClientExe;
namespace LinuxConfigApp
{
internal class LinuxInterop : IInteropFunctions
{
private string InitValue1 { get; }
private int InitValue2 { get; }
public LinuxInterop(string initValue1, int initValue2)
{
InitValue1 = initValue1;
InitValue2 = initValue2;
}
public string TransformInit1() => InitValue1.ToLower();
public int TransformInit2() => InitValue2 + InitValue2;
public void CopyTextToClipboard(string text) => throw new PlatformNotSupportedException();
public void ShowForm() => throw new PlatformNotSupportedException();
}
}

View File

@ -0,0 +1,11 @@
using CrossPlatformClientExe;
namespace LinuxConfigApp
{
class Program : OSConfigBase
{
public override Type InteropFunctionsType => typeof(LinuxInterop);
static void Main() => new Program().Run();
}
}

View File

@ -0,0 +1,39 @@
namespace WindowsConfigApp
{
partial class Form1
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.ClientSize = new System.Drawing.Size(800, 450);
this.Text = "Form1";
}
#endregion
}
}

View File

@ -0,0 +1,10 @@
namespace WindowsConfigApp
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -0,0 +1,18 @@
using CrossPlatformClientExe;
namespace WindowsConfigApp
{
class Program : OSConfigBase
{
public override Type InteropFunctionsType => typeof(WinInterop);
public override Type[] ReferencedTypes => new Type[]
{
typeof(Form1),
typeof(Bitmap),
typeof(Accessibility.IAccIdentity),
typeof(Microsoft.Win32.SystemEvents)
};
static void Main() => new Program().Run();
}
}

View File

@ -0,0 +1,29 @@
using CrossPlatformClientExe;
namespace WindowsConfigApp
{
internal class WinInterop : IInteropFunctions
{
private string InitValue1 { get; }
private int InitValue2 { get; }
public WinInterop() { }
public WinInterop(string initValue1, int initValue2)
{
InitValue1 = initValue1;
InitValue2 = initValue2;
}
public void CopyTextToClipboard(string text) => Clipboard.SetDataObject(text, true, 5, 150);
public void ShowForm()
{
ApplicationConfiguration.Initialize();
Application.Run(new Form1());
}
public string TransformInit1() => InitValue1.ToUpper();
public int TransformInit2() => InitValue2 * InitValue2;
}
}

View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<ImplicitUsings>enable</ImplicitUsings>
<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>
<ProjectReference Include="..\CrossPlatformClientExe\CrossPlatformClientExe.csproj" />
</ItemGroup>
</Project>