Skip to content

Commit c67a86d

Browse files
committed
Merge main: port WineFix to staged plugin loading pipeline
2 parents a2f1c01 + e0639a2 commit c67a86d

14 files changed

Lines changed: 835 additions & 329 deletions

AffinityHook/Program.cs

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,14 +219,43 @@ private static void InjectBootstrap(int processId, string pluginLoaderPath)
219219

220220
Console.WriteLine($"Using native bootstrap: {bootstrapPath}");
221221

222-
// Wait a moment for process to initialize
223-
Thread.Sleep(500);
222+
// Poll until we can open the process
223+
IntPtr hProcess = IntPtr.Zero;
224+
for (int i = 0; i < 50; i++)
225+
{
226+
hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, processId);
227+
if (hProcess != IntPtr.Zero)
228+
break;
229+
Thread.Sleep(10);
230+
}
224231

225-
// Open process with full access
226-
IntPtr hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, processId);
227232
if (hProcess == IntPtr.Zero)
228233
{
229-
throw new Exception($"Failed to open process. Error: {Marshal.GetLastWin32Error()}");
234+
throw new Exception($"Failed to open process after polling. Error: {Marshal.GetLastWin32Error()}");
235+
}
236+
237+
// Wait for CLR to be fully initialized by polling for clrjit.dll,
238+
// one of the last modules loaded during CLR startup.
239+
// Injecting before the CLR is ready causes a fatal crash (80131506).
240+
Console.WriteLine("Waiting for CLR to initialize in target process...");
241+
bool clrReady = false;
242+
for (int i = 0; i < 300; i++) // 300 * 10ms = 3s max
243+
{
244+
if (ProcessHasModule(processId, "clrjit.dll"))
245+
{
246+
clrReady = true;
247+
break;
248+
}
249+
Thread.Sleep(10);
250+
}
251+
252+
if (!clrReady)
253+
{
254+
Console.WriteLine("WARNING: CLR may not be fully initialized, proceeding anyway...");
255+
}
256+
else
257+
{
258+
Console.WriteLine("CLR initialized, injecting...");
230259
}
231260

232261
try
@@ -303,6 +332,62 @@ private static void InjectBootstrap(int processId, string pluginLoaderPath)
303332
private const uint MEM_COMMIT = 0x1000;
304333
private const uint MEM_RESERVE = 0x2000;
305334
private const uint PAGE_READWRITE = 0x04;
335+
private const uint TH32CS_SNAPMODULE = 0x00000008;
336+
private const uint TH32CS_SNAPMODULE32 = 0x00000010;
337+
338+
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
339+
private struct MODULEENTRY32W
340+
{
341+
public uint dwSize;
342+
public uint th32ModuleID;
343+
public uint th32ProcessID;
344+
public uint GlblcntUsage;
345+
public uint ProccntUsage;
346+
public IntPtr modBaseAddr;
347+
public uint modBaseSize;
348+
public IntPtr hModule;
349+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
350+
public string szModule;
351+
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
352+
public string szExePath;
353+
}
354+
355+
private static bool ProcessHasModule(int processId, string moduleName)
356+
{
357+
IntPtr snap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, (uint)processId);
358+
if (snap == IntPtr.Zero || snap == new IntPtr(-1))
359+
return false;
360+
361+
try
362+
{
363+
var entry = new MODULEENTRY32W();
364+
entry.dwSize = (uint)Marshal.SizeOf(typeof(MODULEENTRY32W));
365+
366+
if (!Module32FirstW(snap, ref entry))
367+
return false;
368+
369+
do
370+
{
371+
if (entry.szModule.Equals(moduleName, StringComparison.OrdinalIgnoreCase))
372+
return true;
373+
} while (Module32NextW(snap, ref entry));
374+
375+
return false;
376+
}
377+
finally
378+
{
379+
CloseHandle(snap);
380+
}
381+
}
382+
383+
[DllImport("kernel32.dll", SetLastError = true)]
384+
private static extern IntPtr CreateToolhelp32Snapshot(uint dwFlags, uint th32ProcessID);
385+
386+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
387+
private static extern bool Module32FirstW(IntPtr hSnapshot, ref MODULEENTRY32W lpme);
388+
389+
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
390+
private static extern bool Module32NextW(IntPtr hSnapshot, ref MODULEENTRY32W lpme);
306391

307392
[DllImport("kernel32.dll", SetLastError = true)]
308393
private static extern IntPtr GetModuleHandle(string lpModuleName);

AffinityPluginLoader/AffinityPlugin.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,51 @@
44
namespace AffinityPluginLoader
55
{
66
/// <summary>
7-
/// Base class for all APL plugins. Extend this and override Initialize() to apply Harmony patches.
8-
/// Optionally override DefineSettings() to expose configuration in the preferences dialog.
7+
/// Base class for all APL plugins. Override stage methods to participate in the loading pipeline.
98
/// </summary>
109
public abstract class AffinityPlugin
1110
{
1211
/// <summary>
13-
/// Called when the plugin is loaded. Apply Harmony patches here.
12+
/// Stage 0: Called immediately after plugin discovery and settings init.
13+
/// No Affinity types are available yet. Use for early setup that doesn't touch Affinity code.
1414
/// </summary>
15-
public abstract void Initialize(Harmony harmony);
15+
public virtual void OnLoad(IPluginContext context) { }
16+
17+
/// <summary>
18+
/// Stage 1: Called when Serif assemblies are loaded, before Affinity's OnStartup() runs.
19+
/// Apply Harmony patches here. APL settings are available via context.Settings.
20+
/// </summary>
21+
public virtual void OnPatch(Harmony harmony, IPluginContext context) { }
22+
23+
/// <summary>
24+
/// Stage 2: Called after Affinity's InitialiseServices() completes.
25+
/// All Affinity services and settings are available in the DI container.
26+
/// </summary>
27+
public virtual void OnServicesReady(IPluginContext context) { }
28+
29+
/// <summary>
30+
/// Stage 3: Called after Affinity's OnServicesInitialised() completes.
31+
/// Full runtime is available including native engine, tools, and effects.
32+
/// </summary>
33+
public virtual void OnReady(IPluginContext context) { }
34+
35+
/// <summary>
36+
/// Stage 4: Called after the main window is loaded and visible.
37+
/// Full UI tree is available for custom panels, dialogs, and UI modifications.
38+
/// </summary>
39+
public virtual void OnUiReady(IPluginContext context) { }
40+
41+
/// <summary>
42+
/// Stage 5: Called after startup is fully complete — splash hidden, app idle.
43+
/// Safe to show dialogs, toasts, or do any work that should wait until the user
44+
/// is looking at a fully loaded application.
45+
/// </summary>
46+
public virtual void OnStartupComplete(IPluginContext context) { }
1647

1748
/// <summary>
1849
/// Override to define configuration options for this plugin.
1950
/// Returns null by default (no settings / no preferences tab).
51+
/// Called during Stage 0 before OnLoad.
2052
/// </summary>
2153
public virtual PluginSettingsDefinition DefineSettings() => null;
2254

0 commit comments

Comments
 (0)