@applitools/execution-grid-tunnel
Version:
Allows user to run tests with exection-grid and navigate to private hosts and ips
208 lines (175 loc) • 5.45 kB
JavaScript
const path = require('path')
const {fork} = require('child_process')
const {generatePromise} = require('../utils/index.js')
const TUNNEL_STATUS = require('./tunnel-status.js')
const SEND_MESSAGE_HEARTBEAT_INTERVAL = 3000
// This line is for pkg
require('./convert-frpc-output-to-tunnel-status.js')
function createFrpcTunnelController({
tunnelId,
tunnelConfig,
configFileRootPath,
binPath,
socks5Proxy,
portRange,
logFilePath,
reconnectTimeout = 30000,
frpcProcessWrapperPath = path.join(__dirname, 'frpc-process-wrapper.js'),
}) {
const additionalEnv = {
configFileRootPath,
binPath,
logFilePath,
socks5ProxyPort: socks5Proxy && socks5Proxy.address().port,
stringifyPortRange: JSON.stringify(portRange),
}
return new TunnelController({
type: 'frpc',
tunnelId,
tunnelConfig,
additionalEnv,
reconnectTimeout,
processWrapperPath: frpcProcessWrapperPath,
logFilePath,
})
}
function createTcpTunnelController({
tunnelId,
tunnelConfig,
loggerOptions,
reconnectTimeout = 30000,
processWrapperPath = path.join(__dirname, 'tunnel-connection-pool-wrapper.js'),
}) {
const additionalEnv = {
stringifyloggerOptions: JSON.stringify(loggerOptions),
}
return new TunnelController({
type: 'tcp',
tunnelId,
tunnelConfig,
additionalEnv,
reconnectTimeout,
processWrapperPath,
logFilePath: path.resolve(loggerOptions.dirname, tunnelId),
})
}
class TunnelController {
constructor({
type,
tunnelId,
tunnelConfig,
additionalEnv = {},
processWrapperPath,
reconnectTimeout,
logFilePath,
}) {
this._type = type
this._tunnelId = tunnelId
this._tunnelConfig = tunnelConfig
this._additionalEnv = additionalEnv
this._processWrapperPath = processWrapperPath
this._reconnectTimeout = reconnectTimeout
this._status = TUNNEL_STATUS.OFF
this._onStatusChangedCallbacks = []
this._reconnectTimeoutId = undefined
this._logFilePath = logFilePath
}
get status() {
return this._status
}
set status(newStatus) {
if (this._status === newStatus) {
return
}
this._status = newStatus
this._onStatusChangedCallbacks.forEach((cb) => cb(this._tunnelId, newStatus))
}
get tunnelId() {
return this._tunnelId
}
async start() {
this.status = TUNNEL_STATUS.INIT
const [promise, resolveFn, rejectFn] = generatePromise()
const tunnelProcess = fork(this._processWrapperPath, {
stdio: 'inherit',
detached: false,
windowsHide: true,
env: {
...process.env,
tunnelId: this._tunnelId,
stringifyConfig: JSON.stringify(this._tunnelConfig),
...this._additionalEnv,
heartbeatTimeout: SEND_MESSAGE_HEARTBEAT_INTERVAL * 7,
},
})
this._frpcProcess = tunnelProcess
const resolveAfterConnectingSucceededOrFailed = ({status}) => {
if (status === TUNNEL_STATUS.STOPPED) {
this.status = TUNNEL_STATUS.ERROR
rejectFn(`Frpc was stopped. look at ${this._logFilePath} to get more information`)
}
if (status === TUNNEL_STATUS.INIT_ERROR || status === TUNNEL_STATUS.INIT_TIMEOUT_ERROR) {
this.status = status
const message = status === TUNNEL_STATUS.INIT_ERROR ? 'Start error.' : 'Timeout error.'
rejectFn(`${message} look at ${this._logFilePath} to get more information`)
this.stop()
}
if (status === TUNNEL_STATUS.RUNNING) {
this.status = TUNNEL_STATUS.RUNNING
resolveFn()
}
}
tunnelProcess.once('message', resolveAfterConnectingSucceededOrFailed)
await promise
tunnelProcess.on('message', ({status}) => {
if (status === TUNNEL_STATUS.RUNNING) {
this._reconnectTimeoutId && clearTimeout(this._reconnectTimeoutId)
this._reconnectTimeoutId = undefined
}
if (!this._reconnectTimeoutId && status === TUNNEL_STATUS.RECONNECT) {
this._reconnectTimeoutId = setTimeout(() => this.stop(), this._reconnectTimeout)
}
this.status = status
})
this._heartBitIntervalId = setInterval(() => {
!tunnelProcess.killed && tunnelProcess.send({status: 'ok'})
}, SEND_MESSAGE_HEARTBEAT_INTERVAL)
tunnelProcess.on('error', (err) => {
console.log(err)
clearInterval(this._heartBitIntervalId)
this.status = TUNNEL_STATUS.ERROR
})
return promise
}
stop() {
const [promise, resolveFn, _rejectFn] = generatePromise()
if (this._heartBitIntervalId) {
clearInterval(this._heartBitIntervalId)
this._heartBitIntervalId = undefined
}
if (this._reconnectTimeoutId) {
clearTimeout(this._reconnectTimeoutId)
this._reconnectTimeoutId = undefined
}
if (this.status === TUNNEL_STATUS.STOPPED || this._frpcProcess.killed) {
return
}
this.status = TUNNEL_STATUS.STOPPING
if (this._frpcProcess.exitCode === null) {
this._frpcProcess.once('exit', () => {
// TODO: change the flow only this class set the status to TUNNEL_STATUS.STOPPED
this.status = TUNNEL_STATUS.STOPPED
resolveFn()
})
this._frpcProcess.kill('SIGTERM')
} else {
this.status = TUNNEL_STATUS.STOPPED
resolveFn()
}
return promise
}
onStatusChanged(cb) {
this._onStatusChangedCallbacks.push(cb)
}
}
module.exports = {createFrpcTunnelController, createTcpTunnelController}