import * as vscode from 'vscode' ; import * as cp from 'child_process' ; import * as path from 'path' ; import * as fs from 'fs' ; import * as os from 'os' ; import * as net from 'net' ; import * as http from 'http' ; import * as crypto from 'crypto' ; import { downloadAndUnzipVSCodeServer } from './download' ; import { terminateProcess } from './util/processes' ; let extHostProcess ; const enum CharCode { Backspace = 8 , LineFeed = 10 } let outputChannel ; export function activate ( context ) { function doResolve ( _authority , progress ) { const connectionToken = String ( crypto . randomInt ( 0xffffffffff ) ) ; const serverPromise = new Promise < vscode . ResolvedAuthority > ( async ( res , rej ) => { progress . report ( { message : 'Starting Test Resolver' } ) ; outputChannel = vscode . window . createOutputChannel ( 'TestResolver' ) ; let isResolved = false ; async function processError ( message ) { outputChannel . appendLine ( message ) ; if ( ! isResolved ) { isResolved = true ; outputChannel . show ( ) ; const result = await vscode . window . showErrorMessage ( message , { modal : true } , ... getActions ( ) ) ; if ( result ) { await result . execute ( ) ; } rej ( vscode . RemoteAuthorityResolverError . NotAvailable ( message , true ) ) ; } } let lastProgressLine = '' ; function processOutput ( output ) { outputChannel . append ( output ) ; for ( let i = 0 ; i < output . length ; i ++ ) { const chr = output . charCodeAt ( i ) ; if ( chr === CharCode . LineFeed ) { const match = lastProgressLine . match ( /Extension host agent listening on (\d+)/ ) ; if ( match ) { isResolved = true ; res ( new vscode . ResolvedAuthority ( '127.0.0.1' , parseInt ( match [ 1 ] , 10 ) , connectionToken ) ) ; } lastProgressLine = '' ; } else if ( chr === CharCode . Backspace ) { lastProgressLine = lastProgressLine . substr ( 0 , lastProgressLine . length - 1 ) ; } else { lastProgressLine += output . charAt ( i ) ; } } } const delay = getConfiguration ( 'startupDelay' ) ; if ( typeof delay === 'number' ) { let remaining = Math . ceil ( delay ) ; outputChannel . append ( `Delaying startup by ${ remaining } seconds (configured by "testresolver.startupDelay").` ) ; while ( remaining > 0 ) { progress . report ( { message : `Delayed resolving: Remaining ${ remaining }s` } ) ; await ( sleep ( 1000 ) ) ; remaining -- ; } } if ( getConfiguration ( 'startupError' ) === true ) { processError ( 'Test Resolver failed for testing purposes (configured by "testresolver.startupError").' ) ; return ; } const { updateUrl , commit , quality , serverDataFolderName , serverApplicationName , dataFolderName } = getProductConfiguration ( ) ; const commandArgs = [ '--host=127.0.0.1' , '--port=0' , '--disable-telemetry' , '--use-host-proxy' , '--accept-server-license-terms' ] ; const env = getNewEnv ( ) ; const remoteDataDir = process . env [ 'TESTRESOLVER_DATA_FOLDER' ] || path . join ( os . homedir ( ) , serverDataFolderName || `${ dataFolderName }-testresolver` ) ; const logsDir = process . env [ 'TESTRESOLVER_LOGS_FOLDER' ] ; if ( logsDir ) { commandArgs . push ( '--logsPath' , logsDir ) ; } env [ 'VSCODE_AGENT_FOLDER' ] = remoteDataDir ; outputChannel . appendLine ( `Using data folder at ${ remoteDataDir }` ) ; const connectionTokenFile = path . join ( remoteDataDir , `${ process . pid }-${ new Date ( ) . getTime ( ) }.token` ) ; if ( ! fs . existsSync ( remoteDataDir ) ) { fs . mkdirSync ( remoteDataDir , { recursive : true } ) ; } fs . writeFileSync ( connectionTokenFile , connectionToken ) ; commandArgs . push ( '--connection-token-file' , connectionTokenFile ) ; if ( ! commit ) { const serverCommand = process . platform === 'win32' ? 'code-server.bat' : 'code-server.sh' ; const vscodePath = path . resolve ( path . join ( context . extensionPath , '..' , '..' ) ) ; const serverCommandPath = path . join ( vscodePath , 'resources' , 'server' , 'bin-dev' , serverCommand ) ; outputChannel . appendLine ( `Launching server: VSCODE_AGENT_FOLDER="${ remoteDataDir }" "${ serverCommandPath }" ${ commandArgs . join ( ' ' ) }` ) ; extHostProcess = cp . spawn ( serverCommandPath , commandArgs , { env , cwd : vscodePath } ) ; } else { const extensionToInstall = process . env [ 'TESTRESOLVER_INSTALL_BUILTIN_EXTENSION' ] ; if ( extensionToInstall ) { commandArgs . push ( '--install-builtin-extension' , extensionToInstall ) ; commandArgs . push ( '--start-server' ) ; } const serverCommand = `${ serverApplicationName }${ process . platform === 'win32' ? '.cmd' : '' }` ; let serverLocation = env [ 'VSCODE_REMOTE_SERVER_PATH' ] ; if ( ! serverLocation ) { const serverBin = path . join ( remoteDataDir , 'bin' ) ; progress . report ( { message : 'Installing VSCode Server' } ) ; serverLocation = await downloadAndUnzipVSCodeServer ( updateUrl , commit , quality , serverBin , m => outputChannel . appendLine ( m ) ) ; } outputChannel . appendLine ( `Using server build at ${ serverLocation }` ) ; outputChannel . appendLine ( `Server arguments ${ commandArgs . join ( ' ' ) }` ) ; extHostProcess = cp . spawn ( path . join ( serverLocation , 'bin' , serverCommand ) , commandArgs , { env , cwd : serverLocation } ) ; } extHostProcess . stdout ! . on ( 'data' , ( data ) => processOutput ( data . toString ( ) ) ) ; extHostProcess . stderr ! . on ( 'data' , ( data ) => processOutput ( data . toString ( ) ) ) ; extHostProcess . on ( 'error' , ( error ) => { processError ( `server failed with error:\n${ error . message }` ) ; extHostProcess = undefined ; } ) ; extHostProcess . on ( 'close' , ( code ) => { processError ( `server closed unexpectedly.\nError code: ${ code }` ) ; extHostProcess = undefined ; } ) ; context . subscriptions . push ( { dispose : ( ) => { if ( extHostProcess ) { terminateProcess ( extHostProcess , context . extensionPath ) ; } try { fs . unlinkSync ( connectionTokenFile ) ; } catch ( _e ) { } } } ) ; } ) ; return serverPromise . then ( serverAddr => { return new Promise < vscode . ResolvedAuthority > ( ( res , _rej ) => { const proxyServer = net . createServer ( proxySocket => { outputChannel . appendLine ( `Proxy connection accepted` ) ; let remoteReady = true , localReady = true ; const remoteSocket = net . createConnection ( { port : serverAddr . port } ) ; let isDisconnected = connectionPaused ; connectionPausedEvent . event ( _ => { let newIsDisconnected = connectionPaused ; if ( isDisconnected !== newIsDisconnected ) { outputChannel . appendLine ( `Connection state: ${ newIsDisconnected ? 'open' : 'paused' }` ) ; isDisconnected = newIsDisconnected ; if ( ! isDisconnected ) { outputChannel . appendLine ( `Resume remote and proxy sockets.` ) ; if ( remoteSocket . isPaused ( ) && localReady ) { remoteSocket . resume ( ) ; } if ( proxySocket . isPaused ( ) && remoteReady ) { proxySocket . resume ( ) ; } } else { outputChannel . appendLine ( `Pausing remote and proxy sockets.` ) ; if ( ! remoteSocket . isPaused ( ) ) { remoteSocket . pause ( ) ; } if ( ! proxySocket . isPaused ( ) ) { proxySocket . pause ( ) ; } } } } ) ; proxySocket . on ( 'data' , ( data ) => { remoteReady = remoteSocket . write ( data ) ; if ( ! remoteReady ) { proxySocket . pause ( ) ; } } ) ; remoteSocket . on ( 'data' , ( data ) => { localReady = proxySocket . write ( data ) ; if ( ! localReady ) { remoteSocket . pause ( ) ; } } ) ; proxySocket . on ( 'drain' , ( ) => { localReady = true ; if ( ! isDisconnected ) { remoteSocket . resume ( ) ; } } ) ; remoteSocket . on ( 'drain' , ( ) => { remoteReady = true ; if ( ! isDisconnected ) { proxySocket . resume ( ) ; } } ) ; proxySocket . on ( 'close' , ( ) => { outputChannel . appendLine ( `Proxy socket closed, closing remote socket.` ) ; remoteSocket . end ( ) ; } ) ; remoteSocket . on ( 'close' , ( ) => { outputChannel . appendLine ( `Remote socket closed, closing proxy socket.` ) ; proxySocket . end ( ) ; } ) ; context . subscriptions . push ( { dispose : ( ) => { proxySocket . end ( ) ; remoteSocket . end ( ) ; } } ) ; } ) ; proxyServer . listen ( 0 , '127.0.0.1' , ( ) => { const port = ( < net . AddressInfo > proxyServer . address ( ) ) . port ; outputChannel . appendLine ( `Going through proxy at port ${ port }` ) ; const r = new vscode . ResolvedAuthority ( '127.0.0.1' , port , connectionToken ) ; r . tunnelFeatures = { elevation : true , privacyOptions : vscode . workspace . getConfiguration ( 'testresolver' ) . get ( 'supportPublicPorts' ) ? [ { id : 'public' , label : 'Public' , themeIcon : 'eye' } , { id : 'other' , label : 'Other' , themeIcon : 'circuit-board' } , { id : 'private' , label : 'Private' , themeIcon : 'eye-closed' } ] : [ ] } ; res ( r ) ; } ) ; context . subscriptions . push ( { dispose : ( ) => { proxyServer . close ( ) ; } } ) ; } ) ; } ) ; } let connectionPaused = false ; let connectionPausedEvent = new vscode . EventEmitter < boolean > ( ) ; const authorityResolverDisposable = vscode . workspace . registerRemoteAuthorityResolver ( 'test' , { async getCanonicalURI ( uri ) { return vscode . Uri . file ( uri . path ) ; } , resolve ( _authority ) { return vscode . window . withProgress ( { location : vscode . ProgressLocation . Notification , title : 'Open TestResolver Remote ([details](command:vscode-testresolver.showLog))' , cancellable : false } , ( progress ) => doResolve ( _authority , progress ) ) ; } , tunnelFactory , showCandidatePort } ) ; context . subscriptions . push ( authorityResolverDisposable ) ; context . subscriptions . push ( vscode . commands . registerCommand ( 'vscode-testresolver.newWindow' , ( ) => { return vscode . commands . executeCommand ( 'vscode.newWindow' , { remoteAuthority : 'test+test' } ) ; } ) ) ; context . subscriptions . push ( vscode . commands . registerCommand ( 'vscode-testresolver.currentWindow' , ( ) => { return vscode . commands . executeCommand ( 'vscode.newWindow' , { remoteAuthority : 'test+test' , reuseWindow : true } ) ; } ) ) ; context . subscriptions . push ( vscode . commands . registerCommand ( 'vscode-testresolver.newWindowWithError' , ( ) => { return vscode . commands . executeCommand ( 'vscode.newWindow' , { remoteAuthority : 'test+error' } ) ; } ) ) ; context . subscriptions . push ( vscode . commands . registerCommand ( 'vscode-testresolver.killServerAndTriggerHandledError' , ( ) => { authorityResolverDisposable . dispose ( ) ; if ( extHostProcess ) { terminateProcess ( extHostProcess , context . extensionPath ) ; } vscode . workspace . registerRemoteAuthorityResolver ( 'test' , { async resolve ( _authority ) { setTimeout ( async ( ) => { await vscode . window . showErrorMessage ( 'Just a custom message.' , { modal : true , useCustom : true } , 'OK' , 'Great' ) ; } , 2000 ) ; throw vscode . RemoteAuthorityResolverError . NotAvailable ( 'Intentional Error' , true ) ; } } ) ; } ) ) ; context . subscriptions . push ( vscode . commands . registerCommand ( 'vscode-testresolver.showLog' , ( ) => { if ( outputChannel ) { outputChannel . show ( ) ; } } ) ) ; const pauseStatusBarEntry = vscode . window . createStatusBarItem ( vscode . StatusBarAlignment . Left ) ; pauseStatusBarEntry . text = 'Remote connection paused. Click to undo' ; pauseStatusBarEntry . command = 'vscode-testresolver.toggleConnectionPause' ; pauseStatusBarEntry . backgroundColor = new vscode . ThemeColor ( 'statusBarItem.errorBackground' ) ; context . subscriptions . push ( vscode . commands . registerCommand ( 'vscode-testresolver.toggleConnectionPause' , ( ) => { if ( ! connectionPaused ) { connectionPaused = true ; pauseStatusBarEntry . show ( ) ; } else { connectionPaused = false ; pauseStatusBarEntry . hide ( ) ; } connectionPausedEvent . fire ( connectionPaused ) ; } ) ) ; context . subscriptions . push ( vscode . commands . registerCommand ( 'vscode-testresolver.openTunnel' , async ( ) => { const result = await vscode . window . showInputBox ( { prompt : 'Enter the remote port for the tunnel' , value : '5000' , validateInput : input => /^[\d]+$/ . test ( input ) ? undefined : 'Not a valid number' } ) ; if ( result ) { const port = Number . parseInt ( result ) ; vscode . workspace . openTunnel ( { remoteAddress : { host : '127.0.0.1' , port : port } , localAddressPort : port + 1 } ) ; } } ) ) ; context . subscriptions . push ( vscode . commands . registerCommand ( 'vscode-testresolver.startRemoteServer' , async ( ) => { const result = await vscode . window . showInputBox ( { prompt : 'Enter the port for the remote server' , value : '5000' , validateInput : input => /^[\d]+$/ . test ( input ) ? undefined : 'Not a valid number' } ) ; if ( result ) { runHTTPTestServer ( Number . parseInt ( result ) ) ; } } ) ) ; vscode . commands . executeCommand ( 'setContext' , 'forwardedPortsViewEnabled' , true ) ; } type ActionItem = ( vscode . MessageItem & { execute : ( ) => void ; } ) ; function getActions ( ) { const actions = [ ] ; const isDirty = vscode . workspace . textDocuments . some ( d => d . isDirty ) || vscode . workspace . workspaceFile && vscode . workspace . workspaceFile . scheme === 'untitled' ; actions . push ( { title : 'Retry' , execute : async ( ) => { await vscode . commands . executeCommand ( 'workbench.action.reloadWindow' ) ; } } ) ; if ( ! isDirty ) { actions . push ( { title : 'Close Remote' , execute : async ( ) => { await vscode . commands . executeCommand ( 'vscode.newWindow' , { reuseWindow : true , remoteAuthority : null } ) ; } } ) ; } actions . push ( { title : 'Ignore' , isCloseAffordance : true , execute : async ( ) => { vscode . commands . executeCommand ( 'vscode-testresolver.showLog' ) ; } } ) ; return actions ; } export interface IProductConfiguration { updateUrl : string ; commit : string ; quality : string ; dataFolderName : string ; serverApplicationName ? : string ; serverDataFolderName ? : string ; } function getProductConfiguration ( ) { const content = fs . readFileSync ( path . join ( vscode . env . appRoot , 'product.json' ) ) . toString ( ) ; return JSON . parse ( content ) as IProductConfiguration ; } function getNewEnv ( ) { const env = { ... process . env } ; delete env [ 'ELECTRON_RUN_AS_NODE' ] ; return env ; } function sleep ( ms ) { return new Promise ( resolve => { setTimeout ( resolve , ms ) ; } ) ; } function getConfiguration < T > ( id ) { return vscode . workspace . getConfiguration ( 'testresolver' ) . get < T > ( id ) ; } const remoteServers = [ ] ; async function showCandidatePort ( _host , port , _detail ) { return remoteServers . includes ( port ) || port === 100 ; } async function tunnelFactory ( tunnelOptions , tunnelCreationOptions ) { outputChannel . appendLine ( `Tunnel factory request: Remote ${ tunnelOptions . remoteAddress . port } -> local ${ tunnelOptions . localAddressPort }` ) ; if ( tunnelCreationOptions . elevationRequired ) { await vscode . window . showInformationMessage ( 'This is a fake elevation message. A real resolver would show a native elevation prompt.' , { modal : true } , 'Ok' ) ; } return createTunnelService ( ) ; function newTunnel ( localAddress ) { const onDidDispose = new vscode . EventEmitter ( ) ; let isDisposed = false ; return { localAddress , remoteAddress : tunnelOptions . remoteAddress , public : ! ! vscode . workspace . getConfiguration ( 'testresolver' ) . get ( 'supportPublicPorts' ) && tunnelOptions . public , privacy : tunnelOptions . privacy , protocol : tunnelOptions . protocol , onDidDispose : onDidDispose . event , dispose : ( ) => { if ( ! isDisposed ) { isDisposed = true ; onDidDispose . fire ( ) ; } } } ; } function createTunnelService ( ) { return new Promise < vscode . Tunnel > ( ( res , _rej ) => { const proxyServer = net . createServer ( proxySocket => { const remoteSocket = net . createConnection ( { host : tunnelOptions . remoteAddress . host , port : tunnelOptions . remoteAddress . port } ) ; remoteSocket . pipe ( proxySocket ) ; proxySocket . pipe ( remoteSocket ) ; } ) ; let localPort = 0 ; if ( tunnelOptions . localAddressPort ) { localPort = tunnelOptions . localAddressPort ; } else { localPort = tunnelOptions . remoteAddress . port ; } if ( localPort === tunnelOptions . remoteAddress . port ) { localPort += 1 ; } if ( localPort < 1024 && process . platform !== 'win32' ) { localPort = 0 ; } proxyServer . listen ( localPort , '127.0.0.1' , ( ) => { const localPort = ( < net . AddressInfo > proxyServer . address ( ) ) . port ; outputChannel . appendLine ( `New test resolver tunnel service: Remote ${ tunnelOptions . remoteAddress . port } -> local ${ localPort }` ) ; const tunnel = newTunnel ( { host : '127.0.0.1' , port : localPort } ) ; tunnel . onDidDispose ( ( ) => proxyServer . close ( ) ) ; res ( tunnel ) ; } ) ; } ) ; } } function runHTTPTestServer ( port ) { const server = http . createServer ( ( _req , res ) => { res . writeHead ( 200 ) ; res . end ( `Hello, World from test server running on port ${ port }!` ) ; } ) ; remoteServers . push ( port ) ; server . listen ( port , '127.0.0.1' ) ; const message = `Opened HTTP server on http://127.0.0.1:${ port }` ; console . log ( message ) ; outputChannel . appendLine ( message ) ; return { dispose : ( ) => { server . close ( ) ; const index = remoteServers . indexOf ( port ) ; if ( index !== - 1 ) { remoteServers . splice ( index , 1 ) ; } } } ; }