Implement wtcli, an application to query details about an existing Terminal instance - #20461
Implement wtcli, an application to query details about an existing Terminal instance#20461PankajBhojwani wants to merge 18 commits into
wtcli, an application to query details about an existing Terminal instance#20461Conversation
There was a problem hiding this comment.
check-spelling found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
…pane Windows Terminal has no way for another process to say "read that pane" or "type this into that pane". The only channel available from outside is UI Automation plus SendKeys, and it is broken in two ways that cannot be fixed from outside the process: synthesised keystrokes interleave with the user's own typing (a real capture: /rename my-session arrived as //rreenn/ramemnea mne), and they can only reach the foreground window, addressing a whole window rather than a pane. With any split on screen the write lands somewhere unpredictable. So serve a named pipe, \\.\pipe\wt-control-<pid>, one per Terminal process. Four operations, newline-delimited UTF-8 JSON: ping, list-panes, capture-pane, send-input. Text goes to the connection, not the keyboard. Guarantees the design is built around: * No focus involvement. No SetForegroundWindow, no activation, no tab switching, no Focus(). It works against a minimised window on another virtual desktop with the pane in a background tab, and the user's focus ends up exactly where it started. * send-input takes a requireContains needle that is re-checked against the pane's screen atomically with the write, on the same trip to the UI thread. If the needle has gone, nothing is written. Between a client's find and its write, tabs can be reordered and panes closed; the failure that guard prevents is a command typed into somebody else's shell. * Verbatim writes. Nothing added, escaped or coalesced - a single write containing both text and \r is read by some TUIs as a bulk paste and never submits, so the caller sends Enter as its own call. * No UI-thread blocking. Accept, read, parse, serialise and process-name lookups happen on the pipe threads; only the pane touch is marshalled, via WM_CONTROL_PIPE_REQUEST to WindowEmperor's message window. Security: the pipe is created with an explicit DACL granting the current user's SID and nothing else, protected against inheritance, and rejects remote clients. The op set cannot run a command, touch a file, change a setting or create a pane. Connections are traced; the text being written never is. The `controlPipe` global setting (default true here) is a kill switch that takes effect on settings reload without a restart. Upstream has refused wt send-input twice, in microsoft#9368 and PR microsoft#20106, and the automation surface they are building in PR microsoft#20461 (wtcli, over COM) leaves input out on purpose and cannot be reached from WSL. This is fork-only; list-panes reports each pane's connection SessionId as well, which is how upstream addresses panes, so the two can be reconciled later. Documented in doc/control-pipe.md. Wire format parsing and the pane-id round trip are covered by UnitTests_Control/ControlPipeProtocolTests.cpp; the pipe itself was verified end to end against a dev build with both windows minimised - split panes, a background tab, a second window, the needle-gone and disconnected paths, a client killed mid-request, and the same run driven from WSL.
This comment has been minimized.
This comment has been minimized.
Leonard Hecker (lhecker)
left a comment
There was a problem hiding this comment.
BTW if we plan to just upstream this existing (?) code, I'm fine with just noting down some nits. Otherwise, I have some ideas how to improve it, like the STA/MTA thing.
|
|
||
| // Derive a per-install-path CLSID using the same v5 UUID formula as the server, | ||
| // so that unpackaged/portable clients find the correct matching server instance. | ||
| static GUID _portableClsid(const GUID& brandClsid) |
There was a problem hiding this comment.
Should we put these functions into src/types? We got CreateV5Uuid there already and we could link wtcli with it.
Unless you want to keep it portable of course.
| auto strong = get_strong(); | ||
| co_await wil::resume_foreground(Dispatcher()); |
There was a problem hiding this comment.
Is it not possible to run the COM server on the STA main thread?
|
|
||
| // Register the COM class factory on a dedicated MTA thread so that | ||
| // incoming COM calls are dispatched to MTA worker threads rather than | ||
| // the STA/UI thread. This keeps long-running calls off the UI thread — |
There was a problem hiding this comment.
Ah here's the answer. But ...long-running? What calls do we foresee to be computationally expensive?
Summary of the Pull Request
Implements
wtcli, a new command-line client for Windows Terminal Protocol: a local COM control surface that lets external processes inspect and drive a running Windows Terminal instance. It provides a tmux-style command vocabulary for the terminal.wtclicurrently exposes 10 commands:list-windows(lsw),list-tabs(lst),list-panes(
lsp),active-pane,pane-statuscapture-pane(capturep) — including--last-prompttograb the most recent completed shell command via OSC 133 shell integration
new-tab(neww),split-pane(splitw),kill-pane(
killp),focus-pane(focusp)Every command supports
--jsonfor machine-readable output, otherwise results are rendered as human-readable tables.References and Relevant Issues
Detailed Description of the Pull Request / Additional comments
A client activates the server directly with
CoCreateInstanceusing a per-brand CLSID (Release/Preview/Canary/Dev, selected at compile time), and issues typed commands.Components:
1.
wtcli.exe: the CLI client (src/tools/wtcli/)The user-facing tool. Parses subcommands, activates the protocol server via
WT_COM_CLSID, and calls the interface methods.2.
ITerminalProtocol: the classic-COM interface (src/host/proxy/ITerminalProtocol.idl)The contract between client and Terminal. A classic-COM interface marshaled by the MIDL proxy/stub in
OpenConsoleProxy.dll(registered per-brand in the app manifests). Scalar arguments stay typed; every complex result crosses the boundary as a JSONBSTR.3.
TerminalProtocolComServer: the in-proc server (src/cascadia/WindowsTerminal/)Implements
ITerminalProtocolinsideWindowsTerminal.exe.4.
TerminalPageprotocol bridge (src/cascadia/TerminalApp/TerminalPage.Protocol.cpp)The UI-thread methods that actually query and mutate live terminal state (windows, tabs, panes, process status, pane output) and return typed results.
5.
TerminalProtocolshared library (src/cascadia/TerminalProtocol/)Hosts the shared WinRT struct types (
WindowInfo,TabInfo,PaneInfo, etc) consumed in-process by both the page bridge and the server. Also providesProtocolParsing.h, a header of parsing helpers (ParseSplitDirection,ClassifyPaneOutputSource) shared by the server and the fuzzer.6. Protocol fuzzer (
src/cascadia/TerminalProtocol/ft_fuzzer/)A LibFuzzer harness targeting the parsing functions (
ParseSplitDirection,ClassifyPaneOutputSource).Validation Steps Performed
In a dev build of terminal, all 10 of the commands work
PR Checklist