1
1
import * as _ from 'lodash' ;
2
2
import * as fs from 'fs' ;
3
+ import * as os from 'os' ;
3
4
import * as path from 'path' ;
4
5
import * as util from 'util' ;
5
6
import { spawn , ChildProcess , SpawnOptions } from 'child_process' ;
@@ -20,6 +21,8 @@ const commandExists = (path: string): Promise<boolean> => ensureCommandExists(pa
20
21
21
22
const DEFAULT_GIT_BASH_PATH = 'C:/Program Files/git/git-bash.exe' ;
22
23
const PATH_VAR_SEPARATOR = process . platform === 'win32' ? ';' : ':' ;
24
+ const SHELL = ( process . env . SHELL || '' ) . split ( '/' ) . slice ( - 1 ) [ 0 ] ;
25
+ const OVERRIDE_BIN_PATH = path . join ( __dirname , 'terminal-wrappers' ) ;
23
26
24
27
interface SpawnArgs {
25
28
command : string ;
@@ -72,6 +75,110 @@ const getTerminalCommand = _.memoize(async (): Promise<SpawnArgs | null> => {
72
75
return null ;
73
76
} ) ;
74
77
78
+ // Works in bash, zsh, dash, ksh, sh (not fish)
79
+ const SH_SHELL_PATH_CONFIG = `
80
+ # Do not edit (all lines including $HTTP_TOOLKIT_ACTIVE will be removed automatically)
81
+ if [ -n "$HTTP_TOOLKIT_ACTIVE" ]; then export PATH="${ OVERRIDE_BIN_PATH } :$PATH"; fi` ;
82
+ const FISH_SHELL_PATH_CONFIG = `
83
+ # Do not edit (all lines including $HTTP_TOOLKIT_ACTIVE will be removed automatically)
84
+ [ -n "$HTTP_TOOLKIT_ACTIVE" ]; and set -x PATH "${ OVERRIDE_BIN_PATH } " $PATH;` ;
85
+ // Used to remove these lines from the config later
86
+ const SHELL_PATH_CONFIG_MATCHER = / .* \$ H T T P _ T O O L K I T _ A C T I V E .* / ;
87
+
88
+ const appendOrCreateFile = util . promisify ( fs . appendFile ) ;
89
+ const appendToFirstExisting = async ( paths : string [ ] , forceWrite : boolean , contents : string ) => {
90
+ for ( let path of paths ) {
91
+ // Small race here, but end result is ok either way
92
+ if ( await canAccess ( path ) ) {
93
+ return appendOrCreateFile ( path , contents ) ;
94
+ }
95
+ }
96
+
97
+ if ( forceWrite ) {
98
+ // If force write is set, write the last file anyway
99
+ return appendOrCreateFile ( paths . slice ( - 1 ) [ 0 ] , contents ) ;
100
+ }
101
+ } ;
102
+
103
+ // Find the relevant user shell config file, add the above line to it, so that
104
+ // shells launched with HTTP_TOOLKIT_ACTIVE set use the interception PATH.
105
+ const editShellStartupScripts = async ( ) => {
106
+ await resetShellStartupScripts ( ) ;
107
+
108
+ // .profile is used by Dash, Bash sometimes, and by Sh:
109
+ appendOrCreateFile ( path . join ( os . homedir ( ) , '.profile' ) , SH_SHELL_PATH_CONFIG )
110
+ . catch ( reportError ) ;
111
+
112
+ // Bash uses some other files by preference, if they exist:
113
+ appendToFirstExisting (
114
+ [
115
+ path . join ( os . homedir ( ) , '.bash_profile' ) ,
116
+ path . join ( os . homedir ( ) , '.bash_login' )
117
+ ] ,
118
+ false , // Do nothing if they don't exist - it falls back to .profile
119
+ SH_SHELL_PATH_CONFIG
120
+ ) . catch ( reportError ) ;
121
+
122
+ // Zsh has its own files (both are actually used)
123
+ appendToFirstExisting (
124
+ [
125
+ path . join ( os . homedir ( ) , '.zshenv' ) ,
126
+ path . join ( os . homedir ( ) , '.zshrc' )
127
+ ] ,
128
+ SHELL === 'zsh' , // If you use zsh, we _always_ write a config file
129
+ SH_SHELL_PATH_CONFIG
130
+ ) . catch ( reportError ) ;
131
+
132
+ // Fish always uses the same config file
133
+ appendToFirstExisting (
134
+ [
135
+ path . join ( os . homedir ( ) , '.config' , 'fish' , 'config.fish' ) ,
136
+ ] ,
137
+ SHELL === 'fish' || await canAccess ( path . join ( os . homedir ( ) , '.config' , 'fish' ) ) ,
138
+ FISH_SHELL_PATH_CONFIG
139
+ ) . catch ( reportError ) ;
140
+ } ;
141
+
142
+ const readFile = util . promisify ( fs . readFile ) ;
143
+ const writeFile = util . promisify ( fs . writeFile ) ;
144
+ const renameFile = util . promisify ( fs . rename ) ;
145
+ const removeMatchingInFile = async ( path : string , matcher : RegExp ) => {
146
+ let fileLines : string [ ] ;
147
+
148
+ try {
149
+ fileLines = ( await readFile ( path , 'utf8' ) ) . split ( '\n' ) ;
150
+ } catch ( e ) {
151
+ // Silently skip any files we can't read
152
+ return ;
153
+ }
154
+
155
+ // Drop all matching lines from the config file
156
+ fileLines = fileLines . filter ( line => ! matcher . test ( line ) ) ;
157
+ // Write & rename to ensure this is atomic, and avoid races here
158
+ // as much as we reasonably can.
159
+ const tempFile = path + Date . now ( ) + '.temp' ;
160
+ await writeFile ( tempFile , fileLines . join ( '\n' ) ) ;
161
+ return renameFile ( tempFile , path ) ;
162
+ } ;
163
+
164
+ // Cleanup: strip our extra config line from all config files
165
+ // Good to do for tidiness, not strictly necessary (the config does nothing
166
+ // unless HTTP_TOOLKIT_ACTIVE is set anyway).
167
+ const resetShellStartupScripts = ( ) => {
168
+ // For each possible config file, remove our magic line, if present
169
+ return Promise . all ( [
170
+ path . join ( os . homedir ( ) , '.profile' ) ,
171
+ path . join ( os . homedir ( ) , '.bash_profile' ) ,
172
+ path . join ( os . homedir ( ) , '.bash_login' ) ,
173
+ path . join ( os . homedir ( ) , '.zshenv' ) ,
174
+ path . join ( os . homedir ( ) , '.zshrc' ) ,
175
+ path . join ( os . homedir ( ) , '.config' , 'fish' , 'config.fish' ) ,
176
+ ] . map ( ( configFile ) =>
177
+ removeMatchingInFile ( configFile , SHELL_PATH_CONFIG_MATCHER )
178
+ . catch ( reportError )
179
+ ) ) ;
180
+ } ;
181
+
75
182
const terminals : _ . Dictionary < ChildProcess [ ] | undefined > = { }
76
183
77
184
export class TerminalInterceptor implements Interceptor {
@@ -95,7 +202,16 @@ export class TerminalInterceptor implements Interceptor {
95
202
96
203
const { command, args, options } = terminalSpawnArgs ;
97
204
98
- const childProc = spawn ( command , args || [ ] , _ . assign ( options || { } , {
205
+ // On OSX, our PATH override below doesn't work, because path_helper always runs and prepends
206
+ // the real paths over the top. To fix this, we (very carefully!) rewrite shell startup
207
+ // scripts, to reset PATH there.
208
+ // This gets reset on exit, and is behind a flag so it won't affect other shells anyway.
209
+ if ( process . platform === 'darwin' ) await editShellStartupScripts ( ) ;
210
+
211
+ const childProc = spawn (
212
+ command ,
213
+ ( args || [ ] ) ,
214
+ _ . assign ( options || { } , {
99
215
env : _ . assign ( { } , process . env , {
100
216
'http_proxy' : `http://localhost:${ proxyPort } ` ,
101
217
'HTTP_PROXY' : `http://localhost:${ proxyPort } ` ,
@@ -115,16 +231,18 @@ export class TerminalInterceptor implements Interceptor {
115
231
// Trust cert for HTTPS requests from Git
116
232
'GIT_SSL_CAINFO' : this . config . https . certPath ,
117
233
234
+ 'HTTP_TOOLKIT_ACTIVE' : 'true' ,
235
+
118
236
// Prepend our bin overrides into $PATH
119
- 'PATH' : `${ path . join ( __dirname , 'terminal-wrappers' ) } ${ PATH_VAR_SEPARATOR } ${ process . env . PATH } `
237
+ 'PATH' : `${ OVERRIDE_BIN_PATH } ${ PATH_VAR_SEPARATOR } ${ process . env . PATH } `
120
238
} ) ,
121
239
cwd : process . env . HOME || process . env . USERPROFILE
122
240
} ) ) ;
123
241
124
242
terminals [ proxyPort ] = ( terminals [ proxyPort ] || [ ] ) . concat ( childProc ) ;
125
243
126
-
127
244
const onTerminalClosed = ( ) => {
245
+ if ( process . platform === 'darwin' ) resetShellStartupScripts ( ) ;
128
246
terminals [ proxyPort ] = _ . reject ( terminals [ proxyPort ] , childProc ) ;
129
247
} ;
130
248
childProc . once ( 'exit' , onTerminalClosed ) ;
0 commit comments