Skip to content

Commit c2090c9

Browse files
authored
Enable ConPTY by default in terminals on Windows (#44468) (#44743)
* Add `terminal.windowsUseConpty` config option * Calculate `windowsPty` options * Pass `useConpty` to node-pty * Pass `windowsPty` to xterm * Fix test * Replace `terminal.windowsUseConpty` with `terminal.windowsBackend`, pass boolean all the way through * Wait for pty processes to exit before closing the app * Simplify `Array.from` * `GRACEFUL_KILL_MESSAGE` -> `TERMINATE_MESSAGE` * Adjust callsites to async `dispose` * Add `winpty` to ignored words in `cspell.json` (cherry picked from commit 981aed6)
1 parent 78389d4 commit c2090c9

28 files changed

+311
-37
lines changed

docs/cspell.json

+1
Original file line numberDiff line numberDiff line change
@@ -951,6 +951,7 @@
951951
"winadj",
952952
"windowsaccountname",
953953
"windowsdesktop",
954+
"winpty",
954955
"winscp",
955956
"winserver",
956957
"workgroups",

docs/pages/connect-your-client/teleport-connect.mdx

+2
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,7 @@ Below is the list of the supported config properties.
419419
| `theme` | `system` | Color theme for the app. Available modes: `light`, `dark`, `system`. |
420420
| `terminal.fontFamily` | `Menlo, Monaco, monospace` on macOS<br/>`Consolas, monospace` on Windows<br/>`'Droid Sans Mono', monospace` on Linux | Font family for the terminal. |
421421
| `terminal.fontSize` | 15 | Font size for the terminal. |
422+
| `terminal.windowsBackend` | `auto` | `auto` uses modern [ConPTY](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/) system if available, which requires Windows 10 (19H1) or above. Set to `winpty` to use winpty even if ConPTY is available. |
422423
| `usageReporting.enabled` | `false` | Enables collecting anonymous usage data (see [Telemetry](#telemetry)). |
423424
| `keymap.tab1` - `keymap.tab9` | `Command+1` - `Command+9` on macOS <br/> `Ctrl+1` - `Ctrl+9` on Windows<br/>`Alt+1` - `Alt+9` on Linux | Shortcut to open tab 1–9. |
424425
| `keymap.closeTab` | `Command+W` on macOS<br/>`Ctrl+W` on Windows/Linux | Shortcut to close a tab. |
@@ -431,6 +432,7 @@ Below is the list of the supported config properties.
431432
| `keymap.openProfiles` | `Command+I` on macOS<br/>`Ctrl+I` on Windows/Linux | Shortcut to open the profile selector. |
432433
| `keymap.openSearchBar` | `Command+K` on macOS<br/>`Ctrl+K` on Windows/Linux | Shortcut to open the search bar. |
433434
| `headless.skipConfirm` | false | Skips the confirmation prompt for Headless WebAuthn approval and instead prompts for WebAuthn immediately. |
435+
| `ssh.noResume` | false | Disables SSH connection resumption. |
434436

435437
<Admonition
436438
type="note"

web/packages/teleterm/src/mainProcess/mainProcess.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import {
4444
ChildProcessAddresses,
4545
MainProcessIpc,
4646
RendererIpc,
47+
TERMINATE_MESSAGE,
4748
} from 'teleterm/mainProcess/types';
4849
import { getAssetPath } from 'teleterm/mainProcess/runtimeSettings';
4950
import { RootClusterUri } from 'teleterm/ui/uri';
@@ -141,7 +142,11 @@ export default class MainProcess {
141142
terminateWithTimeout(this.tshdProcess, 10_000, () => {
142143
this.gracefullyKillTshdProcess();
143144
}),
144-
terminateWithTimeout(this.sharedProcess),
145+
terminateWithTimeout(this.sharedProcess, 5_000, process =>
146+
// process.kill doesn't allow running a cleanup code in the child process
147+
// on Windows
148+
process.send(TERMINATE_MESSAGE)
149+
),
145150
this.agentRunner.killAll(),
146151
]);
147152
}

web/packages/teleterm/src/mainProcess/types.ts

+9
Original file line numberDiff line numberDiff line change
@@ -277,3 +277,12 @@ export enum MainProcessIpc {
277277
export enum WindowsManagerIpc {
278278
SignalUserInterfaceReadiness = 'windows-manager-signal-user-interface-readiness',
279279
}
280+
281+
/**
282+
* A custom message to gracefully quit a process.
283+
* It is sent to the child process with `process.send`.
284+
*
285+
* We need this because `process.kill('SIGTERM')` doesn't work on Windows,
286+
* so we couldn't run any cleanup logic.
287+
*/
288+
export const TERMINATE_MESSAGE = 'TERMINATE_MESSAGE';

web/packages/teleterm/src/preload.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,14 @@ async function getElectronGlobals(): Promise<ElectronGlobals> {
7373
credentials.shared,
7474
runtimeSettings,
7575
{
76-
noResume: mainProcessClient.configService.get('ssh.noResume').value,
76+
ssh: {
77+
noResume: mainProcessClient.configService.get('ssh.noResume').value,
78+
},
79+
terminal: {
80+
windowsBackend: mainProcessClient.configService.get(
81+
'terminal.windowsBackend'
82+
).value,
83+
},
7784
}
7885
);
7986
const {

web/packages/teleterm/src/services/config/appConfigSchema.ts

+9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ import { Platform } from 'teleterm/mainProcess/types';
2222

2323
import { createKeyboardShortcutSchema } from './keyboardShortcutSchema';
2424

25+
// When adding a new config property, add it to the docs too
26+
// (teleport-connect.mdx#configuration).
27+
2528
export type AppConfigSchema = ReturnType<typeof createAppConfigSchema>;
2629
export type AppConfig = z.infer<AppConfigSchema>;
2730

@@ -54,6 +57,12 @@ export const createAppConfigSchema = (platform: Platform) => {
5457
.max(256)
5558
.default(15)
5659
.describe('Font size for the terminal.'),
60+
'terminal.windowsBackend': z
61+
.enum(['auto', 'winpty'])
62+
.default('auto')
63+
.describe(
64+
'`auto` uses modern ConPTY system if available, which requires Windows 10 (19H1) or above. Set to `winpty` to use winpty even if ConPTY is available.'
65+
),
5766
'usageReporting.enabled': z
5867
.boolean()
5968
.default(false)

web/packages/teleterm/src/services/pty/fixtures/mocks.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { IPtyProcess } from 'teleterm/sharedProcess/ptyHost';
2020
import {
2121
PtyProcessCreationStatus,
2222
PtyServiceClient,
23+
WindowsPty,
2324
} from 'teleterm/services/pty';
2425

2526
export class MockPtyProcess implements IPtyProcess {
@@ -29,7 +30,7 @@ export class MockPtyProcess implements IPtyProcess {
2930

3031
resize() {}
3132

32-
dispose() {}
33+
async dispose() {}
3334

3435
onData() {
3536
return () => {};
@@ -64,10 +65,12 @@ export class MockPtyServiceClient implements PtyServiceClient {
6465
createPtyProcess(): Promise<{
6566
process: IPtyProcess;
6667
creationStatus: PtyProcessCreationStatus;
68+
windowsPty: WindowsPty;
6769
}> {
6870
return Promise.resolve({
6971
process: new MockPtyProcess(),
7072
creationStatus: PtyProcessCreationStatus.Ok,
73+
windowsPty: undefined,
7174
});
7275
}
7376
}

web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ describe('getPtyProcessOptions', () => {
4747

4848
const { env } = getPtyProcessOptions(
4949
makeRuntimeSettings(),
50-
{ noResume: false },
50+
{ ssh: { noResume: false }, windowsPty: { useConpty: true } },
5151
cmd,
5252
processEnv
5353
);
@@ -76,7 +76,7 @@ describe('getPtyProcessOptions', () => {
7676

7777
const { env } = getPtyProcessOptions(
7878
makeRuntimeSettings(),
79-
{ noResume: false },
79+
{ ssh: { noResume: false }, windowsPty: { useConpty: true } },
8080
cmd,
8181
processEnv
8282
);
@@ -103,7 +103,7 @@ describe('getPtyProcessOptions', () => {
103103

104104
const { args } = getPtyProcessOptions(
105105
makeRuntimeSettings(),
106-
{ noResume: true },
106+
{ ssh: { noResume: true }, windowsPty: { useConpty: true } },
107107
cmd,
108108
processEnv
109109
);

web/packages/teleterm/src/services/pty/ptyHost/buildPtyOptions.ts

+17-5
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,24 @@ import { assertUnreachable } from 'teleterm/ui/utils';
2525
import {
2626
PtyCommand,
2727
PtyProcessCreationStatus,
28-
SshOptions,
2928
TshKubeLoginCommand,
29+
SshOptions,
30+
WindowsPty,
3031
} from '../types';
3132

3233
import {
3334
resolveShellEnvCached,
3435
ResolveShellEnvTimeoutError,
3536
} from './resolveShellEnv';
3637

38+
type PtyOptions = {
39+
ssh: SshOptions;
40+
windowsPty: Pick<WindowsPty, 'useConpty'>;
41+
};
42+
3743
export async function buildPtyOptions(
3844
settings: RuntimeSettings,
39-
sshOptions: SshOptions,
45+
options: PtyOptions,
4046
cmd: PtyCommand
4147
): Promise<{
4248
processOptions: PtyProcessOptions;
@@ -68,7 +74,7 @@ export async function buildPtyOptions(
6874
return {
6975
processOptions: getPtyProcessOptions(
7076
settings,
71-
sshOptions,
77+
options,
7278
cmd,
7379
combinedEnv
7480
),
@@ -79,10 +85,12 @@ export async function buildPtyOptions(
7985

8086
export function getPtyProcessOptions(
8187
settings: RuntimeSettings,
82-
sshOptions: SshOptions,
88+
options: PtyOptions,
8389
cmd: PtyCommand,
8490
env: typeof process.env
8591
): PtyProcessOptions {
92+
const useConpty = options.windowsPty?.useConpty;
93+
8694
switch (cmd.kind) {
8795
case 'pty.shell': {
8896
// Teleport Connect bundles a tsh binary, but the user might have one already on their system.
@@ -104,6 +112,7 @@ export function getPtyProcessOptions(
104112
cwd: cmd.cwd,
105113
env: { ...env, ...cmd.env },
106114
initMessage: cmd.initMessage,
115+
useConpty,
107116
};
108117
}
109118

@@ -129,6 +138,7 @@ export function getPtyProcessOptions(
129138
path: settings.defaultShell,
130139
args: isWindows ? powershellCommandArgs : bashCommandArgs,
131140
env: { ...env, KUBECONFIG: getKubeConfigFilePath(cmd, settings) },
141+
useConpty,
132142
};
133143
}
134144

@@ -140,7 +150,7 @@ export function getPtyProcessOptions(
140150
const args = [
141151
`--proxy=${cmd.rootClusterId}`,
142152
'ssh',
143-
...(sshOptions.noResume ? ['--no-resume'] : []),
153+
...(options.ssh.noResume ? ['--no-resume'] : []),
144154
'--forward-agent',
145155
loginHost,
146156
];
@@ -149,6 +159,7 @@ export function getPtyProcessOptions(
149159
path: settings.tshd.binaryPath,
150160
args,
151161
env,
162+
useConpty,
152163
};
153164
}
154165

@@ -159,6 +170,7 @@ export function getPtyProcessOptions(
159170
path: cmd.path,
160171
args: cmd.args,
161172
env: { ...env, ...cmd.env },
173+
useConpty,
162174
};
163175
}
164176

web/packages/teleterm/src/services/pty/ptyHost/ptyHostClient.ts

+1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export function createPtyHostClient(
4141
args: ptyOptions.args,
4242
path: ptyOptions.path,
4343
env: Struct.fromJson(ptyOptions.env),
44+
useConpty: ptyOptions.useConpty,
4445
});
4546

4647
if (ptyOptions.cwd) {

web/packages/teleterm/src/services/pty/ptyHost/ptyProcess.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export function createPtyProcess(
4747
exchangeEventsStream.resize(columns, rows);
4848
},
4949

50-
dispose(): void {
50+
async dispose(): Promise<void> {
5151
exchangeEventsStream.dispose();
5252
},
5353

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Teleport
3+
* Copyright (C) 2024 Gravitational, Inc.
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
import { makeRuntimeSettings } from 'teleterm/mainProcess/fixtures/mocks';
20+
21+
import { getWindowsPty } from './windowsPty';
22+
23+
test.each([
24+
{
25+
name: 'uses conpty on supported Windows version',
26+
platform: 'win32' as const,
27+
osVersion: '10.0.22621',
28+
terminalOptions: { windowsBackend: 'auto' as const },
29+
expected: { useConpty: true, buildNumber: 22621 },
30+
},
31+
{
32+
name: 'uses winpty on unsupported Windows version',
33+
platform: 'win32' as const,
34+
osVersion: '10.0.18308',
35+
terminalOptions: { windowsBackend: 'auto' as const },
36+
expected: { useConpty: false, buildNumber: 18308 },
37+
},
38+
{
39+
name: 'uses winpty when Windows version is supported, but conpty is disabled in options',
40+
platform: 'win32' as const,
41+
osVersion: '10.0.22621',
42+
terminalOptions: { windowsBackend: 'winpty' as const },
43+
expected: { useConpty: false, buildNumber: 22621 },
44+
},
45+
{
46+
name: 'undefined on non-Windows OS',
47+
platform: 'darwin' as const,
48+
osVersion: '23.5.0',
49+
terminalOptions: { windowsBackend: 'auto' as const },
50+
expected: undefined,
51+
},
52+
])('$name', ({ platform, osVersion, terminalOptions, expected }) => {
53+
const pty = getWindowsPty(
54+
makeRuntimeSettings({
55+
platform,
56+
osVersion,
57+
}),
58+
terminalOptions
59+
);
60+
expect(pty).toEqual(expected);
61+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/**
2+
* Teleport
3+
* Copyright (C) 2024 Gravitational, Inc.
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
import { RuntimeSettings } from 'teleterm/mainProcess/types';
20+
21+
import { TerminalOptions, WindowsPty } from '../types';
22+
23+
export const WIN_BUILD_STABLE_CONPTY = 18309;
24+
25+
export function getWindowsPty(
26+
runtimeSettings: RuntimeSettings,
27+
terminalOptions: TerminalOptions
28+
): WindowsPty {
29+
if (runtimeSettings.platform !== 'win32') {
30+
return undefined;
31+
}
32+
33+
const buildNumber = getWindowsBuildNumber(runtimeSettings.osVersion);
34+
const useConpty =
35+
terminalOptions.windowsBackend === 'auto' &&
36+
buildNumber >= WIN_BUILD_STABLE_CONPTY;
37+
return {
38+
useConpty,
39+
buildNumber,
40+
};
41+
}
42+
43+
function getWindowsBuildNumber(osVersion: string): number {
44+
const parsedOsVersion = /(\d+)\.(\d+)\.(\d+)/g.exec(osVersion);
45+
if (parsedOsVersion?.length === 4) {
46+
return parseInt(parsedOsVersion[3]);
47+
}
48+
return 0;
49+
}

0 commit comments

Comments
 (0)