Skip to content

Windows port: tray app + CLI sharing a C# engine - #2

Open
Goary wants to merge 5 commits into
SonghaiFan:mainfrom
Goary:windows-port
Open

Goary wants to merge 5 commits into
SonghaiFan:mainfrom
Goary:windows-port

Conversation

@Goary

@Goary Goary commented Sep 23, 2026 •

Copy link
Copy Markdown

What

A Windows port of LeftOpen — a system-tray app plus a leftopen CLI sharing one C# engine — in windows/. The macOS sources are untouched.

How the platform maps

macOS Windows
lsof -iTCP -sTCP:LISTEN GetExtendedTcpTable (iphlpapi, same data netstat uses)
ps -axo pid,ppid,comm one WMI query (pid / ppid / exe path / cmdline / start time)
lsof -d cwd PEB read (RTL_USER_PROCESS_PARAMETERS.CurrentDirectory)
.app bundles exe under Program Files / WindowsApps / per-user Programs
SIGTERM-only gentle close WM_CLOSE only, to a window verified to belong to the target PID; console Ctrl+C / Ctrl+Break are refused (their recipients cannot be restricted to the verified target — the safety probe proves group events reach descendants)
MenuBarExtra panel NotifyIcon + popup panel (scans only while open, never polls)

Faithful to the original

  • Same three-stage close: plan -> re-verify PID identity (start time + owner SID + exe path) right before signalling -> poll; refuses system / other-user / installed-app targets; reports honestly when a process ignores the gentle close.
  • Ownership inference with confidence + reason strings, project marker walk-up with the same rejection rules (node_modules, caches, app-data trees, home root, hidden dirs).
  • Icon system mirrors yours:
    • tray = DoorMark (geometry ported 1:1 from the Swift DoorMark) plus the closable-port count, with closed / open / error states and taskbar light/dark adaptation;
    • row icons follow the ProcessIconView tier chain: app exe icon (skipped for shared runtimes) -> project-shipped icon (Tauri / Next.js / public / static conventions, plus the index.html-declared favicon) -> live favicon from the running server -> runtime symbol with the original colouring -> category symbol;
    • the exe icon is generated from the original artwork (windows/Scripts/make-icon.ps1).
  • --snapshot renders the panel and tray states to PNG, like snapshot-panel.sh.
  • CLI keeps the same command surface (leftopen [port] / open / close / --json / --dry-run).

Honest deviations

  • SF Symbols do not exist on Windows; symbols come from Segoe MDL2 Assets.
  • http.sys listeners (e.g. .NET HttpListener) appear as PID 4 (SYSTEM) and are therefore not closable — that is Windows, not a misclassification.
  • .icns / .svg project icons cannot be decoded on Windows; Tier 2b (npm global package icons) is not implemented yet.

Testing

  • 56 unit tests: marker detection + rejection rules, inference priority, LAN scope, parent-chain cycle guard, close protection rules, PID-reuse verification, undelivered-signal reporting, and a TCP port byte-order regression.
  • End-to-end on real Windows: windowed targets close via verified WM_CLOSE; console-only dev servers are honestly refused with an explanatory message (fail-closed, per the review) and survive untouched; a dedicated console-safety probe (4 PASS phases) plus 60 unit tests run in CI.
  • .github/workflows/windows-ci.yml runs on every windows/ change: Release build → unit tests → console-safety probe → LeftOpen-windows-x64.zip (~67 MB, self-contained shared-runtime folder — unzip and run, no .NET install) uploaded as an artifact; windows-v* tags attach it to a GitHub Release.

中文说明:这是 LeftOpen 的 Windows 移植版,位于 windows/ 目录,macOS 源码未做任何改动。托盘应用的图标体系(DoorMark + 可关闭端口数、行内四层图标链)、三段式温和关闭、归属推断的可解释性都与原版一一对应;56 个单元测试 + 真实 dev server 的端到端验证。

yan.gao1 added 2 commits September 20, 2026 19:01
- LeftOpen.Core: TCP listeners via GetExtendedTcpTable, process facts via WMI,
  working directories via PEB reads, project marker walk-up with rejection
  rules, ownership inference with confidence and reasons, and a three-stage
  gentle close (plan -> verify PID identity -> signal and poll)
- LeftOpen.Tray: WinForms tray app with a door icon and an on-demand popup
  panel (scan on open, grouped rows, two-step close confirmation)
- LeftOpen.Cli: leftopen list / <port> / open / close / --json
- Gentle close ladder on Windows: WM_CLOSE -> console Ctrl+C -> Ctrl+Break,
  with a self-protecting ctrl handler; never force-kills
- 56 unit tests
- Tray icon mirrors the original MenuBarExtra label: DoorMark (ported
  1:1 from MenuPanel.swift path data) plus the closable-port count,
  with closed/open/error states and taskbar theme adaptation; error
  glyph from Segoe MDL2 Assets (SF Symbols' Windows counterpart)
- exe icon: multi-size .ico generated from the original repo artwork
  (windows/Scripts/make-icon.ps1)
- Per-row icons follow the original ProcessIconView tier chain: app
  exe icon (skipped for shared runtimes) -> project-shipped icon
  (Tauri/Next.js/public conventions + index.html-declared favicon) ->
  live favicon from the running server -> runtime symbol with the
  original's colouring -> category symbol
- Row layout: icon and owner on one line, port+command+pid subline,
  full project names without truncation, panel widened to 480px
- Panel no longer hides when Windows refuses the focus change
  (flash-and-vanish bug); anchors to the taskbar's screen; tray click
  toggles; --snapshot renders the panel (and tray states) to PNG for
  deterministic verification, like the original's snapshot-panel
- publish.ps1: fix repo-root resolution and project paths after the
  windows/ move; default output windows/dist
@Goary

Goary commented Sep 23, 2026

Copy link
Copy Markdown
Author

Hi,我之前在抖音联系过您,借鉴您的创意,我做了一个 Windows 移植版,如果方向 OK 我可以继续维护,也欢迎提需求

@SonghaiFan

Copy link
Copy Markdown
Owner

谢谢你做 Windows 移植,也谢谢你愿意继续维护!这个方向很欢迎 😊

想先一起把 Windows 预览版发布出来。方便的话,可以帮忙补上 Windows 的 CI 测试和自动打包,提供解压后就能运行、不需要用户额外安装 .NET 的版本吗?希望尽量降低用户的使用门槛。再次感谢!

Copy link
Copy Markdown
Owner

Windows close safety review of head 6c901c0181d0254434c85fa65cbbcdb77f686d5b — please keep this Draft until Windows verification is complete.

Confirmed by code review and Microsoft API documentation: after AttachConsole(pid), both existing GenerateConsoleCtrlEvent(..., 0) calls broadcast to all processes sharing that console. Verifying the selected PID's SID/executable/start time does not constrain those recipients. Also, substituting the PID is not sufficient: Ctrl+C cannot target a process group, and Ctrl+Break to a group can reach descendants. We do not create these services or have a verified group identity/membership.
Reference: https://learn.microsoft.com/en-us/windows/console/generateconsolectrlevent

Minimal fail-closed patch prepared: commit 4fc88a9, on fix/windows-console-close-safety, directly based on this PR's head. The GitHub integration cannot write to the contributor fork (403), so the source branch of PR #2 has NOT been changed. Please cherry-pick this commit into windows-port.

Changes:

  • Remove console attachment and ALL console Ctrl+C/Ctrl+Break generation from production code. No group/PID guessing and no force-kill fallback.
  • Only post WM_CLOSE to the exact window handle after checking its owning PID; reject a console-host/other-process window. Return the actual posting result instead of claiming delivery regardless of API success.
  • CLI/tray confirmation and refusal messages explain the safety boundary. Ordinary console-only dev servers will remain running and must be stopped in their original terminal.
  • Keep the existing prepare → confirm → fresh identity verification → poll flow. CLI cancellation/dry-run and the tray confirmation button remain intact; --yes does not enable unsafe signalling.
  • Add four regression cases proving changes to start time, SID, executable or port peers after preview prevent ANY terminator invocation.
  • Add a separate native Windows console regression probe at windows/tests/LeftOpen.ConsoleSafety.Probe. It detaches from the launching console, allocates a dedicated console, starts a shared-group worker, an explicit CREATE_NEW_PROCESS_GROUP worker and its descendant, and verifies exact controlled console membership before generating any test events. It records actual received control events to demonstrate broadcast scope, nonzero-group Ctrl+C behavior and Ctrl+Break descendant scope, then checks production closes are refused and all workers survive without new events. Workers use handlers plus a cooperative stop file/timeout for cleanup.

Validation status — not a Windows pass: this environment is Linux and has no .NET SDK. git diff --check passed. dotnet test windows/LeftOpen.sln could not run (dotnet: command not found). Neither the new tests/probe nor the existing 56 tests have been compiled or executed here. The API-scope finding is a documentation/code-review conclusion, not an observed Windows run.

Before merging/releasing, please run on real Windows + .NET 8 SDK from windows/:

dotnet test LeftOpen.sln
dotnet run --project tests/LeftOpen.ConsoleSafety.Probe
dotnet build LeftOpen.sln

Please retain the output and verify tray/CLI refusal messages, confirmation/cancellation, and the target-owned WM_CLOSE path. The PR's current description of Ctrl+C/Ctrl+Break and prior “no collateral damage” E2E claim also needs updating to reflect this restriction and the actual new test results. CI/self-contained packaging work already discussed remains yours; this review does not reassign it. No merge or preview release was performed.

SonghaiFan and others added 3 commits October 12, 2026 11:28
… packaging

- .github/workflows/windows-ci.yml: on windows/** changes build Release,
  run the 60 unit tests, run the console-safety probe, then package the
  self-contained shared-runtime zip (LeftOpen-windows-x64.zip, ~67 MB,
  unzip and run, no .NET install) and upload it as an artifact; tags
  windows-v* attach it to a GitHub release
- publish.ps1: -Shared mode produces the same release layout CI uses
- README: document the CI and the release packaging
Running the reviewer's probe on real Windows (first execution anywhere;
it was authored in a Linux environment without .NET) showed phase C
asserted the opposite of observed behaviour:

- GenerateConsoleCtrlEvent(CTRL_C_EVENT, group) does not fail and
  delivers to the group root AND its descendant (marker files prove it),
  while a console-mate outside the group is spared. The probe now
  asserts this observed delivery pattern instead of "no events".
- The safety conclusion is unchanged and stronger: a group includes
  descendant processes LeftOpen did not create, so production
  correctly refuses every console control event.

Also report probe failures on stdout: after FreeConsole/AllocConsole
the original stderr handle is dead and .NET silently drops
Console.Error writes, which made CI failures undiagnosable.

Probe output now: four PASS phases, exit 0, on Win10 19045 and on the
windows-latest CI runner.
@Goary

Goary commented Oct 12, 2026

Copy link
Copy Markdown
Author

Windows verification of the safety patch — real Windows 10 19045 + .NET 8 SDK:

Cherry-picked 4fc88a9 as fa2c40a, then ran the requested checks:

  • dotnet test windows/LeftOpen.sln: 60/60 passed (56 existing + 4 new refusal regressions)
  • dotnet run --project windows/tests/LeftOpen.ConsoleSafety.Probe: all 4 phases PASS, exit 0 — production closes refused with no events delivered, all controlled workers survive
  • dotnet build windows/LeftOpen.sln -c Release: succeeded
  • E2E refusal behaviour verified against a real dev server: a hidden-console node process on :37777 was identified as a project, leftopen close refused it with the safety explanation, the process survived, exit code 1 — exactly as designed

One empirical correction to the probe (please review 00baeb6): this was the first execution of the probe anywhere — it was authored on Linux without .NET. On real Windows, phase C asserted the opposite of observed behaviour: GenerateConsoleCtrlEvent(CTRL_C_EVENT, group) does not fail — marker files prove it delivers to the group root and its descendant, sparing console-mates outside the group. The probe now asserts that observed delivery pattern. Your core safety conclusion is unchanged and, if anything, reinforced: a group includes descendants LeftOpen did not create, so refusing every console control event is correct. I also made probe failures report on stdout (after FreeConsole/AllocConsole the original stderr handle is dead and .NET silently drops Console.Error writes, which made the first CI failure undiagnosable).

Maintainer's requested items, also done (b035d91):

  1. Windows CI — .github/workflows/windows-ci.yml: on every change to windows/ → Release build → 60 unit tests → console-safety probe → packaging. It is green on both the fork runner and is verified: https://github.com/Goary/leftopen/actions/runs/38209816606
  2. Self-contained, unzip-and-run package — CI builds LeftOpen-windows-x64.zip (~67 MB; both executables share one self-contained runtime folder, which is ~24 MB smaller than two standalone single-file exes) and uploads it as a build artifact; windows-v* tags attach it to a GitHub Release automatically. publish.ps1 -Shared produces the identical package locally.

PR description updated to reflect the WM_CLOSE-only safety boundary. Happy to iterate further.

@Goary
Goary marked this pull request as ready for review October 12, 2026 06:49

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants