Repository navigation
Description
What's wrong
SingleAppInstance reads and writes its PID file with reflection-based System.Text.Json:
SingleAppInstance/SingleAppInstance.cs:217:JsonSerializer.Deserialize<ProcessInfo>(ref reader)SingleAppInstance/SingleAppInstance.cs:472:JsonSerializer.Serialize(processInfo)
An app published with PublishTrimmed=true or PublishAot=true gets System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault=false by default, which is visible in the published runtimeconfig. Under that setting, both calls throw InvalidOperationException. Neither call site catches it:
TryWritePidFilecatches onlyIOExceptionandUnauthorizedAccessException.- The read path catches only
JsonException.
The exception therefore escapes ShouldLaunch() and ExitIfAlreadyRunning(), and the app dies at startup:
Unhandled exception. System.InvalidOperationException: Reflection-based serialization has been disabled for this application ...
at ktsu.SingleAppInstance.SingleAppInstance.WritePidFile() line 472
How to reproduce
- Create a console app that references the library and calls
SingleAppInstance.ShouldLaunch(). - Either publish it with
dotnet publish -r linux-x64 --self-contained -p:PublishTrimmed=true, or keep the normal JIT build and setSystem.Text.Json.JsonSerializer.IsReflectionEnabledByDefaulttofalsein its runtimeconfig. - Run it.
The JIT variant reproduces the crash directly. The publish itself only reports IL2026 warnings for lines 217 and 472, which are easy to miss.
Note on trimmed publishes: a trimmed publish currently hits an earlier failure before it reaches the JSON calls. ReadPidFileState throws TypeLoadException: Method 'CompareTo' in type 'ktsu.Semantics.Paths.AbsoluteFilePath' ... does not have an implementation, which is a trimming problem in ktsu.Semantics.Paths. Rooting the Semantics assemblies gets past it, and the app then fails with the JSON exception above. Fixing this repository's JSON usage is needed either way.
Why it matters
Single-instance enforcement is mostly used by desktop apps, tray apps and CLI tools. Those are the apps most likely to be published trimmed, single-file or AOT. For them, the library turns "only one instance" into "no instance": the app crashes on every launch.
Suggested fix
- Add a source-generated context, for example
[JsonSerializable(typeof(ProcessInfo))] internal sealed partial class ProcessInfoJsonContext : JsonSerializerContext;, and passProcessInfoJsonContext.Default.ProcessInfoto both calls. - Set
<IsAotCompatible>true</IsAotCompatible>(orIsTrimmable) on the library project for the TFMs that support it, so the trim and AOT analyzers flag regressions at build time.
Acceptance criteria
- With
IsReflectionEnabledByDefault=false,ShouldLaunch()writes the PID file and returnstruefor the first instance, andfalsefor a second instance while the first is running. - Building the library produces no IL2026/IL3050 warnings.
- Existing PID files written by the current version still read correctly. The JSON shape is unchanged.
Activity
matt-edmondson commentedon Nov 4, 2026
Triage
ShouldLaunch(), with no workaround short of turning reflection back on.SingleAppInstance.csread at :217, write at :472)JsonSerializerContextforProcessInfoand pass itsJsonTypeInfoto both calls. A trimmed or AOT smoke test in CI would stop this coming back.Generated by Claude Code