Skip to content

Commit 86e2f81

Browse files
committed
Refactor terminal script handling into its own file
1 parent 53ac130 commit 86e2f81

File tree

2 files changed

+175
-164
lines changed

2 files changed

+175
-164
lines changed

src/interceptors/terminal/fresh-terminal-interceptor.ts

Lines changed: 2 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
import * as _ from 'lodash';
22
import * as fs from 'fs';
3-
import * as os from 'os';
4-
import * as path from 'path';
53
import * as util from 'util';
64
import { spawn, ChildProcess, SpawnOptions } from 'child_process';
75
import * as GSettings from 'node-gsettings-wrapper';
@@ -13,20 +11,15 @@ const findOsxExecutable = util.promisify(findOsxExecutableCb);
1311
import { Interceptor } from '..';
1412
import { HtkConfig } from '../../config';
1513
import { reportError, addBreadcrumb } from '../../error-tracking';
16-
import { OVERRIDE_BIN_PATH, getTerminalEnvVars } from './terminal-env-overrides';
14+
import { getTerminalEnvVars } from './terminal-env-overrides';
15+
import { editShellStartupScripts, resetShellStartupScripts } from './terminal-scripts';
1716

1817
const checkAccess = util.promisify(fs.access);
1918
const canAccess = (path: string) => checkAccess(path).then(() => true).catch(() => false);
2019

2120
const commandExists = (path: string): Promise<boolean> => ensureCommandExists(path).then(() => true).catch(() => false);
2221

2322
const DEFAULT_GIT_BASH_PATH = 'C:/Program Files/git/git-bash.exe';
24-
const SHELL = (process.env.SHELL || '').split('/').slice(-1)[0];
25-
26-
// Generate POSIX paths for git-bash on Windows (or use the normal path everywhere where)
27-
const POSIX_OVERRIDE_BIN_PATH = process.platform === 'win32'
28-
? OVERRIDE_BIN_PATH.replace(/\\/g, '/').replace(/^(\w+):/, (_all, driveLetter) => `/${driveLetter.toLowerCase()}`)
29-
: OVERRIDE_BIN_PATH;
3023

3124
interface SpawnArgs {
3225
command: string;
@@ -219,161 +212,6 @@ const getXfceTerminalCommand = async (command = 'xfce4-terminal'): Promise<Spawn
219212
return { command, args: extraArgs };
220213
};
221214

222-
const appendOrCreateFile = util.promisify(fs.appendFile);
223-
const appendToFirstExisting = async (paths: string[], forceWrite: boolean, contents: string) => {
224-
for (let path of paths) {
225-
// Small race here, but end result is ok either way
226-
if (await canAccess(path)) {
227-
return appendOrCreateFile(path, contents);
228-
}
229-
}
230-
231-
if (forceWrite) {
232-
// If force write is set, write the last file anyway
233-
return appendOrCreateFile(paths.slice(-1)[0], contents);
234-
}
235-
};
236-
237-
const START_CONFIG_SECTION = '# --httptoolkit--';
238-
const END_CONFIG_SECTION = '# --httptoolkit-end--';
239-
240-
// The shell config required to ensure every spawned shell always has the right
241-
// configuration, even if it has its env vars reset somehow. This also includes
242-
// fixes for winpty with git-bash. By default, winpty will intercept known
243-
// commands to manage them, so our PATH overrides never get run. We avoid that
244-
// with trivial aliases, and then run winpty ourselves inside the overrides.
245-
246-
// Works in bash, zsh, dash, ksh, sh (not fish)
247-
const SH_SHELL_PATH_CONFIG = `
248-
${START_CONFIG_SECTION}
249-
# This section will be reset each time a HTTP Toolkit terminal is opened
250-
if [ -n "$HTTP_TOOLKIT_ACTIVE" ]; then
251-
# When HTTP Toolkit is active, we inject various overrides into PATH
252-
export PATH="${POSIX_OVERRIDE_BIN_PATH}:$PATH"
253-
254-
if command -v winpty >/dev/null 2>&1; then
255-
# Work around for winpty's hijacking of certain commands
256-
alias php=php
257-
alias node=node
258-
fi
259-
fi
260-
${END_CONFIG_SECTION}`;
261-
const FISH_SHELL_PATH_CONFIG = `
262-
${START_CONFIG_SECTION}
263-
# This section will be reset each time a HTTP Toolkit terminal is opened
264-
if [ -n "$HTTP_TOOLKIT_ACTIVE" ]
265-
# When HTTP Toolkit is active, we inject various overrides into PATH
266-
set -x PATH "${POSIX_OVERRIDE_BIN_PATH}" $PATH;
267-
268-
if command -v winpty >/dev/null 2>&1
269-
# Work around for winpty's hijacking of certain commands
270-
alias php=php
271-
alias node=node
272-
end
273-
end
274-
${END_CONFIG_SECTION}`;
275-
276-
// Find the relevant user shell config file, add the above line to it, so that
277-
// shells launched with HTTP_TOOLKIT_ACTIVE set use the interception PATH.
278-
const editShellStartupScripts = async () => {
279-
await resetShellStartupScripts();
280-
281-
// The key risk here is that one of these scripts (or some other process) will be
282-
// overriding PATH itself, so we need to append some PATH reset logic. The main
283-
// offenders are: nvm config's in .bashrc/.bash_profile, OSX's path_helper and
284-
// git-bash ignoring the inherited $PATH.
285-
286-
// .profile is used by Dash, Bash sometimes, and by Sh:
287-
appendOrCreateFile(path.join(os.homedir(), '.profile'), SH_SHELL_PATH_CONFIG)
288-
.catch(reportError);
289-
290-
// Bash login shells use some other files by preference, if they exist.
291-
// Note that on OSX, all shells are login - elsewhere they only are at actual login time.
292-
appendToFirstExisting(
293-
[
294-
path.join(os.homedir(), '.bash_profile'),
295-
path.join(os.homedir(), '.bash_login')
296-
],
297-
false, // Do nothing if they don't exist - it falls back to .profile
298-
SH_SHELL_PATH_CONFIG
299-
).catch(reportError);
300-
301-
// Bash non-login shells use .bashrc, if it exists:
302-
appendToFirstExisting(
303-
[
304-
path.join(os.homedir(), '.bashrc')
305-
],
306-
SHELL === 'bash', // If you use bash, we _always_ want to set this
307-
SH_SHELL_PATH_CONFIG
308-
).catch(reportError);
309-
310-
// Zsh has its own files (both are actually used)
311-
appendToFirstExisting(
312-
[
313-
path.join(os.homedir(), '.zshenv'),
314-
path.join(os.homedir(), '.zshrc')
315-
],
316-
SHELL === 'zsh', // If you use zsh, we _always_ write a config file
317-
SH_SHELL_PATH_CONFIG
318-
).catch(reportError);
319-
320-
// Fish always uses the same config file
321-
appendToFirstExisting(
322-
[
323-
path.join(os.homedir(), '.config', 'fish', 'config.fish'),
324-
],
325-
SHELL === 'fish' || await canAccess(path.join(os.homedir(), '.config', 'fish')),
326-
FISH_SHELL_PATH_CONFIG
327-
).catch(reportError);
328-
};
329-
330-
const readFile = util.promisify(fs.readFile);
331-
const writeFile = util.promisify(fs.writeFile);
332-
const renameFile = util.promisify(fs.rename);
333-
const removeConfigSectionsFromFile = async (path: string) => {
334-
let fileLines: string[];
335-
336-
try {
337-
fileLines = (await readFile(path, 'utf8')).split('\n');
338-
} catch (e) {
339-
// Silently skip any files we can't read
340-
return;
341-
}
342-
343-
// Remove everything between each pair of start/end section markers
344-
let sectionStart = _.findIndex(fileLines, (l) => l.startsWith(START_CONFIG_SECTION));
345-
while (sectionStart !== -1) {
346-
let sectionEnd = _.findIndex(fileLines, (l) => l.startsWith(END_CONFIG_SECTION));
347-
348-
if (sectionEnd === -1 || sectionEnd <= sectionStart) return; // Odd config file state - don't edit it
349-
fileLines.splice(sectionStart, (sectionEnd - sectionStart) + 1);
350-
sectionStart = _.findIndex(fileLines, (l) => l.startsWith(START_CONFIG_SECTION));
351-
}
352-
353-
// Write & rename to ensure this is atomic, and avoid races here
354-
// as much as we reasonably can.
355-
const tempFile = path + Date.now() + '.temp';
356-
await writeFile(tempFile, fileLines.join('\n'));
357-
return renameFile(tempFile, path);
358-
};
359-
360-
// Cleanup: strip our extra config line from all config files
361-
// Good to do for tidiness, not strictly necessary (the config does nothing
362-
// unless HTTP_TOOLKIT_ACTIVE is set anyway).
363-
const resetShellStartupScripts = () => {
364-
// For each possible config file, remove our magic line, if present
365-
return Promise.all([
366-
path.join(os.homedir(), '.profile'),
367-
path.join(os.homedir(), '.bash_profile'),
368-
path.join(os.homedir(), '.bash_login'),
369-
path.join(os.homedir(), '.zshenv'),
370-
path.join(os.homedir(), '.zshrc'),
371-
path.join(os.homedir(), '.config', 'fish', 'config.fish'),
372-
].map((configFile) =>
373-
removeConfigSectionsFromFile(configFile).catch(reportError)
374-
));
375-
};
376-
377215
const terminals: _.Dictionary<ChildProcess[] | undefined> = {}
378216

379217
export class TerminalInterceptor implements Interceptor {
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
import * as _ from 'lodash';
2+
import * as fs from 'fs';
3+
import * as util from 'util';
4+
import * as os from 'os';
5+
import * as path from 'path';
6+
7+
import { reportError } from '../../error-tracking';
8+
import { OVERRIDE_BIN_PATH } from './terminal-env-overrides';
9+
10+
const checkAccess = util.promisify(fs.access);
11+
const canAccess = (path: string) => checkAccess(path).then(() => true).catch(() => false);
12+
13+
// Generate POSIX paths for git-bash on Windows (or use the normal path everywhere else)
14+
const POSIX_OVERRIDE_BIN_PATH = process.platform === 'win32'
15+
? OVERRIDE_BIN_PATH.replace(/\\/g, '/').replace(/^(\w+):/, (_all, driveLetter) => `/${driveLetter.toLowerCase()}`)
16+
: OVERRIDE_BIN_PATH;
17+
18+
const SHELL = (process.env.SHELL || '').split('/').slice(-1)[0];
19+
20+
const appendOrCreateFile = util.promisify(fs.appendFile);
21+
const appendToFirstExisting = async (paths: string[], forceWrite: boolean, contents: string) => {
22+
for (let path of paths) {
23+
// Small race here, but end result is ok either way
24+
if (await canAccess(path)) {
25+
return appendOrCreateFile(path, contents);
26+
}
27+
}
28+
29+
if (forceWrite) {
30+
// If force write is set, write the last file anyway
31+
return appendOrCreateFile(paths.slice(-1)[0], contents);
32+
}
33+
};
34+
35+
const START_CONFIG_SECTION = '# --httptoolkit--';
36+
const END_CONFIG_SECTION = '# --httptoolkit-end--';
37+
38+
// The shell config required to ensure every spawned shell always has the right
39+
// configuration, even if it has its env vars reset somehow. This also includes
40+
// fixes for winpty with git-bash. By default, winpty will intercept known
41+
// commands to manage them, so our PATH overrides never get run. We avoid that
42+
// with trivial aliases, and then run winpty ourselves inside the overrides.
43+
44+
// Works in bash, zsh, dash, ksh, sh (not fish)
45+
const SH_SHELL_PATH_CONFIG = `
46+
${START_CONFIG_SECTION}
47+
# This section will be reset each time a HTTP Toolkit terminal is opened
48+
if [ -n "$HTTP_TOOLKIT_ACTIVE" ]; then
49+
# When HTTP Toolkit is active, we inject various overrides into PATH
50+
export PATH="${POSIX_OVERRIDE_BIN_PATH}:$PATH"
51+
52+
if command -v winpty >/dev/null 2>&1; then
53+
# Work around for winpty's hijacking of certain commands
54+
alias php=php
55+
alias node=node
56+
fi
57+
fi
58+
${END_CONFIG_SECTION}`;
59+
const FISH_SHELL_PATH_CONFIG = `
60+
${START_CONFIG_SECTION}
61+
# This section will be reset each time a HTTP Toolkit terminal is opened
62+
if [ -n "$HTTP_TOOLKIT_ACTIVE" ]
63+
# When HTTP Toolkit is active, we inject various overrides into PATH
64+
set -x PATH "${POSIX_OVERRIDE_BIN_PATH}" $PATH;
65+
66+
if command -v winpty >/dev/null 2>&1
67+
# Work around for winpty's hijacking of certain commands
68+
alias php=php
69+
alias node=node
70+
end
71+
end
72+
${END_CONFIG_SECTION}`;
73+
74+
// Find the relevant user shell config file, add the above line to it, so that
75+
// shells launched with HTTP_TOOLKIT_ACTIVE set use the interception PATH.
76+
export const editShellStartupScripts = async () => {
77+
await resetShellStartupScripts();
78+
79+
// The key risk here is that one of these scripts (or some other process) will be
80+
// overriding PATH itself, so we need to append some PATH reset logic. The main
81+
// offenders are: nvm config's in .bashrc/.bash_profile, OSX's path_helper and
82+
// git-bash ignoring the inherited $PATH.
83+
84+
// .profile is used by Dash, Bash sometimes, and by Sh:
85+
appendOrCreateFile(path.join(os.homedir(), '.profile'), SH_SHELL_PATH_CONFIG)
86+
.catch(reportError);
87+
88+
// Bash login shells use some other files by preference, if they exist.
89+
// Note that on OSX, all shells are login - elsewhere they only are at actual login time.
90+
appendToFirstExisting(
91+
[
92+
path.join(os.homedir(), '.bash_profile'),
93+
path.join(os.homedir(), '.bash_login')
94+
],
95+
false, // Do nothing if they don't exist - it falls back to .profile
96+
SH_SHELL_PATH_CONFIG
97+
).catch(reportError);
98+
99+
// Bash non-login shells use .bashrc, if it exists:
100+
appendToFirstExisting(
101+
[
102+
path.join(os.homedir(), '.bashrc')
103+
],
104+
SHELL === 'bash', // If you use bash, we _always_ want to set this
105+
SH_SHELL_PATH_CONFIG
106+
).catch(reportError);
107+
108+
// Zsh has its own files (both are actually used)
109+
appendToFirstExisting(
110+
[
111+
path.join(os.homedir(), '.zshenv'),
112+
path.join(os.homedir(), '.zshrc')
113+
],
114+
SHELL === 'zsh', // If you use zsh, we _always_ write a config file
115+
SH_SHELL_PATH_CONFIG
116+
).catch(reportError);
117+
118+
// Fish always uses the same config file
119+
appendToFirstExisting(
120+
[
121+
path.join(os.homedir(), '.config', 'fish', 'config.fish'),
122+
],
123+
SHELL === 'fish' || await canAccess(path.join(os.homedir(), '.config', 'fish')),
124+
FISH_SHELL_PATH_CONFIG
125+
).catch(reportError);
126+
};
127+
128+
const readFile = util.promisify(fs.readFile);
129+
const writeFile = util.promisify(fs.writeFile);
130+
const renameFile = util.promisify(fs.rename);
131+
const removeConfigSectionsFromFile = async (path: string) => {
132+
let fileLines: string[];
133+
134+
try {
135+
fileLines = (await readFile(path, 'utf8')).split('\n');
136+
} catch (e) {
137+
// Silently skip any files we can't read
138+
return;
139+
}
140+
141+
// Remove everything between each pair of start/end section markers
142+
let sectionStart = _.findIndex(fileLines, (l) => l.startsWith(START_CONFIG_SECTION));
143+
while (sectionStart !== -1) {
144+
let sectionEnd = _.findIndex(fileLines, (l) => l.startsWith(END_CONFIG_SECTION));
145+
146+
if (sectionEnd === -1 || sectionEnd <= sectionStart) return; // Odd config file state - don't edit it
147+
fileLines.splice(sectionStart, (sectionEnd - sectionStart) + 1);
148+
sectionStart = _.findIndex(fileLines, (l) => l.startsWith(START_CONFIG_SECTION));
149+
}
150+
151+
// Write & rename to ensure this is atomic, and avoid races here
152+
// as much as we reasonably can.
153+
const tempFile = path + Date.now() + '.temp';
154+
await writeFile(tempFile, fileLines.join('\n'));
155+
return renameFile(tempFile, path);
156+
};
157+
158+
// Cleanup: strip our extra config line from all config files
159+
// Good to do for tidiness, not strictly necessary (the config does nothing
160+
// unless HTTP_TOOLKIT_ACTIVE is set anyway).
161+
export const resetShellStartupScripts = () => {
162+
// For each possible config file, remove our magic line, if present
163+
return Promise.all([
164+
path.join(os.homedir(), '.profile'),
165+
path.join(os.homedir(), '.bash_profile'),
166+
path.join(os.homedir(), '.bash_login'),
167+
path.join(os.homedir(), '.zshenv'),
168+
path.join(os.homedir(), '.zshrc'),
169+
path.join(os.homedir(), '.config', 'fish', 'config.fish'),
170+
].map((configFile) =>
171+
removeConfigSectionsFromFile(configFile).catch(reportError)
172+
));
173+
};

0 commit comments

Comments
 (0)