diff --git a/app/core/core.ts b/app/core/core.ts index 6a1c67631..16256aa96 100644 --- a/app/core/core.ts +++ b/app/core/core.ts @@ -7,12 +7,11 @@ import path from 'path'; import glob from 'glob'; import camelCase from 'camelcase'; import globby from 'globby'; -import pWaitFor from 'p-wait-for'; import pIteration from 'p-iteration'; import clearModule from 'clear-module'; import { coreLogger } from './log'; import { paths } from './paths'; -import { subscribeToNchanEndpoint, isNchanUp } from './utils'; +import { subscribeToNchanEndpoint } from './utils'; import { config } from './config'; import { pluginManager } from './plugin-manager'; import * as watchers from './watchers'; @@ -160,23 +159,8 @@ const loadNchan = async (): Promise => { coreLogger.debug('Trying to connect to nchan'); - // Wait for nchan to be up. - await pWaitFor(isNchanUp, { - timeout: TEN_SECONDS, - interval: ONE_SECOND - }) - // Once connected open a connection to each known endpoint - .then(async () => connectToNchanEndpoints(endpoints)) - .catch(error => { - // Nchan is likely unreachable - if (error.message.includes('Promise timed out')) { - coreLogger.error('Nchan timed out while trying to establish a connection.'); - return; - } - - // Some other error occured - throw error; - }); + // Connect to each known endpoint + await connectToNchanEndpoints(endpoints); }; /** diff --git a/app/core/utils/clients/nchan.ts b/app/core/utils/clients/nchan.ts index c033ac398..9a4301a97 100644 --- a/app/core/utils/clients/nchan.ts +++ b/app/core/utils/clients/nchan.ts @@ -3,38 +3,31 @@ * Written by: Alexis Tyler */ -import path from 'path'; -import fetch from 'node-fetch'; -import { debugTimer, parseConfig, sleep } from '..'; +import xhr2 from 'xhr2'; +import windowPolyFill from 'node-window-polyfill'; +import { EventSource } from 'launchdarkly-eventsource'; +import { parseConfig } from '..'; import * as states from '../../states'; -import { coreLogger } from '../../log'; +import { log } from '../../log'; import { AppError } from '../../errors'; -const data = {}; +const nchanLogger = log.createChild({ + prefix: 'nchan' +}); + +// Load polyfills for nchan +windowPolyFill.register(false); +global.XMLHttpRequest = xhr2; +global.EventSource = EventSource; + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const NchanSubscriber = require('nchan'); const getSubEndpoint = () => { const httpPort: string = states.varState.data?.port; return `http://localhost:${httpPort}/sub`; }; -export const isNchanUp = async () => { - const isUp = await fetch(`${getSubEndpoint()}/non-existant`, { - method: 'HEAD' - }) - .then(() => true) - .catch(error => { - // Socket is up but this endpoint is invalid - // That's to be expected though. - if (error.code === 'ECONNRESET') { - return true; - } - - return false; - }); - - return isUp; -}; - const endpointToStateMapping = { // Cpuload: , devs: states.devicesState, @@ -49,66 +42,34 @@ const endpointToStateMapping = { var: states.varState }; -const subscribe = async (endpoint: string) => { - // Wait 1s before subscribing - await sleep(1000); - - debugTimer(`subscribe(${endpoint})`); - const response = await fetch(`${getSubEndpoint()}/${endpoint}`).catch(async () => { - // If we throw then let's check if nchan is down - // or if it's an actual error - const isUp = await isNchanUp(); - - if (isUp) { - throw new AppError(`Cannot connect to nchan at ${getSubEndpoint()}/${endpoint}`); - } - - throw new AppError('Cannot connect to nchan'); +const subscribe = async (endpoint: string) => new Promise(resolve => { + const sub = new NchanSubscriber(`${getSubEndpoint()}/${endpoint}`, { + subscriber: 'eventsource' }); - if (response.status === 502) { - // Status 502 is a connection timeout error, - // may happen when the connection was pending for too long, - // and the remote server or a proxy closed it - // let's reconnect - await subscribe(endpoint); - } else if (response.status === 200) { - // Get and show the message - const message = await response.text(); + sub.on('connect', function (_event) { + nchanLogger.debug('Connected!'); + resolve(); + }); - // Create endpoint field on data - if (!data[endpoint]) { - const fileName = endpoint + '.js'; - data[endpoint] = { - handlerPath: path.resolve(__dirname, '../../states', fileName) - }; - } + sub.on('message', function (message, _messageMetadata) { + try { + const state = parseConfig({ + file: message, + type: 'ini' + }); - // Only re-run parser if the message changed - if (data[endpoint].message !== message) { - data[endpoint].updated = new Date(); - data[endpoint].message = message; + // Update state + endpointToStateMapping[endpoint].parse(state); + } catch {} + }); - try { - const state = parseConfig({ - file: message, - type: 'ini' - }); + sub.on('error', function (error, error_description) { + nchanLogger.error('Error: "%s" \nDescription: "%s"', error, error_description); + }); - // Update state - endpointToStateMapping[endpoint].parse(state); - } catch { } - } - - debugTimer(`subscribe(${endpoint})`); - } else { - // An error - let's show it - coreLogger.error(JSON.stringify(response)); - } - - // Re-subscribe - await subscribe(endpoint); -}; + sub.start(); +}); export const subscribeToNchanEndpoint = async (endpoint: string) => { if (!Object.keys(endpointToStateMapping).includes(endpoint)) { diff --git a/package-lock.json b/package-lock.json index dbe018d69..e8ccf315b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1244,6 +1244,12 @@ "integrity": "sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==", "dev": true }, + "@types/nanoajax": { + "version": "0.2.30", + "resolved": "https://registry.npmjs.org/@types/nanoajax/-/nanoajax-0.2.30.tgz", + "integrity": "sha512-gsC1dTR1cC3BVXQ0J3ZSe5Romn4BjdkJF1Q2aLcT+wpmFfYcfcNarn4nmLT+EZUoJcs7tjj7rezxJFgxZ1uT4g==", + "dev": true + }, "@types/node": { "version": "14.14.22", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.22.tgz", @@ -9189,6 +9195,14 @@ "package-json": "^6.3.0" } }, + "launchdarkly-eventsource": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/launchdarkly-eventsource/-/launchdarkly-eventsource-1.4.0.tgz", + "integrity": "sha512-NhyafaXyX2EuO8fDlQORY51eeDx0w5TJWVNqbjDc+3N69grj0zt9FYf8TE+KoFPL1IoxZMLq9mmPvhtBtpfSSQ==", + "requires": { + "original": "^1.0.0" + } + }, "leven": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", @@ -10139,6 +10153,11 @@ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==" }, + "nanoajax": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/nanoajax/-/nanoajax-0.4.3.tgz", + "integrity": "sha1-r3q+wQjXJPuX9vdfEcKmPPMVopg=" + }, "nanoassert": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", @@ -10210,6 +10229,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "nchan": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nchan/-/nchan-1.0.10.tgz", + "integrity": "sha512-jB6fV/03M1AXT08I+hldsU5m2r4t2c7J1UnOsCIK/idowxtJ/nYAPwUahMwhFsuv453miOOCvhmao+kpcScb4Q==" + }, "ncp": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", @@ -10335,6 +10359,24 @@ "process-on-spawn": "^1.0.0" } }, + "node-window-polyfill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-window-polyfill/-/node-window-polyfill-1.0.0.tgz", + "integrity": "sha1-MEJv+fRNHKDYPaS20eJEqA3J5l4=", + "requires": { + "ws": "^5.1.1" + }, + "dependencies": { + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "requires": { + "async-limiter": "~1.0.0" + } + } + } + }, "nodemailer-fetch": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/nodemailer-fetch/-/nodemailer-fetch-1.6.0.tgz", @@ -10702,6 +10744,14 @@ "readable-stream": "^2.0.1" } }, + "original": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", + "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "requires": { + "url-parse": "^1.4.3" + } + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -11252,6 +11302,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, "quick-format-unescaped": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-3.0.3.tgz", @@ -11711,6 +11766,11 @@ "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + }, "reserved-words": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/reserved-words/-/reserved-words-0.1.2.tgz", @@ -14034,6 +14094,15 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, + "url-parse": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", + "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "url-parse-lax": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", @@ -14273,6 +14342,11 @@ "resolved": "https://registry.npmjs.org/xerror/-/xerror-1.1.3.tgz", "integrity": "sha512-2l5hmDymDUIuKT53v/nYxofTMUDQuu5P/Y3qHOjQiih6QUHBCgWpbpL3I8BoE5TVfUVTMmUQ0jdUAimTGc9UIg==" }, + "xhr2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", + "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==" + }, "xss": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.8.tgz", diff --git a/package.json b/package.json index d497fd4eb..3b9db6520 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "graphql-type-uuid": "^0.2.0", "htpasswd-js": "^1.0.2", "ini": "^2.0.0", + "launchdarkly-eventsource": "^1.4.0", "lodash.get": "^4.4.2", "logger": "github:unraid/logger#master", "map-obj": "^4.1.0", @@ -90,9 +91,12 @@ "ms": "^2.1.3", "multi-ini": "^2.1.2", "mustache": "^4.1.0", + "nanoajax": "^0.4.3", "nanobus": "^4.4.0", + "nchan": "^1.0.10", "node-cache": "5.1.2", "node-fetch": "^2.6.1", + "node-window-polyfill": "^1.0.0", "number-to-color": "^0.4.1", "observable-to-promise": "^1.0.0", "os-uptime": "^2.0.2", @@ -119,7 +123,8 @@ "unix-dgram": "^2.0.4", "upcast": "^4.0.0", "uuid": "^8.3.2", - "uuid-apikey": "^1.5.1" + "uuid-apikey": "^1.5.1", + "xhr2": "^0.2.1" }, "devDependencies": { "@commitlint/cli": "^11.0.0", @@ -127,6 +132,7 @@ "@types/cli-table": "^0.3.0", "@types/dockerode": "^3.2.2", "@types/lodash.get": "^4.4.6", + "@types/nanoajax": "^0.2.30", "@types/pify": "^5.0.0", "@types/semver-regex": "^3.1.0", "@types/stoppable": "^1.1.0",