Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion AffinityHook/Program.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
Expand Down Expand Up @@ -90,14 +91,16 @@ static int Main(string[] args)
Console.WriteLine();
}

var forwardedArgs = string.Join(" ", args.Select(a => a.Contains(" ") ? "\"" + a + "\"" : a));

Console.WriteLine($"Starting Affinity from: {affinityExe}");
Console.WriteLine($"Mode: {(detachMode ? "Detached" : "Attached")}");

// Start the process with inherited console
var startInfo = new ProcessStartInfo
{
FileName = affinityExe,
Arguments = string.Join(" ", args),
Arguments = forwardedArgs,
WorkingDirectory = affinityDir,
UseShellExecute = false,
CreateNoWindow = false
Expand Down
196 changes: 196 additions & 0 deletions WineFix/Patches/CommandLineFileOpenPatch.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
using System;
using System.IO;
using System.IO.Pipes;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Windows.Threading;
using HarmonyLib;
using AffinityPluginLoader.Core;

namespace WineFix.Patches
{
/// <summary>
/// Fixes command-line file opening on Wine.
///
/// Affinity's ProcessCommandLineArguments() references the WinRT type
/// SharedStorageAccessManager, which doesn't exist in Wine. The JIT throws a
/// TypeLoadException when compiling the method — even for code paths that don't
/// use it.
///
/// Two code paths are affected:
/// 1. Fresh launch: ProcessArguments() catches the exception silently, so CLI
/// file paths are never queued. Fixed by hooking OnMainWindowLoaded.
/// 2. Single-instance IPC: When a second instance sends file args via named pipe,
/// the first instance dispatches ProcessCommandLineArguments on the UI thread,
/// crashing the app. Fixed by replacing SingleInstanceThread.
/// </summary>
public static class CommandLineFileOpenPatch
{
private static MethodInfo _getService;
private static Type _iDocViewType;

public static void ApplyPatches(Harmony harmony)
{
Assembly serifAssembly = null;
foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
if (a.GetName().Name == "Serif.Affinity") { serifAssembly = a; break; }
if (serifAssembly == null) return;

var appType = serifAssembly.GetType("Serif.Affinity.Application");
if (appType == null) return;

// Cache reflection lookups
Assembly serifInterop = null;
foreach (var a in AppDomain.CurrentDomain.GetAssemblies())
if (a.GetName().Name == "Serif.Interop.Persona") { serifInterop = a; break; }
_iDocViewType = serifInterop?.GetType("Serif.Interop.Persona.Services.IDocumentViewService");

foreach (var m in appType.GetMethods(BindingFlags.Public | BindingFlags.Instance))
if (m.Name == "GetService" && m.IsGenericMethod && m.GetParameters().Length == 0)
{ _getService = m; break; }

// Patch 1: Fresh launch — open files after main window loads
if (Environment.GetCommandLineArgs().Length >= 2)
{
var onLoaded = appType.GetMethod("OnMainWindowLoaded",
BindingFlags.NonPublic | BindingFlags.Instance);
if (onLoaded != null)
{
harmony.Patch(onLoaded,
postfix: new HarmonyMethod(typeof(CommandLineFileOpenPatch), nameof(OnMainWindowLoaded_Postfix)));
Logger.Info("Patched OnMainWindowLoaded for CLI file opening");
}
}

// Patch 2: Single-instance IPC — replace thread to avoid ProcessCommandLineArguments crash
var singleInstanceThread = appType.GetMethod("SingleInstanceThread",
BindingFlags.NonPublic | BindingFlags.Static);
if (singleInstanceThread != null)
{
harmony.Patch(singleInstanceThread,
prefix: new HarmonyMethod(typeof(CommandLineFileOpenPatch), nameof(SingleInstanceThread_Prefix)));
Logger.Info("Patched SingleInstanceThread for IPC file opening");
}
}

public static void OnMainWindowLoaded_Postfix(object __instance)
{
try
{
var filePaths = Environment.GetCommandLineArgs().Skip(1)
.Where(a => !a.StartsWith("--") && !a.StartsWith("affinity-open-file:"))
.ToList();

if (filePaths.Count == 0) return;

Dispatcher.CurrentDispatcher.BeginInvoke(
DispatcherPriority.Background,
new Action(() => OpenFiles(__instance, filePaths.ToArray())));
}
catch (Exception ex)
{
Logger.Error("CommandLineFileOpenPatch OnMainWindowLoaded error", ex);
}
}

/// <summary>
/// Replaces SingleInstanceThread to avoid calling ProcessCommandLineArguments.
/// Reimplements the named pipe listener, parsing file args ourselves.
/// </summary>
public static bool SingleInstanceThread_Prefix()
{
// Get the Application instance and required fields
var appType = AppDomain.CurrentDomain.GetAssemblies()
.First(a => a.GetName().Name == "Serif.Affinity")
.GetType("Serif.Affinity.Application");

var currentProp = appType.GetProperty("Current",
BindingFlags.Public | BindingFlags.Static);
var closingField = appType.GetField("m_closing",
BindingFlags.NonPublic | BindingFlags.Instance);
var delayField = appType.GetField("m_delayDocumentOpen",
BindingFlags.NonPublic | BindingFlags.Instance);
var singleIdProp = appType.GetProperty("SingleInstanceId",
BindingFlags.NonPublic | BindingFlags.Instance);

var app = currentProp.GetValue(null);
var singleInstanceId = (string)singleIdProp.GetValue(app);

var thread = new Thread(() =>
{
while (!(bool)closingField.GetValue(app))
{
try
{
using (var pipe = new NamedPipeServerStream(singleInstanceId))
{
pipe.WaitForConnection();

while ((bool)delayField.GetValue(app))
Thread.Sleep(500);

if ((bool)closingField.GetValue(app))
continue;

try
{
using (var reader = new BinaryReader(pipe))
{
var text = reader.ReadString();
var arguments = text.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);

// Skip first arg (exe path), filter flags
var filePaths = arguments.Skip(1)
.Where(a => !a.StartsWith("--") && !a.StartsWith("affinity-open-file:"))
.ToArray();

if (filePaths.Length > 0)
{
((DispatcherObject)app).Dispatcher.BeginInvoke(
new Action(() => OpenFiles(app, filePaths)));
}
}
}
catch (Exception) { }
}
}
catch (Exception) { }
}
});
thread.IsBackground = true;
thread.Start();

return false; // skip original
}

private static void OpenFiles(object appInstance, string[] paths)
{
try
{
if (_getService == null || _iDocViewType == null) return;

var svc = _getService.MakeGenericMethod(_iDocViewType).Invoke(appInstance, null);
var loadDoc = svc.GetType().GetMethod("LoadDocument",
BindingFlags.Public | BindingFlags.Instance,
null, new[] { typeof(string), typeof(bool), typeof(bool), typeof(bool) }, null);
if (loadDoc == null) return;

// Activate main window
var activateMethod = appInstance.GetType().GetMethod("ActivateMainWindow",
BindingFlags.NonPublic | BindingFlags.Instance);
activateMethod?.Invoke(appInstance, null);

foreach (var path in paths)
{
loadDoc.Invoke(svc, new object[] { path, true, false, false });
Logger.Info($"Opened file: {path}");
}
}
catch (Exception ex)
{
Logger.Error("Failed to open file", ex);
}
}
}
}
1 change: 1 addition & 0 deletions WineFix/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ For detailed instructions, see the [WineFix Installation Guide](https://apl.ncur
- **Color picker Wayland fix** — Color picker zoom preview displays a black image on Wayland. Fixed by patching the magnifier capture to use Wine's window capture instead of screen capture.
- **Font enumeration fix** — Intermittent startup crash from parallel font enumeration. Fixed by forcing synchronous font loading.
- **Canva sign-in helper** — Canva sign-in helper to allow copy/paste of the authorization URL to complete sign-in, no protocol handler required.
- **Command-line file opening fix** — Opening files from the desktop or command line crashes or silently fails due to a missing WinRT type. Fixed by bypassing `ProcessCommandLineArguments` and opening files directly via `IDocumentViewService`.

## Configuration

Expand Down
12 changes: 12 additions & 0 deletions WineFix/WineFixPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public class WineFixPlugin : AffinityPlugin
public const string CollinearJoinFixKey = "collinear_join_fix";
public const string SettingForceSyncFontEnum = "force_sync_font_enum";
public const string SettingCanvaSignInHelper = "canva_sign_in_helper";
public const string SettingCommandLineFileOpen = "command_line_file_open";

public override PluginSettingsDefinition DefineSettings()
{
Expand Down Expand Up @@ -54,6 +55,10 @@ public override PluginSettingsDefinition DefineSettings()
},
defaultValue: "native",
description: "- **Native:** Use Affinity's built-in color sampling. Colors sampled within the canvas bounds will use the native document color space, but the color of the highlighted pixel in the zoom preview may differ slightly from the actual color value sampled.\n- **Exact:** Pick the exact color of the highlighted pixel in the zoom preview. Samples from a screen capture in sRGB rather than the document's native color space. May be more intuitive, but not recommended when editing documents using CMYK or wide-gamut color spaces.")
.AddBool(SettingCommandLineFileOpen, "Command-line file opening fix",
defaultValue: true,
restartRequired: true,
description: "Fix opening files from the command line or desktop file manager.")
.AddSection("Crash Fixes")
.AddBool(SettingForceSyncFontEnum, "Force synchronous font enumeration",
defaultValue: true,
Expand All @@ -63,6 +68,13 @@ public override PluginSettingsDefinition DefineSettings()

public override void OnPatch(Harmony harmony, IPluginContext context)
{
// Fix command-line file opening (WinRT TypeLoadException workaround)
if (context.Settings.GetEffectiveValue<bool>(SettingCommandLineFileOpen))
{
context.Patch("CommandLineFileOpen fix",
h => Patches.CommandLineFileOpenPatch.ApplyPatches(h));
}

// Since these patch native code that we load ourselves,
// we don't need to apply these with the defferal logic.
if (context.Settings.GetEffectiveValue<bool>(BezierRenderingFixKey))
Expand Down
3 changes: 3 additions & 0 deletions docs/winefix/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ WineFix uses plugin ID `winefix`. Settings are stored in `apl/config/winefix.tom
|---|---|---|---|---|
| `color_picker_magnifier_fix` | enum | `auto` | Yes | Wayland zoom preview fix. Replaces `CopyFromScreen` (which returns black on Wayland) with a `BitBlt` from the canvas window. |
| `color_picker_sampling_mode` | enum | `native` | No | Controls how the color picker samples color values. See [Sampling Modes](index.md#color-picker-sampling-modes). |
| `canva_sign_in_helper` | bool | `true` | Yes | Patch the Canva sign-in dialog to include a helper textbox and instructions to complete sign-in without a protocol URL handler. |
| `command_line_file_open` | bool | `true` | Yes | Fix opening files from the command line or desktop file manager. Bypasses Affinity's broken `ProcessCommandLineArguments` which references a WinRT type missing in Wine. |

#### `color_picker_magnifier_fix`

Expand Down Expand Up @@ -52,5 +54,6 @@ APL__WINEFIX__<KEY>=<value>
| Color picker magnifier fix | `APL__WINEFIX__COLOR_PICKER_MAGNIFIER_FIX` | `APL__WINEFIX__COLOR_PICKER_MAGNIFIER_FIX=disabled` |
| Color picker sampling mode | `APL__WINEFIX__COLOR_PICKER_SAMPLING_MODE` | `APL__WINEFIX__COLOR_PICKER_SAMPLING_MODE=exact` |
| Force synchronous font enumeration | `APL__WINEFIX__FORCE_SYNC_FONT_ENUM` | `APL__WINEFIX__FORCE_SYNC_FONT_ENUM=false` |
| Command-line file opening fix | `APL__WINEFIX__COMMAND_LINE_FILE_OPEN` | `APL__WINEFIX__COMMAND_LINE_FILE_OPEN=false` |

Environment variable overrides take priority over both the GUI and TOML values. They are temporary — the override only applies while the variable is set.
6 changes: 6 additions & 0 deletions docs/winefix/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ Use Native for color-accurate work (especially CMYK or wide-gamut documents). Us

Intermittent startup crash from parallel font enumeration in `libkernel.dll`. Forces synchronous font loading. Enabled by default; [configurable](configuration.md).

### Command-line file opening fix

Opening `.af` files from the Linux desktop (e.g. double-clicking in a file manager) or via command line arguments fails under Wine. Affinity's `ProcessCommandLineArguments()` references the WinRT type `SharedStorageAccessManager`, which doesn't exist in Wine. The JIT throws a `TypeLoadException` when compiling the method — even for code paths that don't use it — which silently prevents file paths from being queued on a fresh launch, and crashes the app when a second instance sends files to an already-running instance via the single-instance IPC pipe.

WineFix hooks `OnMainWindowLoaded` to open files from `GetCommandLineArgs()` directly via `IDocumentViewService`, and replaces `SingleInstanceThread` with a Wine-compatible implementation that avoids calling `ProcessCommandLineArguments`. Enabled by default; [configurable](configuration.md).

## Known Open Bugs

These are under investigation and not yet patched:
Expand Down
Loading