UNPKG

@atomist/automation-client

Version:
229 lines • 9.62 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const axios_1 = require("axios"); const HttpsProxyAgent = require("https-proxy-agent"); const stringify = require("json-stringify-safe"); const promiseRetry = require("promise-retry"); const serializeError = require("serialize-error"); const WebSocket = require("ws"); const zlib = require("zlib"); const Deferred_1 = require("../../util/Deferred"); const http_1 = require("../../util/http"); const logger_1 = require("../../util/logger"); const shutdown_1 = require("../../util/shutdown"); const RequestProcessor_1 = require("../RequestProcessor"); const WebSocketMessageClient_1 = require("./WebSocketMessageClient"); class WebSocketClient { constructor(registrationCallback, configuration, requestProcessor) { this.registrationCallback = registrationCallback; this.configuration = configuration; this.requestProcessor = requestProcessor; } start() { const connection = register(this.registrationCallback, this.configuration, this.requestProcessor, 5) .then(registration => connect(this.registrationCallback, registration, this.configuration, this.requestProcessor)); return connection.then(() => { shutdown_1.registerShutdownHook(() => { reconnect = false; if (this.configuration.ws.termination && this.configuration.ws.termination.graceful === true) { logger_1.logger.info("Initiating WebSocket connection shutdown"); // Now wait for configured timeout to let in-flight messages finish processing const deferred = new Deferred_1.Deferred(); setTimeout(() => { ws.close(); logger_1.logger.info("Closing WebSocket connection"); deferred.resolve(0); }, this.configuration.ws.termination.gracePeriod); return deferred.promise .then(code => { return code; }); } else { ws.close(); logger_1.logger.info("Closing WebSocket connection"); return Promise.resolve(0); } }); }).catch(() => { logger_1.logger.error("Persistent error registering with Atomist. Exiting..."); process.exit(1); }); } } exports.WebSocketClient = WebSocketClient; let reconnect = true; let ping = 0; let pong; let ws; function connect(registrationCallback, registration, configuration, requestProcessor) { // Functions are inline to avoid "this" peculiarities function invokeCommandHandler(chr) { requestProcessor.processCommand(chr); } function invokeEventHandler(e) { requestProcessor.processEvent(e); } return new Promise(resolve => { if (process.env.HTTPS_PROXY || process.env.https_proxy) { const proxy = process.env.HTTPS_PROXY || process.env.https_proxy; logger_1.logger.debug(`Opening WebSocket connection using proxy '${proxy}'`); const agent = new HttpsProxyAgent(proxy); ws = new WebSocket(registration.url, { agent }); } else { logger_1.logger.info(`Opening WebSocket connection`); ws = new WebSocket(registration.url); } let timer; ws.on("open", function open() { requestProcessor.onConnect(this); resolve(ws); // Install ping/pong timer and shutdown hooks timer = setInterval(() => { if (pong + 1 < ping) { reset(); ws.terminate(); logger_1.logger.error("Missing ping/pong from the server. Closing WebSocket"); } else { WebSocketMessageClient_1.sendMessage({ ping }, ws, false); ping++; } }, 10000); }); ws.on("message", function incoming(data) { function handleMessage(request) { try { request = JSON.parse(request); } catch (err) { logger_1.logger.error(`Failed to parse incoming message: %s`, request); return; } try { if (isPing(request)) { WebSocketMessageClient_1.sendMessage({ pong: request.ping }, ws, false); } else if (isPong(request)) { pong = request.pong; } else if (isControl(request)) { logger_1.logger.info("WebSocket connection stopped listening for incoming messages"); } else { if (RequestProcessor_1.isCommandIncoming(request)) { invokeCommandHandler(request); } else if (RequestProcessor_1.isEventIncoming(request)) { invokeEventHandler(request); } else { logger_1.logger.error(`Unknown message payload received: ${data}`); } } } catch (err) { console.error("Failed processing of message payload with: %s", JSON.stringify(serializeError(err))); } } if (configuration.ws.compress) { zlib.gunzip(data, (err, result) => { if (!err) { handleMessage(result.toString()); } else { logger_1.logger.warn(`Failed to decompress incoming message: %s`, data); handleMessage(data); } }); } else { handleMessage(data); } }); // On close this websocket is meant to reconnect ws.on("close", (code, message) => { if (code) { logger_1.logger.warn(`WebSocket connection closed with ${code}: ${message}`); } else { logger_1.logger.warn(`WebSocket connection closed`); } reset(); // Only attempt to reconnect if we aren't shutting down if (reconnect) { register(registrationCallback, configuration, requestProcessor) .then(reg => connect(registrationCallback, reg, configuration, requestProcessor)); } }); ws.on("error", err => { if (err) { logger_1.logger.warn(`WebSocket error occurred: ${JSON.stringify(serializeError(err))}`); } }); function reset() { requestProcessor.onDisconnect(); clearInterval(timer); ping = 0; pong = 0; } }); } function register(registrationCallback, configuration, handler, retries = 100) { const registrationPayload = registrationCallback(); logger_1.logger.debug(`Registering ${registrationPayload.name}:${registrationPayload.version} ` + `with Atomist at '${configuration.endpoints.api}': ${stringify(registrationPayload)}`); const retryOptions = { retries, factor: 3, minTimeout: 1 * 500, maxTimeout: 5 * 1000, randomize: true, }; return promiseRetry(retryOptions, (retry, retryCount) => { if (retryCount > 1) { logger_1.logger.warn("Retrying registration due to previous error"); } const authorization = configuration.apiKey ? `Bearer ${configuration.apiKey}` : `token ${configuration.token}`; const config = { headers: { Authorization: authorization }, timeout: configuration.ws.timeout || 10000, }; return axios_1.default.post(configuration.endpoints.api, registrationPayload, http_1.configureProxy(config)) .then(result => { const registration = result.data; registration.name = registrationPayload.name; registration.version = registrationPayload.version; handler.onRegistration(registration); return registration; }) .catch(error => { const nameVersion = `${registrationPayload.name}@${registrationPayload.version}`; if (error.response && error.response.status === 409) { logger_1.logger.error(`Registration failed because a session for ${nameVersion} is already active`); retry(error); } else if (error.response && (error.response.status === 400 || error.response.status === 401 || error.response.status === 403)) { logger_1.logger.error(`Registration failed with code '%s': '%s'`, error.response.status, JSON.stringify(error.response.data)); process.exit(1); } else { logger_1.logger.error("Registration failed with '%s'", error); retry(error); } }); }); } function isPing(a) { return a.ping != null; } function isPong(a) { return a.pong != null; } function isControl(a) { return a.control != null; } //# sourceMappingURL=WebSocketClient.js.map