From 0b60406b43d53b6e4d369059f779953d500eb0be Mon Sep 17 00:00:00 2001 From: Zaid Abdulla Date: Tue, 8 Sep 2020 17:33:48 -0700 Subject: [PATCH] Add lint rule for unhandled promise rejections --- .eslintrc.json | 1 + bin/genieacs-cwmp.ts | 2 +- bin/genieacs-nbi.ts | 2 +- bin/genieacs-ui.ts | 6 +- lib/extensions.ts | 1 + lib/session.ts | 6 +- lib/ui/api-functions.ts | 5 +- lib/ui/db.ts | 144 +++++++++++++++++---------------- ui/app.ts | 2 +- ui/components/ping.ts | 35 ++++---- ui/components/summon-button.ts | 3 + ui/drawer-component.ts | 3 + ui/store.ts | 58 ++++++++++--- ui/wizard-page.ts | 26 +++--- 14 files changed, 177 insertions(+), 117 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index a9dce3a..997e246 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -21,6 +21,7 @@ "@typescript-eslint/no-explicit-any": 0, "@typescript-eslint/no-use-before-define": ["error", { "functions": false }], "@typescript-eslint/explicit-function-return-type": ["error", { "allowExpressions": true }], + "@typescript-eslint/no-floating-promises": "error", "require-atomic-updates": 0, "no-prototype-builtins": 0 } diff --git a/bin/genieacs-cwmp.ts b/bin/genieacs-cwmp.ts index b74b513..6e557e3 100755 --- a/bin/genieacs-cwmp.ts +++ b/bin/genieacs-cwmp.ts @@ -43,7 +43,7 @@ function exitWorkerGracefully(): void { } function exitWorkerUngracefully(): void { - extensions.killAll().then(() => { + extensions.killAll().finally(() => { process.exit(1); }); } diff --git a/bin/genieacs-nbi.ts b/bin/genieacs-nbi.ts index 783cc42..4111884 100755 --- a/bin/genieacs-nbi.ts +++ b/bin/genieacs-nbi.ts @@ -43,7 +43,7 @@ function exitWorkerGracefully(): void { } function exitWorkerUngracefully(): void { - extensions.killAll().then(() => { + extensions.killAll().finally(() => { process.exit(1); }); } diff --git a/bin/genieacs-ui.ts b/bin/genieacs-ui.ts index e798779..faae2bf 100755 --- a/bin/genieacs-ui.ts +++ b/bin/genieacs-ui.ts @@ -45,7 +45,7 @@ function exitWorkerGracefully(): void { } function exitWorkerUngracefully(): void { - extensions.killAll().then(() => { + extensions.killAll().finally(() => { process.exit(1); }); } @@ -99,7 +99,9 @@ if (!cluster.worker) { const _listener = (req, res): void => { if (stopping) res.setHeader("Connection", "close"); - listener(req, res); + listener(req, res).catch((err) => { + throw err; + }); }; const initPromise = Promise.all([db2.connect(), cache.connect()]) diff --git a/lib/extensions.ts b/lib/extensions.ts index 52b6b39..df7c082 100644 --- a/lib/extensions.ts +++ b/lib/extensions.ts @@ -58,6 +58,7 @@ export function run(args: string[]): Promise<{ fault: Fault; value: any }> { }); } + // eslint-disable-next-line @typescript-eslint/no-floating-promises kill(processes[scriptName]); delete processes[scriptName]; } diff --git a/lib/session.ts b/lib/session.ts index ddbad1e..6f1765e 100644 --- a/lib/session.ts +++ b/lib/session.ts @@ -299,7 +299,7 @@ export async function transferComplete( sessionContext.operationsTouched[commandKey] = 1; if (rpcReq.faultStruct && rpcReq.faultStruct.faultCode !== "0") { - await revertDownloadParameters(sessionContext, operation.args.instance); + revertDownloadParameters(sessionContext, operation.args.instance); const fault: Fault = { code: `cwmp.${rpcReq.faultStruct.faultCode}`, @@ -398,10 +398,10 @@ export async function transferComplete( }; } -async function revertDownloadParameters( +function revertDownloadParameters( sessionContext: SessionContext, instance -): Promise { +): void { const timestamp = sessionContext.timestamp + sessionContext.iteration + 1; let p; diff --git a/lib/ui/api-functions.ts b/lib/ui/api-functions.ts index b9fa27a..733cdc0 100644 --- a/lib/ui/api-functions.ts +++ b/lib/ui/api-functions.ts @@ -110,7 +110,7 @@ export async function postTasks( try { await connectionRequest(deviceId, device); } catch (err) { - for (const t of statuses) db.deleteTask(new ObjectID(t._id)); + await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id)))); return { connectionRequest: err.message, tasks: statuses, @@ -138,9 +138,10 @@ export async function postTasks( r.status = "fault"; r["fault"] = res[i * 2 + 1][0]; } - db.deleteTask(new ObjectID(r._id)); } + await Promise.all(statuses.map((t) => db.deleteTask(new ObjectID(t._id)))); + return { connectionRequest: "OK", tasks: statuses }; } diff --git a/lib/ui/db.ts b/lib/ui/db.ts index 265c1bc..3b76133 100644 --- a/lib/ui/db.ts +++ b/lib/ui/db.ts @@ -128,78 +128,80 @@ export function query( } return new Promise((resolve, reject) => { - getClient().then((client) => { - const collection = client - .db() - .collection(RESOURCE_COLLECTION[resource] || resource); - const cursor = collection.find(q); - if (options.projection) { - let projection = options.projection; - if (resource === "devices") { - projection = mongodbFunctions.processDeviceProjection( - options.projection - ); + getClient() + .then((client) => { + const collection = client + .db() + .collection(RESOURCE_COLLECTION[resource] || resource); + const cursor = collection.find(q); + if (options.projection) { + let projection = options.projection; + if (resource === "devices") { + projection = mongodbFunctions.processDeviceProjection( + options.projection + ); + } + + if (resource === "presets") projection.configurations = 1; + cursor.project(projection); } - if (resource === "presets") projection.configurations = 1; - cursor.project(projection); - } + if (resource === "users") cursor.project({ password: 0, salt: 0 }); - if (resource === "users") cursor.project({ password: 0, salt: 0 }); + if (options.skip) cursor.skip(options.skip); + if (options.limit) cursor.limit(options.limit); - if (options.skip) cursor.skip(options.skip); - if (options.limit) cursor.limit(options.limit); + if (options.sort) { + let s = Object.entries(options.sort) + .sort((a, b) => Math.abs(b[1]) - Math.abs(a[1])) + .reduce( + (obj, [k, v]) => + Object.assign(obj, { [k]: Math.min(Math.max(v, -1), 1) }), + {} + ); - if (options.sort) { - let s = Object.entries(options.sort) - .sort((a, b) => Math.abs(b[1]) - Math.abs(a[1])) - .reduce( - (obj, [k, v]) => - Object.assign(obj, { [k]: Math.min(Math.max(v, -1), 1) }), - {} - ); + if (resource === "devices") s = mongodbFunctions.processDeviceSort(s); + cursor.sort(s); + } - if (resource === "devices") s = mongodbFunctions.processDeviceSort(s); - cursor.sort(s); - } - - if (!callback) { - cursor.toArray((err, docs) => { - if (err) return reject(err); - if (resource === "devices") - docs = docs.map((d) => mongodbFunctions.flattenDevice(d)); - else if (resource === "faults") - docs = docs.map((d) => mongodbFunctions.flattenFault(d)); - else if (resource === "tasks") - docs = docs.map((d) => mongodbFunctions.flattenTask(d)); - else if (resource === "presets") - docs = docs.map((d) => mongodbFunctions.flattenPreset(d)); - else if (resource === "files") - docs = docs.map((d) => mongodbFunctions.flattenFile(d)); - return resolve(docs); - }); - } else { - cursor.forEach( - (doc) => { + if (!callback) { + cursor.toArray((err, docs) => { + if (err) return reject(err); if (resource === "devices") - doc = mongodbFunctions.flattenDevice(doc); + docs = docs.map((d) => mongodbFunctions.flattenDevice(d)); else if (resource === "faults") - doc = mongodbFunctions.flattenFault(doc); + docs = docs.map((d) => mongodbFunctions.flattenFault(d)); else if (resource === "tasks") - doc = mongodbFunctions.flattenTask(doc); + docs = docs.map((d) => mongodbFunctions.flattenTask(d)); else if (resource === "presets") - doc = mongodbFunctions.flattenPreset(doc); + docs = docs.map((d) => mongodbFunctions.flattenPreset(d)); else if (resource === "files") - doc = mongodbFunctions.flattenFile(doc); - callback(doc); - }, - (err) => { - if (err) reject(err); - else resolve(); - } - ); - } - }); + docs = docs.map((d) => mongodbFunctions.flattenFile(d)); + return resolve(docs); + }); + } else { + cursor.forEach( + (doc) => { + if (resource === "devices") + doc = mongodbFunctions.flattenDevice(doc); + else if (resource === "faults") + doc = mongodbFunctions.flattenFault(doc); + else if (resource === "tasks") + doc = mongodbFunctions.flattenTask(doc); + else if (resource === "presets") + doc = mongodbFunctions.flattenPreset(doc); + else if (resource === "files") + doc = mongodbFunctions.flattenFile(doc); + callback(doc); + }, + (err) => { + if (err) reject(err); + else resolve(); + } + ); + } + }) + .catch(reject); }); } @@ -221,15 +223,17 @@ export function count(resource: string, filter: Expression): Promise { } return new Promise((resolve, reject) => { - getClient().then((client) => { - const collection = client - .db() - .collection(RESOURCE_COLLECTION[resource] || resource); - collection.find(q).count((err, c) => { - if (err) reject(err); - else resolve(c); - }); - }); + getClient() + .then((client) => { + const collection = client + .db() + .collection(RESOURCE_COLLECTION[resource] || resource); + collection.find(q).count((err, c) => { + if (err) reject(err); + else resolve(c); + }); + }) + .catch(reject); }); } diff --git a/ui/app.ts b/ui/app.ts index 2494513..3418054 100644 --- a/ui/app.ts +++ b/ui/app.ts @@ -71,7 +71,7 @@ let fulfillTimeout = null; function fulfill(): void { clearTimeout(fulfillTimeout); fulfillTimeout = setTimeout(() => { - store.fulfill(lastRenderTimestamp, pageVisitTimestamp).then((updated) => { + store.fulfill(lastRenderTimestamp, pageVisitTimestamp, (updated) => { if (updated) m.redraw(); }); }, 100); diff --git a/ui/components/ping.ts b/ui/components/ping.ts index ac093bc..82768ed 100644 --- a/ui/components/ping.ts +++ b/ui/components/ping.ts @@ -27,23 +27,28 @@ const component: ClosureComponent = (vn): Component => { let interval: ReturnType; let host: string; - const refresh = async (): Promise => { - let status = ""; - if (host) { - try { - const res = await store.ping(host); - if (res["avg"] != null) status = `${Math.trunc(res["avg"])} ms`; - else status = "Unreachable"; - } catch (err) { - setTimeout(() => { - throw err; - }, 0); - } + const refresh = (): void => { + if (!host) { + const dom = (vn as VnodeDOM).dom; + if (dom) dom.innerHTML = ""; + return; } - const dom = (vn as VnodeDOM).dom; - - if (dom) dom.innerHTML = `Pinging ${host}: ${status}`; + let status = ""; + store + .ping(host) + .then((res) => { + if (res["avg"] != null) status = `${Math.trunc(res["avg"])} ms`; + else status = "Unreachable"; + }) + .catch(() => { + status = "Error!"; + clearInterval(interval); + }) + .finally(() => { + const dom = (vn as VnodeDOM).dom; + if (dom) dom.innerHTML = `Pinging ${host}: ${status}`; + }); }; return { diff --git a/ui/components/summon-button.ts b/ui/components/summon-button.ts index 8afc770..79a5566 100644 --- a/ui/components/summon-button.ts +++ b/ui/components/summon-button.ts @@ -79,6 +79,9 @@ const component: ClosureComponent = (): Component => { .then(() => { e.target.disabled = false; store.fulfill(0, Date.now()); + }) + .catch((err) => { + throw err; }); }, }, diff --git a/ui/drawer-component.ts b/ui/drawer-component.ts index a6056e3..f87e89e 100644 --- a/ui/drawer-component.ts +++ b/ui/drawer-component.ts @@ -441,6 +441,9 @@ const component: ClosureComponent = (): Component => { ) .then(() => { store.fulfill(0, Date.now()); + }) + .catch((err) => { + throw err; }); }, }, diff --git a/ui/store.ts b/ui/store.ts index 95a9cac..aac511b 100644 --- a/ui/store.ts +++ b/ui/store.ts @@ -152,6 +152,8 @@ function checkConnection(): void { } } }, + }).catch((err) => { + notifications.push("error", err.message); }); } @@ -169,11 +171,25 @@ export async function xhrRequest( ): any => { if (typeof extract === "function") return extract(xhr, _options); + // https://mithril.js.org/request.html#error-handling + if (xhr.status !== 304 && Math.floor(xhr.status / 100) !== 2) { + const err = new Error(); + err["message"] = + xhr.status === 0 + ? "Server is unrachable" + : `Unexpected response status code ${xhr.status}`; + err["code"] = xhr.status; + err["response"] = xhr.responseText; + throw err; + } + let response: any; if (typeof deserialize === "function") { response = deserialize(xhr.responseText); } else if ( - xhr.getResponseHeader("content-type").startsWith("application/json") + (xhr.getResponseHeader("content-type") || "").startsWith( + "application/json" + ) ) { try { response = xhr.responseText ? JSON.parse(xhr.responseText) : null; @@ -184,15 +200,6 @@ export async function xhrRequest( response = xhr.responseText; } - // https://mithril.js.org/request.html#error-handling - if (xhr.status !== 304 && Math.floor(xhr.status / 100) !== 2) { - const err = new Error(); - err["message"] = xhr.responseText; - err["code"] = xhr.status; - err["response"] = response; - throw err; - } - return response; }; @@ -340,7 +347,7 @@ export function fetch( return queryResponse; } -export function fulfill( +function _fulfill( accessTimestamp: number, _fulfillTimestamp: number ): Promise { @@ -377,7 +384,16 @@ export function fulfill( m.buildQueryString({ filter: memoizedStringify(filter), }), - extract: (xhr) => +xhr.getResponseHeader("x-total-count"), + extract: (xhr) => { + if (!xhr.status) { + throw new Error("Server is unrachable"); + } else if (xhr.status !== 200) { + throw new Error( + `Unexpected response status code ${xhr.status}` + ); + } + return +xhr.getResponseHeader("x-total-count"); + }, background: true, }) .then((c) => { @@ -560,6 +576,17 @@ export function fulfill( }); } +export function fulfill( + accessTimestamp: number, + _fulfillTimestamp: number, + callback?: (updated: boolean) => void +): void { + _fulfill(accessTimestamp, _fulfillTimestamp).then(callback, (err) => { + notifications.push("error", err.message); + if (callback) callback(false); + }); +} + export function getTimestamp(): number { return fulfillTimestamp; } @@ -647,7 +674,12 @@ export function resourceExists(resource: string, id: string): Promise { m.buildQueryString({ filter: memoizedStringify(filter), }), - extract: (xhr) => +xhr.getResponseHeader("x-total-count"), + extract: (xhr) => { + if (!xhr.status) throw new Error("Server is unrachable"); + else if (xhr.status !== 200) + throw new Error(`Unexpected response status code ${xhr.status}`); + return +xhr.getResponseHeader("x-total-count"); + }, background: true, }); } diff --git a/ui/wizard-page.ts b/ui/wizard-page.ts index 5dd156d..5f05018 100644 --- a/ui/wizard-page.ts +++ b/ui/wizard-page.ts @@ -87,16 +87,24 @@ export const component: ClosureComponent = (vnode): Component => { }) .then(() => { setTimeout(() => { - m.request({ url: "init" }).then((o) => { - e.target.disabled = false; - options = o; - notifications.push("success", "Initialization complete", { - "Open Sesame!": () => { - m.route.set("/login"); - window.location.reload(); - }, + m.request({ url: "init" }) + .then((o) => { + e.target.disabled = false; + options = o; + notifications.push( + "success", + "Initialization complete", + { + "Open Sesame!": () => { + m.route.set("/login"); + window.location.reload(); + }, + } + ); + }) + .catch((err) => { + notifications.push("error", err.message); }); - }); }, 3000); if (opts["users"]) { alert(