feat(m4): single instance IPC

- LaunchCommand: ShowMain / OpenWorkspace(path), doubles as IPC message
- LaunchArguments: first non-blank arg wins, blank-only args -> ShowMain
- IpcProtocol: strict newline-line codec (show / open+path), tolerant
  of trailing-newline framing only
- SingleInstanceService: per-session named mutex decides primary;
  primary serves a named-pipe loop (decode -> dispatch -> ack) with
  handler failures contained; secondary retries send, exits on ack
- Program/App wiring: secondary forwards command and exits; primary
  activates the main window for forwarded commands (workspace switch
  deferred to the workspace UI milestone)
- 15 new tests (protocol codec, argument parsing); smoke-verified:
  instance 1 stays alive, instance 2 self-exits with code 0
This commit is contained in:
yyc12345 committed 2026-10-02 14:29:42 +08:00
1 parent b5614ff59b
commit 4fc8fdda6f
9 files changed
+485 -3

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
using YKanBan.Ipc;
namespace YKanBan.Tests.Ipc;
/// <summary>Tests for command-line argument parsing.</summary>
[TestClass]
public class LaunchArgumentsTests
{
[TestMethod]
public void NoArgumentsYieldShowMain()
{
Assert.AreEqual(new LaunchCommand.ShowMain(), LaunchArguments.Parse([]));
}
[TestMethod]
public void BlankArgumentsOnlyYieldShowMain()
{
Assert.AreEqual(new LaunchCommand.ShowMain(), LaunchArguments.Parse(["", " "]));
}
[TestMethod]
public void SingleArgumentBecomesWorkspacePath()
{
Assert.AreEqual(
new LaunchCommand.OpenWorkspace(@"D:\repo"),
LaunchArguments.Parse([@"D:\repo"]));
}
[TestMethod]
public void PathWithSpacesIsPreserved()
{
Assert.AreEqual(
new LaunchCommand.OpenWorkspace(@"D:\my repo\ws"),
LaunchArguments.Parse([@"D:\my repo\ws"]));
}
[TestMethod]
public void FirstMeaningfulArgumentWins()
{
Assert.AreEqual(
new LaunchCommand.OpenWorkspace(@"D:\a"),
LaunchArguments.Parse([@"D:\a", @"D:\b"]));
Assert.AreEqual(
new LaunchCommand.OpenWorkspace(@"D:\b"),
LaunchArguments.Parse(["", @"D:\b", @"D:\c"]));
}
}