import * as _ from 'lodash' ; import { spawn , ChildProcess , SpawnOptions } from 'child_process' ; import * as GSettings from 'node-gsettings-wrapper' ; import { findExecutableById } from '@httptoolkit/osx-find-executable' ; import { Interceptor } from '..' ; import { HtkConfig } from '../../config' ; import { reportError , addBreadcrumb } from '../../error-tracking' ; import { isErrorLike } from '../../util/error' ; import { canAccess , commandExists } from '../../util/fs' ; import { spawnToResult } from '../../util/process-management' ; import { getTerminalEnvVars } from './terminal-env-overrides' ; import { editShellStartupScripts , resetShellStartupScripts } from './terminal-scripts' ; const DEFAULT_GIT_BASH_PATH = 'C:/Program Files/git/git-bash.exe' ; interface SpawnArgs { command : string ; args ? : string [ ] ; options ? : SpawnOptions ; skipStartupScripts ? : true ; } const getTerminalCommand = _ . memoize ( async ( ) : Promise < SpawnArgs | null > => { let result ; if ( process . platform === 'win32' ) result = getWindowsTerminalCommand ( ) ; else if ( process . platform === 'darwin' ) result = getOSXTerminalCommand ( ) ; else if ( process . platform === 'linux' ) result = getLinuxTerminalCommand ( ) ; else result = Promise . resolve ( null ) ; result . then ( ( terminal ) => { if ( terminal ) addBreadcrumb ( 'Found terminal' , { data : { terminal } } ) ; else reportError ( 'No terminal could be detected' ) ; } ) ; return result ; } ) ; const getWindowsTerminalCommand = async ( ) : Promise < SpawnArgs | null > => { if ( await canAccess ( DEFAULT_GIT_BASH_PATH ) ) { return { command : DEFAULT_GIT_BASH_PATH } ; } else if ( await commandExists ( 'git-bash' ) ) { return { command : 'git-bash' } ; } return { command : 'start' , args : [ 'cmd' ] , options : { shell : true } , skipStartupScripts : true } ; } ; const getOSXTerminalCommand = async ( ) : Promise < SpawnArgs | null > => { const terminalExecutables = ( await Promise . all ( [ 'co.zeit.hyper' , 'com.googlecode.iterm2' , 'com.googlecode.iterm' , 'com.apple.Terminal' ] . map ( ( bundleId ) => findExecutableById ( bundleId ) . catch ( ( ) => null ) ) ) ) . filter ( ( executablePath ) => ! ! executablePath ) ; const bestAvailableTerminal = terminalExecutables [ 0 ] ; if ( bestAvailableTerminal ) return { command : bestAvailableTerminal } ; else return null ; } ; const getLinuxTerminalCommand = async ( ) : Promise < SpawnArgs | null > => { if ( await commandExists ( 'x-terminal-emulator' ) ) return getXTerminalCommand ( ) ; if ( GSettings . isAvailable ( ) ) { const gSettingsTerminalKey = GSettings . Key . findById ( 'org.gnome.desktop.default-applications.terminal' , 'exec' ) ; const defaultTerminal = gSettingsTerminalKey && gSettingsTerminalKey . getValue ( ) ; if ( defaultTerminal && await commandExists ( defaultTerminal ) ) { if ( defaultTerminal . includes ( 'gnome-terminal' ) ) return getGnomeTerminalCommand ( defaultTerminal ) ; if ( defaultTerminal . includes ( 'konsole' ) ) return getKonsoleTerminalCommand ( defaultTerminal ) ; if ( defaultTerminal . includes ( 'xfce4-terminal' ) ) return getXfceTerminalCommand ( defaultTerminal ) ; if ( defaultTerminal . includes ( 'x-terminal-emulator' ) ) return getXTerminalCommand ( defaultTerminal ) ; if ( defaultTerminal . includes ( 'terminator' ) ) return { command : 'terminator' , args : [ '-u' ] } ; return { command : defaultTerminal } ; } } if ( await commandExists ( 'konsole' ) ) return getKonsoleTerminalCommand ( ) ; if ( await commandExists ( 'xfce4-terminal' ) ) return getXfceTerminalCommand ( ) ; if ( await commandExists ( 'kitty' ) ) return { command : 'kitty' } ; if ( await commandExists ( 'urxvt' ) ) return { command : 'urxvt' } ; if ( await commandExists ( 'rxvt' ) ) return { command : 'rxvt' } ; if ( await commandExists ( 'termit' ) ) return { command : 'termit' } ; if ( await commandExists ( 'terminator' ) ) return { command : 'terminator' , args : [ '-u' ] } ; if ( await commandExists ( 'alacritty' ) ) return { command : 'alacritty' } ; if ( await commandExists ( 'uxterm' ) ) return { command : 'uxterm' } ; if ( await commandExists ( 'xterm' ) ) return { command : 'xterm' } ; return null ; } ; const getXTerminalCommand = async ( command = 'x-terminal-emulator' ) : Promise < SpawnArgs > => { try { const { stdout } = await spawnToResult ( command , [ '-h' ] ) ; const helpOutput = stdout . toLowerCase ( ) . replace ( /[^\w\d]+/g , ' ' ) ; if ( helpOutput . includes ( 'gnome terminal' ) && await commandExists ( 'gnome-terminal' ) ) { return getGnomeTerminalCommand ( ) ; } else if ( helpOutput . includes ( 'xfce4 terminal' ) && await commandExists ( 'xfce4-terminal' ) ) { return getXfceTerminalCommand ( ) ; } else if ( helpOutput . includes ( 'konsole' ) && await commandExists ( 'konsole' ) ) { return getKonsoleTerminalCommand ( ) ; } } catch ( e ) { if ( isErrorLike ( e ) && e . message ?. includes ( 'rxvt' ) ) { } else { reportError ( e ) ; } } return { command } ; } ; const getKonsoleTerminalCommand = async ( command = 'konsole' ) : Promise < SpawnArgs > => { let extraArgs = [ ] ; const { stdout } = await spawnToResult ( command , [ '--help' ] ) ; if ( stdout . includes ( '--nofork' ) ) { extraArgs = [ '--nofork' ] ; } return { command , args : extraArgs } ; } ; const getGnomeTerminalCommand = async ( command = 'gnome-terminal' ) : Promise < SpawnArgs > => { let extraArgs = [ ] ; const { stdout } = await spawnToResult ( command , [ '--help-all' ] ) ; if ( stdout . includes ( '--wait' ) ) { extraArgs = [ '--wait' ] ; } else { const randomId = Math . round ( ( Math . random ( ) * 100000 ) ) ; extraArgs = [ '--app-id' , `com.httptoolkit.${ randomId }` ] ; } return { command , args : extraArgs } ; } ; const getXfceTerminalCommand = async ( command = 'xfce4-terminal' ) : Promise < SpawnArgs > => { let extraArgs = [ ] ; const { stdout } = await spawnToResult ( command , [ '--help' ] ) ; if ( stdout . includes ( '--disable-server' ) ) { extraArgs = [ '--disable-server' ] ; } return { command , args : extraArgs } ; } ; const terminals = { } ; export class FreshTerminalInterceptor implements Interceptor { id = 'fresh-terminal' ; version = '1.0.0' ; constructor ( private config ) { } async isActivable ( ) { return ! ! ( await getTerminalCommand ( ) ) ; } isActive ( proxyPort ) { return ! ! ( terminals [ proxyPort ] && terminals [ proxyPort ] ! . length ) ; } async activate ( proxyPort , activationOptions = { } ) { const terminalSpawnArgs = await getTerminalCommand ( ) ; if ( ! terminalSpawnArgs ) throw new Error ( 'Could not find a suitable terminal' ) ; const { command , args , options , skipStartupScripts } = terminalSpawnArgs ; if ( ! skipStartupScripts ) await editShellStartupScripts ( ) ; const currentEnv = ( process . platform === 'win32' ) ? _ . mapKeys ( process . env , ( _value , key ) => key . toUpperCase ( ) ) : process . env ; const childProc = spawn ( command , ( args || [ ] ) , _ . assign ( options || { } , { env : { ... currentEnv , ... getTerminalEnvVars ( proxyPort , this . config . https , currentEnv , { } , { dockerEnabled : activationOptions . dockerEnabled } ) , } , cwd : currentEnv . HOME || currentEnv . USERPROFILE } ) ) ; terminals [ proxyPort ] = ( terminals [ proxyPort ] || [ ] ) . concat ( childProc ) ; const onTerminalClosed = ( ) => { terminals [ proxyPort ] = _ . reject ( terminals [ proxyPort ] , childProc ) ; setTimeout ( ( ) => { if ( _ . every ( terminals , ts => _ . isEmpty ( ts ) ) ) resetShellStartupScripts ( ) ; } , 500 ) ; } ; childProc . once ( 'close' , onTerminalClosed ) ; childProc . once ( 'error' , ( e ) => { reportError ( e ) ; onTerminalClosed ( ) ; } ) ; return new Promise ( ( resolve , reject ) => { setTimeout ( resolve , 500 ) ; childProc . once ( 'error' , reject ) ; } ) ; } async deactivate ( proxyPort ) { if ( ! this . isActive ( proxyPort ) ) return ; await Promise . all ( ( terminals [ proxyPort ] || [ ] ) . map ( ( proc ) => { return new Promise ( ( resolve ) => { proc . once ( 'close' , resolve ) ; proc . kill ( ) ; } ) ; } ) ) ; } async deactivateAll ( ) { await Promise . all ( Object . keys ( terminals ) . map ( ( proxyPort ) => this . deactivate ( proxyPort ) ) ) ; await resetShellStartupScripts ( ) ; } }