Skip to content

feat(connections): reach a database through a tunnel command - #2638

Merged
datlechin merged 2 commits into
mainfrom
feat/tunnel-command
Sep 5, 2026
Merged

feat(connections): reach a database through a tunnel command#2638
datlechin merged 2 commits into
mainfrom
feat/tunnel-command

Conversation

@datlechin

Copy link
Copy Markdown
Member

Adds a fifth ConnectionTunnelKind: a process TablePro starts on connect, holds for the life of the connection, and restarts when it dies. Presets cover kubectl port-forward and aws ssm start-session; a third method takes a command line.

Fixes #2520

Approach

This follows the extension point CLAUDE.md already names: a new tunnel type extends ConnectionTunnelKind, activeTunnelManager(for:) and buildEffectiveConnection, rather than adding per-site branches. TunnelCommandManager is shaped like CloudSQLProxyManager (allocate a loopback port, start the process, wait for the port or for the process to give up, watch it afterwards) and reuses SupervisedProcessRunner, LoopbackPort, StaleProcessReaper and recoverDeadTunnel unchanged.

The local port is TablePro's to pick. {port} marks where it goes; {host} and {remotePort} carry the connection's own endpoint. Presets read the remote port from the connection, so it is never typed twice.

No implicit shell. The command is split into an argument vector the way a shell would split it, quoting included, and execved directly. Two reasons: a shell between TablePro and the forward is one more thing to signal, and nothing is expanded, so $HOME stays four characters. A user who wants a shell writes /usr/bin/env or /bin/sh -c as the first word. Substitution runs after the split, so a value carrying a space cannot become two arguments.

A preset carries parameters, not code. Every preset flag is written --flag=value, so no field can be read as a flag of its own, and the two positional arguments kubectl still needs are validated instead (TunnelCommandBuilderTests.presetValuesCannotBecomeFlags).

Killing the process is the load-bearing part

ProcessSupervisedRunner.stop() sent SIGTERM to the child pid and nothing else. aws ssm start-session runs session-manager-plugin beside itself, and the plugin is what actually holds the forwarded port, so signalling the pid alone leaves an orphan holding it.

Measured on this machine: Foundation's Process puts every child in its own process group (getpgid(child) == child, distinct from ours) and descendants inherit it. stop() now signals the group and escalates to SIGKILL after two seconds, with the guard that both terminationResult and isRunning must still say the process is live, so a forced kill can never reach a group that inherited a recycled pid. Cloudflare and Cloud SQL Proxy get the same improvement. SupervisedProcessRunnerTests pins both the group-leadership fact and the descendant teardown.

A cancelled connect now stops the process too. It is not registered as a tunnel until it is ready, so nothing else would have closed it.

Sharing, and the arbitrary-code question

The issue asked for a confirmation on import. The change does both halves:

Route What happens
iCloud sync Not carried. SyncRecordMapper excludes it with the other three tunnel modes
tablepro:// deeplink, team library Stripped before the connection is offered
.tablepro file Offered, and importing prints every command in full and asks. keepTunnelCommands defaults to false, so a route that never asks cannot let one in by omission
Hand-edited connections.json ConnectionStoreIntegrity already detects this. The tunnel refuses to launch until the connection is saved again in the app, the same gate PasswordSource uses

The split is deliberate: a deeplink is a click, a file import is a decision.

Before / After

New pane, so there is no before.

docs/images/tunnel-command-pane*.png are placeholder cards at 1560x960, which is what socks-proxy-pane.png, cloud-sql-proxy-pane.png and cloudflare-tunnel-pane.png already are (a shipped shot is 3024x1722). The page markup and dimensions are right; a real capture is still owed, and I did not take one because another session is running builds in this checkout and a second TablePro instance breaks an in-flight test host.

Verification

Run in an isolated worktree, because the main checkout has unrelated in-flight work that does not compile.

  • verify.sh build: PASS
  • verify.sh test over 15 suites (the five new TunnelCommand* suites, SupervisedProcessRunnerTests, and the nine existing suites that own the types this touches): PASS, 117 of 117 cases
  • verify.sh docs: PASS, house style and source claims both agree
  • swiftlint --strict over the 51 changed Swift files: clean

Codex could not review this diff: the account is out of credits until Sep 7 (Codex error: You've hit your usage limit, read from the job log, not stdout). Skill(code-review) at high effort read it instead and found one thing on this branch: TunnelCommandLine.ParseError escaping unlocalized, which made TunnelCommandError.commandEmpty and .unbalancedQuote unreachable and would have shown "The operation couldn't be completed" for an imported command with an unclosed quote. Fixed, with a test.

No UI automation. The pane's behaviour that matters is the argument vector and the process lifecycle, both covered by unit tests; a UI test would only assert that a Form renders its fields.

https://claude.ai/code/session_016cKgYoccE88XFeVPuBLRak

@mintlify

mintlify Bot commented Sep 4, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated
TablePro 🟢 Ready View Preview Sep 4, 2026, 1:10 PM

💡 Tip: Enable Automations to automatically generate PRs for you.

Signed-off-by: Ngô Quốc Đạt <datlechin@gmail.com>
@datlechin
datlechin merged commit 2bdbd43 into main Sep 5, 2026
14 checks passed
@datlechin
datlechin deleted the feat/tunnel-command branch September 5, 2026 03:12
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.

Reach a database through kubectl port-forward, AWS SSM, or a custom tunnel command

1 participant