import { access } from 'fs' ; import { watch } from 'chokidar' ; import { find } from 'lodash' ; import { promisify } from 'util' ; import { actions } from '../actions' ; import { spawnSshMaster } from '../ssh' ; import { HostConfig } from '../types' ; import { AsyncThunkAction , Dispatch , GetState } from '../../types/redux' ; import { ErrorWithCode } from '../../utils/error-with-code' ; import { createLogger } from '../../log' ; import { makeTmpPath } from '../../utils/tmp' ; const log = createLogger ( __filename ) ; const extractConfigControlPath = ( hostConfig ) : string | null => { const sshConfig = hostConfig . ssh . config ; const key = Object . keys ( sshConfig ) . find ( k => k . toLowerCase ( ) === 'controlpath' ) ; return key ? sshConfig [ key ] : null ; } ; const accessAsync = promisify ( access ) ; const fileExists = ( path ) : Promise < boolean > => accessAsync ( path ) . then ( ( ) => true ) . catch ( ( ) => false ) ; export const hostConnect = ( id , reason ) : AsyncThunkAction => async ( dispatch , getState ) => { const state = getState ( ) ; const host = find ( state . hosts , { id } ) ; if ( ! host ) { throw new ErrorWithCode ( 404 , `host not found: ${ id }` ) ; } if ( ! [ 'disconnected' , 'error' ] . includes ( host . state . status ) ) { log . info ( `host not down: ${ id }` ) ; return ; } const controlPath = extractConfigControlPath ( host . config ) || await makeTmpPath ( __filename ) ( id ) ; if ( await fileExists ( controlPath ) ) { log . error ( `control path already exists: ${ controlPath }` ) ; dispatch ( actions . hostStateError ( id ) ) ; return ; } const process = spawnSshMaster ( { sshCommand : 'ssh' , controlPath , sshParams : host . config . ssh } ) ; dispatch ( actions . hostStateConnecting ( id , process . pid , controlPath , reason ) ) ; const watcher = watch ( controlPath , { useFsEvents : false } ) . on ( 'error' , err => log . error ( { err } , 'watcher error' ) ) . once ( 'add' , ( ) => { watcher . close ( ) ; dispatch ( actions . hostStateConnected ( id , new Date ( ) ) ) ; } ) ; process . on ( 'exit' , ( code ) => { watcher . close ( ) ; dispatch ( ( code === 0 || code === null ) ? actions . hostStateDisconnected ( id ) : actions . hostStateError ( id ) ) ; } ) ; } ; export const hostDisconnect = ( id , reason ) : AsyncThunkAction => ( dispatch , getState ) => { const state = getState ( ) ; const host = find ( state . hosts , { id } ) ; if ( ! host ) { throw new ErrorWithCode ( 404 , `host not found: ${ id }` ) ; } if ( ! [ 'connecting' , 'connected' ] . includes ( host . state . status ) ) { log . info ( 'host is not connected:' , id ) ; return Promise . resolve ( ) ; } if ( host . state . pid === null ) { throw new ErrorWithCode ( 500 , `bad state: ${ id } (${ JSON . stringify ( host . state ) })` ) ; } process . kill ( host . state . pid ) ; dispatch ( actions . hostStateDisconnecting ( id , reason ) ) ; return Promise . resolve ( ) ; } ;