@duetds/date-picker
Version:
Duet Date Picker is an open source version of Duet Design System’s accessible date picker.
229 lines (196 loc) • 7.38 kB
JavaScript
const debug = require('debug')('wait-port');
const net = require('net');
const outputFunctions = require('./output-functions');
const validateParameters = require('./validate-parameters');
const ConnectionError = require('./errors/connection-error');
function createConnectionWithTimeout({ host, port }, timeout, callback) {
// Variable to hold the timer we'll use to kill the socket if we don't
// connect in time.
let timer = null;
// Try and open the socket, with the params and callback.
const socket = net.createConnection({ host, port }, (err) => {
if (!err) clearTimeout(timer);
return callback(err);
});
// TODO: Check for the socket ECONNREFUSED event.
socket.on('error', (error) => {
debug(`Socket error: ${error}`);
clearTimeout(timer);
socket.destroy();
callback(error);
});
// Kill the socket if we don't open in time.
timer = setTimeout(() => {
socket.destroy();
const error = new Error(`Timeout trying to open socket to ${host}:${port}`);
error.code = 'ECONNTIMEOUT';
callback(error);
}, timeout);
// Return the socket.
return socket;
}
function checkHttp(socket, params, timeout, callback) {
// Create the HTTP request.
const request =
`GET ${params.path} HTTP/1.1
Host: ${params.host}
`;
let timer = null;
timer = setTimeout(() => {
socket.destroy();
const error = new Error(`Timeout waiting for data from ${params.host}:${params.port}`);
error.code = 'EREQTIMEOUT';
callback(error);
}, timeout);
// Get ready for a response.
socket.on('data', function(data) {
// Get the response as text.
const response = data.toString();
const statusLine = response.split('\n')[0];
// Stop the timer.
clearTimeout(timer);
// Check the data. Remember an HTTP response is:
// HTTP/1.1 XXX Stuff
const statusLineParts = statusLine.split(' ');
if (statusLineParts < 2 || statusLineParts[1].startsWith('2') === false) {
debug(`Invalid HTTP status line: ${statusLine}`);
const error = new Error('Invalid response from server');
error.code = 'ERESPONSE';
callback(error);
}
// ALL good!
debug(`Successful HTTP status line: ${statusLine}`);
callback();
});
// Send the request.
socket.write(request);
}
// This function attempts to open a connection, given a limited time window.
// This is the function which we will run repeatedly until we connect.
function tryConnect(options, timeout) {
return new Promise((resolve, reject) => {
try {
const socket = createConnectionWithTimeout(options, timeout, (err) => {
if (err) {
if (err.code === 'ECONNREFUSED') {
// We successfully *tried* to connect, so resolve with false so
// that we try again.
debug('Socket not open: ECONNREFUSED');
socket.destroy();
return resolve(false);
} else if (err.code === 'ECONNTIMEOUT') {
// We've successfully *tried* to connect, but we're timing out
// establishing the connection. This is not ideal (either
// the port is open or it ain't).
debug('Socket not open: ECONNTIMEOUT');
socket.destroy();
return resolve(false);
} else if (err.code === 'ECONNRESET') {
// This can happen if the target server kills its connection before
// we can read from it, we can normally just try again.
debug('Socket not open: ECONNRESET');
socket.destroy();
return resolve(false);
} else if (err.code === 'ENOTFOUND') {
// This will occur if the address is not found, i.e. due to a dns
// lookup fail (normally a problem if the domain is wrong).
debug('Socket cannot be opened: ENOTFOUND');
socket.destroy();
// If we are going to wait for DNS records, we can actually just try
// again...
if (options.waitForDns === true) return resolve(false);
// ...otherwise, we will explicitly fail with a meaningful error for
// the user.
return reject(new ConnectionError(`The address '${options.host}' cannot be found`));
}
// Trying to open the socket has resulted in an error we don't
// understand. Better give up.
debug(`Unexpected error trying to open socket: ${err}`);
socket.destroy();
return reject(err);
}
// Boom, we connected!
debug('Socket connected!');
// If we are not dealing with http, we're done.
if (options.protocol !== 'http') {
// Disconnect, stop the timer and resolve.
socket.destroy();
return resolve(true);
}
// TODO: we should only use the portion of the timeout for this interval which is still left to us.
// Now we've got to wait for a HTTP response.
checkHttp(socket, options, timeout, (err) => {
if (err) {
if (err.code === 'EREQTIMEOUT') {
debug('HTTP error: EREQTIMEOUT');
socket.destroy();
return resolve(false);
} else if (err.code === 'ERESPONSE') {
debug('HTTP error: ERESPONSE');
socket.destroy();
return resolve(false);
}
debug(`Unexpected error checking http response: ${err}`);
socket.destroy();
return reject(err);
}
socket.destroy();
return resolve(true);
});
});
} catch (err) {
// Trying to open the socket has resulted in an exception we don't
// understand. Better give up.
debug(`Unexpected exception trying to open socket: ${err}`);
return reject(err);
}
});
}
function waitPort(params) {
return new Promise((resolve, reject) => {
const {
protocol,
host,
port,
path,
interval,
timeout,
output,
waitForDns,
} = validateParameters(params);
// Keep track of the start time (needed for timeout calcs).
const startTime = new Date();
// Don't wait for more than connectTimeout to try and connect.
const connectTimeout = 1000;
// Grab the object for output.
const outputFunction = outputFunctions[output];
outputFunction.starting({ host, port });
// Start trying to connect.
const loop = () => {
outputFunction.tryConnect();
tryConnect({ protocol, host, port, path, waitForDns }, connectTimeout)
.then((open) => {
debug(`Socket status is: ${open}`);
// The socket is open, we're done.
if (open) {
outputFunction.connected();
return resolve(true);
}
// If we have a timeout, and we've passed it, we're done.
if (timeout && (new Date() - startTime) > timeout) {
outputFunction.timeout();
return resolve(false);
}
// Run the loop again.
return setTimeout(loop, interval);
})
.catch((err) => {
debug(`Unhandled error occured trying to connect: ${err}`);
return reject(err);
});
};
// Start the loop.
loop();
});
}
module.exports = waitPort;