Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 45 additions & 2 deletions apps/desktop/src/main/terminal/tmux.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ import {
isDescendantOf,
parseFormatLines,
pollRun,
resolveAttachment,
runPaneState,
startRun,
stopRun,
type TmuxRunHandle,
} from '@/main/terminal/tmux'

/** The separator the format strings use; no tmux field can contain it. */
const F = '\u001f'
/** The separator the format strings use. */
const F = '|~sim~|'

describe('parseFormatLines', () => {
it('drops lines with the wrong field count rather than mis-assigning them', () => {
Expand Down Expand Up @@ -104,6 +105,8 @@ interface FakeTmuxState {
log: string[]
/** Commands the fake fails, with the error tmux would print. */
fail?: Record<string, string>
/** Attached clients, as `list-clients` reports them. */
clients?: Array<{ pid: string; tty: string; session: string }>
}

const FAKE_TMUX = `
Expand All @@ -114,6 +117,12 @@ const args = process.argv.slice(2)
const save = () => fs.writeFileSync(file, JSON.stringify(state))
const target = () => args[args.indexOf('-t') + 1]
const fail = (message) => { process.stderr.write(message); process.exit(1) }
// Prints a format's output as tmux 3.4 and 3.5 do: a backslash doubled, and every other control
// character as its octal escape, so a control-character separator would arrive as text.
const escaped = (text) =>
text
.replace(/\\\\/g, '\\\\\\\\')
.replace(/[\\x00-\\x1f]/g, (c) => '\\\\' + c.charCodeAt(0).toString(8).padStart(3, '0'))
if (state.fail && state.fail[args[0]]) fail(state.fail[args[0]])
switch (args[0]) {
case 'new-window': {
Expand Down Expand Up @@ -144,6 +153,17 @@ switch (args[0]) {
process.stdout.write((pane.options[name] ?? '') + '\\n')
break
}
case 'list-clients': {
const format = args[args.indexOf('-F') + 1]
for (const client of state.clients ?? []) {
const line = format
.replace('#{client_pid}', client.pid)
.replace('#{client_tty}', client.tty)
.replace('#{client_session}', client.session)
process.stdout.write(escaped(line) + '\\n')
}
break
}
case 'send-keys':
case 'kill-pane': {
if (!state.panes[target()]) fail("can't find pane")
Expand Down Expand Up @@ -191,6 +211,29 @@ function fakeTmux(options: { exec?: boolean } = {}) {
}
}

describe('finding the tmux session a shell runs', () => {
const dirs: string[] = []

afterEach(() => {
for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true })
})

it('reads the clients tmux 3.4 and 3.5 list, which escape control characters', async () => {
const tmux = fakeTmux()
dirs.push(tmux.dir)
// A client of this very process: its parent stands in for the shell the client runs in.
tmux.write({
...tmux.read(),
clients: [{ pid: String(process.pid), tty: '/dev/pts/3', session: 'work' }],
})

expect(await resolveAttachment(process.ppid, tmux.env)).toEqual({
session: 'work',
clientTty: '/dev/pts/3',
})
})
})

describe('stopping a tmux run touches only its own pane', () => {
const dirs: string[] = []

Expand Down
11 changes: 8 additions & 3 deletions apps/desktop/src/main/terminal/tmux.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,13 @@ const TMUX_TIMEOUT_MS = 5_000
/** How often the status file is checked while a tmux-run command is going. */
const RUN_POLL_INTERVAL_MS = 250

/** Field separator for `-F` output. Chosen because no tmux field contains it. */
const FIELD = '\u001f'
/**
* Field separator for `-F` output. Printable on purpose: tmux 3.4 and 3.5 print a control
* character as its octal escape, so a control-character separator arrived as the text `\037` and
* no line split. No tmux escapes these characters, and a field that happened to contain the
* separator would change the line's field count, so that line is dropped rather than misread.
*/
const FIELD = '|~sim~|'

export interface TmuxCommandResult {
ok: boolean
Expand Down Expand Up @@ -125,7 +130,7 @@ export function runTmux(args: string[], env: NodeJS.ProcessEnv): Promise<TmuxCom
/**
* Parses `list-clients`/`list-panes` output into records.
*
* Split on a control character rather than whitespace: window names and
* Split on a dedicated separator rather than whitespace: window names and
* working directories contain spaces, and a path with a space would otherwise
* shift every later field by one.
*/
Expand Down
Loading