import { promises as fs } from 'fs' ; import { createServer , Server } from 'net' ; import * as vscode from 'vscode' ; import * as nls from 'vscode-nls' ; const localize = nls . loadMessageBundle ( ) ; const TEXT_STATUSBAR_LABEL = { [ State . Disabled ] : localize ( 'status.text.auto.attach.disabled' , 'Auto Attach: Disabled' ) , [ State . Always ] : localize ( 'status.text.auto.attach.always' , 'Auto Attach: Always' ) , [ State . Smart ] : localize ( 'status.text.auto.attach.smart' , 'Auto Attach: Smart' ) , [ State . OnlyWithFlag ] : localize ( 'status.text.auto.attach.withFlag' , 'Auto Attach: With Flag' ) , } ; const TEXT_STATE_LABEL = { [ State . Disabled ] : localize ( 'debug.javascript.autoAttach.disabled.label' , 'Disabled' ) , [ State . Always ] : localize ( 'debug.javascript.autoAttach.always.label' , 'Always' ) , [ State . Smart ] : localize ( 'debug.javascript.autoAttach.smart.label' , 'Smart' ) , [ State . OnlyWithFlag ] : localize ( 'debug.javascript.autoAttach.onlyWithFlag.label' , 'Only With Flag' ) , } ; const TEXT_STATE_DESCRIPTION = { [ State . Disabled ] : localize ( 'debug.javascript.autoAttach.disabled.description' , 'Auto attach is disabled and not shown in status bar' ) , [ State . Always ] : localize ( 'debug.javascript.autoAttach.always.description' , 'Auto attach to every Node.js process launched in the terminal' ) , [ State . Smart ] : localize ( 'debug.javascript.autoAttach.smart.description' , "Auto attach when running scripts that aren't in a node_modules folder" ) , [ State . OnlyWithFlag ] : localize ( 'debug.javascript.autoAttach.onlyWithFlag.description' , 'Only auto attach when the `--inspect` flag is given' ) , } ; const TEXT_TOGGLE_WORKSPACE = localize ( 'scope.workspace' , 'Toggle auto attach in this workspace' ) ; const TEXT_TOGGLE_GLOBAL = localize ( 'scope.global' , 'Toggle auto attach on this machine' ) ; const TEXT_TEMP_DISABLE = localize ( 'tempDisable.disable' , 'Temporarily disable auto attach in this session' ) ; const TEXT_TEMP_ENABLE = localize ( 'tempDisable.enable' , 'Re-enable auto attach' ) ; const TEXT_TEMP_DISABLE_LABEL = localize ( 'tempDisable.suffix' , 'Auto Attach: Disabled' ) ; const TOGGLE_COMMAND = 'extension.node-debug.toggleAutoAttach' ; const STORAGE_IPC = 'jsDebugIpcState' ; const SETTING_SECTION = 'debug.javascript' ; const SETTING_STATE = 'autoAttachFilter' ; /**
 * settings that, when changed, should cause us to refresh the state vars
 */ const SETTINGS_CAUSE_REFRESH = new Set ( [ 'autoAttachSmartPattern' , SETTING_STATE ] . map ( s => `${ SETTING_SECTION }.${ s }` ) ) ; const enum State { Disabled = 'disabled' , OnlyWithFlag = 'onlyWithFlag' , Smart = 'smart' , Always = 'always' } let currentState ; let statusItem ; let server ; let isTemporarilyDisabled = false ; export function activate ( context ) { currentState = Promise . resolve ( { context , state : null } ) ; context . subscriptions . push ( vscode . commands . registerCommand ( TOGGLE_COMMAND , toggleAutoAttachSetting . bind ( null , context ) ) ) ; context . subscriptions . push ( vscode . workspace . onDidChangeConfiguration ( e => { if ( e . affectsConfiguration ( `${ SETTING_SECTION }.${ SETTING_STATE }` ) || [ ... SETTINGS_CAUSE_REFRESH ] . some ( setting => e . affectsConfiguration ( setting ) ) ) { updateAutoAttach ( State . Disabled ) ; updateAutoAttach ( readCurrentState ( ) ) ; } } ) ) ; updateAutoAttach ( readCurrentState ( ) ) ; } export async function deactivate ( ) { await destroyAttachServer ( ) ; } function getDefaultScope ( info ) { if ( ! info ) { return vscode . ConfigurationTarget . Global ; } else if ( info . workspaceFolderValue ) { return vscode . ConfigurationTarget . WorkspaceFolder ; } else if ( info . workspaceValue ) { return vscode . ConfigurationTarget . Workspace ; } else if ( info . globalValue ) { return vscode . ConfigurationTarget . Global ; } return vscode . ConfigurationTarget . Global ; } type PickResult = { state : State ; } | { setTempDisabled : boolean ; } | { scope : vscode . ConfigurationTarget ; } | undefined ; type PickItem = vscode . QuickPickItem & ( { state : State ; } | { setTempDisabled : boolean ; } ) ; async function toggleAutoAttachSetting ( context , scope ? ) { const section = vscode . workspace . getConfiguration ( SETTING_SECTION ) ; scope = scope || getDefaultScope ( section . inspect ( SETTING_STATE ) ) ; const isGlobalScope = scope === vscode . ConfigurationTarget . Global ; const quickPick = vscode . window . createQuickPick < PickItem > ( ) ; const current = readCurrentState ( ) ; const items = [ State . Always , State . Smart , State . OnlyWithFlag , State . Disabled ] . map ( state => ( { state , label : TEXT_STATE_LABEL [ state ] , description : TEXT_STATE_DESCRIPTION [ state ] , alwaysShow : true , } ) ) ; if ( current !== State . Disabled ) { items . unshift ( { setTempDisabled : ! isTemporarilyDisabled , label : isTemporarilyDisabled ? TEXT_TEMP_ENABLE : TEXT_TEMP_DISABLE , alwaysShow : true , } ) ; } quickPick . items = items ; quickPick . activeItems = isTemporarilyDisabled ? [ items [ 0 ] ] : quickPick . items . filter ( i => 'state' in i && i . state === current ) ; quickPick . title = isGlobalScope ? TEXT_TOGGLE_GLOBAL : TEXT_TOGGLE_WORKSPACE ; quickPick . buttons = [ { iconPath : new vscode . ThemeIcon ( isGlobalScope ? 'folder' : 'globe' ) , tooltip : isGlobalScope ? TEXT_TOGGLE_WORKSPACE : TEXT_TOGGLE_GLOBAL , } , ] ; quickPick . show ( ) ; let result = await new Promise < PickResult > ( resolve => { quickPick . onDidAccept ( ( ) => resolve ( quickPick . selectedItems [ 0 ] ) ) ; quickPick . onDidHide ( ( ) => resolve ( undefined ) ) ; quickPick . onDidTriggerButton ( ( ) => { resolve ( { scope : isGlobalScope ? vscode . ConfigurationTarget . Workspace : vscode . ConfigurationTarget . Global , } ) ; } ) ; } ) ; quickPick . dispose ( ) ; if ( ! result ) { return ; } if ( 'scope' in result ) { return await toggleAutoAttachSetting ( context , result . scope ) ; } if ( 'state' in result ) { if ( result . state !== current ) { section . update ( SETTING_STATE , result . state , scope ) ; } else if ( isTemporarilyDisabled ) { result = { setTempDisabled : false } ; } } if ( 'setTempDisabled' in result ) { updateStatusBar ( context , current , true ) ; isTemporarilyDisabled = result . setTempDisabled ; if ( result . setTempDisabled ) { await destroyAttachServer ( ) ; } else { await createAttachServer ( context ) ; } updateStatusBar ( context , current , false ) ; } } function readCurrentState ( ) { const section = vscode . workspace . getConfiguration ( SETTING_SECTION ) ; return section . get < State > ( SETTING_STATE ) ?? State . Disabled ; } async function clearJsDebugAttachState ( context ) { if ( server || await context . workspaceState . get ( STORAGE_IPC ) ) { await context . workspaceState . update ( STORAGE_IPC , undefined ) ; await vscode . commands . executeCommand ( 'extension.js-debug.clearAutoAttachVariables' ) ; await destroyAttachServer ( ) ; } } /**
 * Turns auto attach on, and returns the server auto attach is listening on
 * if it's successful.
 */ async function createAttachServer ( context ) { const ipcAddress = await getIpcAddress ( context ) ; if ( ! ipcAddress ) { return undefined ; } server = createServerInner ( ipcAddress ) . catch ( err => { console . error ( err ) ; return undefined ; } ) ; return await server ; } const createServerInner = async ( ipcAddress ) => { try { return await createServerInstance ( ipcAddress ) ; } catch ( e ) { await fs . unlink ( ipcAddress ) . catch ( ( ) => undefined ) ; return await createServerInstance ( ipcAddress ) ; } } ; const createServerInstance = ( ipcAddress ) => new Promise < Server > ( ( resolve , reject ) => { const s = createServer ( socket => { let data = [ ] ; socket . on ( 'data' , async ( chunk ) => { if ( chunk [ chunk . length - 1 ] !== 0 ) { data . push ( chunk ) ; return ; } data . push ( chunk . slice ( 0 , - 1 ) ) ; try { await vscode . commands . executeCommand ( 'extension.js-debug.autoAttachToProcess' , JSON . parse ( Buffer . concat ( data ) . toString ( ) ) ) ; socket . write ( Buffer . from ( [ 0 ] ) ) ; } catch ( err ) { socket . write ( Buffer . from ( [ 1 ] ) ) ; console . error ( err ) ; } } ) ; } ) . on ( 'error' , reject ) . listen ( ipcAddress , ( ) => resolve ( s ) ) ; } ) ; /**
 * Destroys the auto-attach server, if it's running.
 */ async function destroyAttachServer ( ) { const instance = await server ; if ( instance ) { await new Promise ( r => instance . close ( r ) ) ; } } interface CachedIpcState { ipcAddress : string ; jsDebugPath : string ; settingsValue : string ; } /**
 * Map of logic that happens when auto attach states are entered and exited.
 * All state transitions are queued and run in order; promises are awaited.
 */ const transitions = { async [ State . Disabled ] ( context ) { await clearJsDebugAttachState ( context ) ; } , async [ State . OnlyWithFlag ] ( context ) { await createAttachServer ( context ) ; } , async [ State . Smart ] ( context ) { await createAttachServer ( context ) ; } , async [ State . Always ] ( context ) { await createAttachServer ( context ) ; } , } ; /**
 * Ensures the status bar text reflects the current state.
 */ function updateStatusBar ( context , state , busy = false ) { if ( state === State . Disabled && ! busy ) { statusItem ?. hide ( ) ; return ; } if ( ! statusItem ) { statusItem = vscode . window . createStatusBarItem ( 'status.debug.autoAttach' , vscode . StatusBarAlignment . Left ) ; statusItem . name = localize ( 'status.name.auto.attach' , "Debug Auto Attach" ) ; statusItem . command = TOGGLE_COMMAND ; statusItem . tooltip = localize ( 'status.tooltip.auto.attach' , "Automatically attach to node.js processes in debug mode" ) ; context . subscriptions . push ( statusItem ) ; } let text = busy ? '$(loading) ' : '' ; text += isTemporarilyDisabled ? TEXT_TEMP_DISABLE_LABEL : TEXT_STATUSBAR_LABEL [ state ] ; statusItem . text = text ; statusItem . show ( ) ; } /**
 * Updates the auto attach feature based on the user or workspace setting
 */ function updateAutoAttach ( newState ) { currentState = currentState . then ( async ( { context , state : oldState } ) => { if ( newState === oldState ) { return { context , state : oldState } ; } if ( oldState !== null ) { updateStatusBar ( context , oldState , true ) ; } await transitions [ newState ] ( context ) ; isTemporarilyDisabled = false ; updateStatusBar ( context , newState , false ) ; return { context , state : newState } ; } ) ; } /**
 * Gets the IPC address for the server to listen on for js-debug sessions. This
 * is cached such that we can reuse the address of previous activations.
 */ async function getIpcAddress ( context ) { const cachedIpc = context . workspaceState . get < CachedIpcState > ( STORAGE_IPC ) ; const jsDebugPath = vscode . extensions . getExtension ( 'ms-vscode.js-debug-nightly' ) ?. extensionPath || vscode . extensions . getExtension ( 'ms-vscode.js-debug' ) ?. extensionPath ; const settingsValue = getJsDebugSettingKey ( ) ; if ( cachedIpc ?. jsDebugPath === jsDebugPath && cachedIpc ?. settingsValue === settingsValue ) { return cachedIpc . ipcAddress ; } const result = await vscode . commands . executeCommand < { ipcAddress : string ; } > ( 'extension.js-debug.setAutoAttachVariables' , cachedIpc ?. ipcAddress ) ; if ( ! result ) { return ; } const ipcAddress = result . ipcAddress ; await context . workspaceState . update ( STORAGE_IPC , { ipcAddress , jsDebugPath , settingsValue , } as CachedIpcState ) ; return ipcAddress ; } function getJsDebugSettingKey ( ) { let o = { } ; const config = vscode . workspace . getConfiguration ( SETTING_SECTION ) ; for ( const setting of SETTINGS_CAUSE_REFRESH ) { o [ setting ] = config . get ( setting ) ; } return JSON . stringify ( o ) ; }