Skip to content

Commit 98cdbed

Browse files
committed
Ensure Linux terminals always run in the foreground
This is nice for UX (being able to show if they're active), and so that we have a little more control, but is mainly important to ensure we know when they're alive, so we can keep .profile etc populated for their full lifetime.
1 parent d173c26 commit 98cdbed

File tree

1 file changed

+145
-42
lines changed

1 file changed

+145
-42
lines changed

src/interceptors/fresh-terminal.ts

Lines changed: 145 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as fs from 'fs';
33
import * as os from 'os';
44
import * as path from 'path';
55
import * as util from 'util';
6-
import { spawn, ChildProcess, SpawnOptions } from 'child_process';
6+
import { spawn, exec, ChildProcess, SpawnOptions } from 'child_process';
77
import * as GSettings from 'node-gsettings-wrapper';
88
import * as ensureCommandExists from 'command-exists';
99

@@ -39,51 +39,150 @@ interface SpawnArgs {
3939
skipStartupScripts?: true;
4040
}
4141

42+
const execAsync = (command: string): Promise<{ stdout: string, stderr: string }> => {
43+
return new Promise((resolve, reject) => exec(command, (error, stdout, stderr) => {
44+
if (error) reject(error);
45+
else resolve({ stdout, stderr });
46+
}));
47+
};
48+
4249
const getTerminalCommand = _.memoize(async (): Promise<SpawnArgs | null> => {
43-
if (process.platform === 'win32') {
44-
if (await commandExists('git-bash')) {
45-
return { command: 'git-bash' };
46-
} else if (await canAccess(DEFAULT_GIT_BASH_PATH)) {
47-
return { command: DEFAULT_GIT_BASH_PATH };
48-
} else {
49-
return { command: 'start', args: ['cmd'], options: { shell: true }, skipStartupScripts: true };
50-
}
51-
} else if (process.platform === 'linux') {
52-
if (await commandExists('x-terminal-emulator')) return { command: 'x-terminal-emulator' };
53-
54-
if (GSettings.isAvailable()) {
55-
const gSettingsTerminalKey = GSettings.Key.findById(
56-
'org.gnome.desktop.default-applications.terminal', 'exec'
57-
);
58-
59-
const defaultTerminal = gSettingsTerminalKey && gSettingsTerminalKey.getValue();
60-
if (defaultTerminal && await commandExists(defaultTerminal)) {
61-
return { command: defaultTerminal };
62-
}
63-
}
50+
if (process.platform === 'win32') return getWindowsTerminalCommand();
51+
else if (process.platform === 'darwin') return getOSXTerminalCommand();
52+
else if (process.platform === 'linux') return getLinuxTerminalCommand();
53+
else return null;
54+
});
55+
56+
const getWindowsTerminalCommand = async (): Promise<SpawnArgs | null> => {
57+
if (await commandExists('git-bash')) {
58+
return { command: 'git-bash' };
59+
} else if (await canAccess(DEFAULT_GIT_BASH_PATH)) {
60+
return { command: DEFAULT_GIT_BASH_PATH };
61+
}
62+
63+
return { command: 'start', args: ['cmd'], options: { shell: true }, skipStartupScripts: true };
64+
};
65+
66+
const getOSXTerminalCommand = async (): Promise<SpawnArgs | null> => {
67+
const terminalExecutables = (await Promise.all(
68+
[
69+
'co.zeit.hyper',
70+
'com.googlecode.iterm2',
71+
'com.googlecode.iterm',
72+
'com.apple.Terminal'
73+
].map(
74+
(bundleId) => findOsxExecutable(bundleId).catch(() => null)
75+
)
76+
)).filter((executablePath) => !!executablePath);
77+
78+
const bestAvailableTerminal = terminalExecutables[0];
79+
80+
if (bestAvailableTerminal) return { command: bestAvailableTerminal };
81+
else return null;
82+
};
83+
84+
const getLinuxTerminalCommand = async (): Promise<SpawnArgs | null> => {
85+
// Symlink/wrapper that should indicate the system default
86+
if (await commandExists('x-terminal-emulator')) return getXTerminalCommand();
87+
88+
// Check gnome app settings, if available
89+
if (GSettings.isAvailable()) {
90+
const gSettingsTerminalKey = GSettings.Key.findById(
91+
'org.gnome.desktop.default-applications.terminal', 'exec'
92+
);
6493

65-
if (await commandExists('xfce4-terminal')) return { command: 'xfce4-terminal' };
66-
if (await commandExists('xterm')) return { command: 'xterm' };
67-
} else if (process.platform === 'darwin') {
68-
const terminalExecutables = (await Promise.all(
69-
[
70-
'co.zeit.hyper',
71-
'com.googlecode.iterm2',
72-
'com.googlecode.iterm',
73-
'com.apple.Terminal'
74-
].map(
75-
(bundleId) => findOsxExecutable(bundleId).catch(() => null)
76-
)
77-
)).filter((executablePath) => !!executablePath);
78-
79-
const bestAvailableTerminal = terminalExecutables[0];
80-
if (bestAvailableTerminal) {
81-
return { command: bestAvailableTerminal };
94+
const defaultTerminal = gSettingsTerminalKey && gSettingsTerminalKey.getValue();
95+
if (defaultTerminal && await commandExists(defaultTerminal)) {
96+
if (defaultTerminal.includes('gnome-terminal')) return getGnomeTerminalCommand(defaultTerminal);
97+
if (defaultTerminal.includes('konsole')) return getKonsoleTerminalCommand(defaultTerminal);
98+
if (defaultTerminal.includes('xfce4-terminal')) return getXfceTerminalCommand(defaultTerminal);
99+
if (defaultTerminal.includes('x-terminal-emulator')) return getXTerminalCommand(defaultTerminal);
100+
return { command: defaultTerminal };
82101
}
83102
}
84103

104+
// If a specific term like this is installed, it's probably the preferred one
105+
if (await commandExists('konsole')) return getKonsoleTerminalCommand();
106+
if (await commandExists('xfce4-terminal')) return getXfceTerminalCommand();
107+
if (await commandExists('xterm')) return { command: 'xterm' };
108+
85109
return null;
86-
});
110+
}
111+
112+
const getXTerminalCommand = async (command = 'x-terminal-emulator'): Promise<SpawnArgs> => {
113+
// x-terminal-emulator is a wrapper/symlink to the terminal of choice.
114+
// Unfortunately, we need to pass specific args that aren't supported by all terminals (to ensure
115+
// terminals run in the foreground), and the Debian XTE wrapper at least doesn't pass through
116+
// any of the args we want to use. To fix this, we parse --help to try and detect the underlying
117+
// terminal, and run it directly with the args we need.
118+
try {
119+
const { stdout } = await execAsync(`${command} --help`); // debian wrapper ignores --version
120+
const helpOutput = stdout.toLowerCase().replace(/[^\w\d]+/g, ' ');
121+
122+
if (helpOutput.includes('gnome terminal') && await commandExists('gnome-terminal')) {
123+
return getGnomeTerminalCommand();
124+
} else if (helpOutput.includes('xfce4 terminal') && await commandExists('xfce4-terminal')) {
125+
return getXfceTerminalCommand();
126+
} else if (helpOutput.includes('konsole') && await commandExists('konsole')) {
127+
return getKonsoleTerminalCommand();
128+
}
129+
} catch (e) {
130+
reportError(e);
131+
}
132+
133+
// If there's an error, or we just don't recognize the console, give up & run it directly
134+
return { command: 'x-terminal-emulator' };
135+
};
136+
137+
const getKonsoleTerminalCommand = async (command = 'konsole'): Promise<SpawnArgs> => {
138+
let extraArgs: string[] = [];
139+
140+
const { stdout } = await execAsync(`${command} --help`);
141+
142+
// Forces Konsole to run in the foreground, with no separate process
143+
// Seems to be well supported for a long time, but check just in case
144+
if (stdout.includes('--nofork')) {
145+
extraArgs = ['--nofork'];
146+
}
147+
148+
return { command, args: extraArgs };
149+
};
150+
151+
const getGnomeTerminalCommand = async (command = 'gnome-terminal'): Promise<SpawnArgs> => {
152+
let extraArgs: string[] = [];
153+
154+
const { stdout } = await execAsync(`${command} --help-all`);
155+
156+
// Officially supported option, but only supported in v3.28+
157+
if (stdout.includes('--wait')) {
158+
extraArgs = ['--wait'];
159+
} else {
160+
// Debugging option - works back to v3.7 (2012), but not officially supported
161+
// Documented at https://wiki.gnome.org/Apps/Terminal/Debugging
162+
const randomId = Math.round((Math.random() * 100000));
163+
extraArgs = ['--app-id', `com.httptoolkit.${randomId}`];
164+
}
165+
166+
// We're assuming here that nobody is developing in a pre-2012 un-updated gnome-terminal.
167+
// If they are then gnome-terminal is not going to recognize --app-id, and will fail to
168+
// start. Hard to avoid, rare case, so c'est la vie.
169+
170+
return { command, args: extraArgs };
171+
};
172+
173+
const getXfceTerminalCommand = async (command = 'xfce4-terminal'): Promise<SpawnArgs> => {
174+
let extraArgs: string[] = [];
175+
176+
const { stdout } = await execAsync(`${command} --help`);
177+
178+
// Disables the XFCE terminal server for this terminal, so it runs in the foreground.
179+
// Seems to be well supported for a long time, but check just in case
180+
if (stdout.includes('--disable-server')) {
181+
extraArgs = ['--disable-server'];
182+
}
183+
184+
return { command, args: extraArgs };
185+
};
87186

88187
const appendOrCreateFile = util.promisify(fs.appendFile);
89188
const appendToFirstExisting = async (paths: string[], forceWrite: boolean, contents: string) => {
@@ -105,8 +204,10 @@ const END_CONFIG_SECTION = '# --httptoolkit-end--';
105204

106205
// Works in bash, zsh, dash, ksh, sh (not fish)
107206
const SH_SHELL_PATH_CONFIG = `
108-
${START_CONFIG_SECTION} This section will be removed shortly
207+
${START_CONFIG_SECTION}
208+
# This section will be reset each time a HTTP Toolkit terminal is opened
109209
if [ -n "$HTTP_TOOLKIT_ACTIVE" ]; then
210+
# When HTTP Toolkit is active, we inject various overrides into PATH
110211
export PATH="${POSIX_OVERRIDE_BIN_PATH}:$PATH"
111212
112213
if command -v winpty >/dev/null 2>&1; then
@@ -116,8 +217,10 @@ if [ -n "$HTTP_TOOLKIT_ACTIVE" ]; then
116217
fi
117218
${END_CONFIG_SECTION}`;
118219
const FISH_SHELL_PATH_CONFIG = `
119-
${START_CONFIG_SECTION} This section will be removed shortly
220+
${START_CONFIG_SECTION}
221+
# This section will be reset each time a HTTP Toolkit terminal is opened
120222
if [ -n "$HTTP_TOOLKIT_ACTIVE" ]
223+
# When HTTP Toolkit is active, we inject various overrides into PATH
121224
set -x PATH "${POSIX_OVERRIDE_BIN_PATH}" $PATH;
122225
123226
if command -v winpty >/dev/null 2>&1

0 commit comments

Comments
 (0)