From f305a333ba70e90d098decdf81c220bfcbeec899 Mon Sep 17 00:00:00 2001 From: Cameron Diver Date: Thu, 3 Sep 2020 14:25:33 +0100 Subject: [PATCH] Add device system information to state endpoint patch Change-type: minor Signed-off-by: Cameron Diver --- package-lock.json | 34 ++++++--- package.json | 5 +- src/device-state/current-state.ts | 47 ++++++++++++- src/lib/system-info.ts | 113 ++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 12 deletions(-) create mode 100644 src/lib/system-info.ts diff --git a/package-lock.json b/package-lock.json index e11a65bf..cfff3399 100644 --- a/package-lock.json +++ b/package-lock.json @@ -613,6 +613,12 @@ "integrity": "sha1-qIc1gLOoS2msHmhzI7Ffu+uQR5o=", "dev": true }, + "@types/os-utils": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/os-utils/-/os-utils-0.0.1.tgz", + "integrity": "sha512-r5a1I4xr8e25GFiLdpHV+jHUawtbmtuCQHR1P3SH/Y1gmRy0KLjBVGe5uUkODU/6GntoGPhOu05jv+sBdAVpog==", + "dev": true + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", @@ -1311,7 +1317,7 @@ }, "util": { "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", + "resolved": "http://registry.npmjs.org/util/-/util-0.10.3.tgz", "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", "dev": true, "requires": { @@ -3370,7 +3376,7 @@ }, "stream-combiner": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "resolved": "http://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", "integrity": "sha1-rsjLrBd7Vrb0+kec7YwZEs7lKFg=", "dev": true, "requires": { @@ -3716,7 +3722,7 @@ }, "event-stream": { "version": "3.3.4", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", + "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", "dev": true, "requires": { @@ -6204,7 +6210,7 @@ }, "map-stream": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", + "resolved": "http://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", "dev": true }, @@ -6393,7 +6399,7 @@ }, "minimist": { "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", "dev": true }, @@ -6544,7 +6550,7 @@ "dependencies": { "commander": { "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", "dev": true }, @@ -7151,6 +7157,11 @@ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, + "os-utils": { + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/os-utils/-/os-utils-0.0.14.tgz", + "integrity": "sha1-KeURaXsZgrjGJ3Ihdf45eX72QVY=" + }, "osenv": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", @@ -7705,7 +7716,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", @@ -8584,7 +8595,7 @@ }, "split": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", + "resolved": "http://registry.npmjs.org/split/-/split-0.3.3.tgz", "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", "dev": true, "requires": { @@ -8692,7 +8703,7 @@ }, "stream-combiner": { "version": "0.0.4", - "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", + "resolved": "http://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz", "integrity": "sha1-TV5DPBhSYd3mI8o/RMWGvPXErRQ=", "dev": true, "requires": { @@ -8842,6 +8853,11 @@ "has-flag": "^3.0.0" } }, + "systeminformation": { + "version": "4.27.3", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-4.27.3.tgz", + "integrity": "sha512-0Nc8AYEK818h7FI+bbe/kj7xXsMD5zOHvO9alUqQH/G4MHXu5tHQfWqC/bzWOk4JtoQPhnyLgxMYncDA2eeSBw==" + }, "tapable": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.1.tgz", diff --git a/package.json b/package.json index 31ebcbaf..d5969e00 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,10 @@ "dependencies": { "dbus": "^1.0.7", "mdns-resolver": "^1.0.0", + "os-utils": "0.0.14", "semver": "^7.3.2", - "sqlite3": "^4.1.1" + "sqlite3": "^4.1.1", + "systeminformation": "^4.27.3" }, "engines": { "node": "^12.16.2", @@ -56,6 +58,7 @@ "@types/morgan": "^1.9.0", "@types/mz": "0.0.32", "@types/node": "^12.12.54", + "@types/os-utils": "0.0.1", "@types/request": "^2.48.5", "@types/rimraf": "^2.0.4", "@types/rwlock": "^5.0.2", diff --git a/src/device-state/current-state.ts b/src/device-state/current-state.ts index afcd00ee..a535d772 100644 --- a/src/device-state/current-state.ts +++ b/src/device-state/current-state.ts @@ -11,6 +11,8 @@ import DeviceState from '../device-state'; import { CoreOptions } from 'request'; import * as url from 'url'; +import * as sysInfo from '../lib/system-info'; + // The exponential backoff starts at 15s const MINIMUM_BACKOFF_DELAY = 15000; @@ -121,6 +123,39 @@ const getStateDiff = (): DeviceStatus => { .value(), }; + const shouldReportSysInfo = (type: string, past: number, now: number) => { + if (!past) { + return true; + } + // TODO: Deduplicate this code + switch (type) { + case 'cpu_usage': + // The bucket size of cpu usage is 20 + return Math.floor(past / 20) !== Math.floor(now / 20); + case 'cpu_temp': + return Math.floor(past / 5) !== Math.floor(now / 5); + case 'memory_usage': + return Math.floor(past / 10) !== Math.floor(now / 10); + } + return true; + }; + + const toOmit: string[] = []; + _.each(diff.local, (value, key) => { + // if we have some system information that has changed, we check that it's + // within a certain range before reporting + if ( + !shouldReportSysInfo( + key, + (lastReportedLocal as any)[key], + value as number, + ) + ) { + toOmit.push(key); + } + }); + + diff.local = _.omit(diff.local, toOmit); return _.omitBy(diff, _.isEmpty); }; @@ -171,8 +206,16 @@ const reportCurrentState = (): null => { reportPending = true; try { const currentDeviceState = await deviceState.getStatus(); - _.assign(stateForReport.local, currentDeviceState.local); - _.assign(stateForReport.dependent, currentDeviceState.dependent); + const info = await sysInfo.getSysInfoToReport(); + stateForReport.local = { + ...stateForReport.local, + ...currentDeviceState.local, + ...info, + }; + stateForReport.dependent = { + ...stateForReport.dependent, + ...currentDeviceState.dependent, + }; const stateDiff = getStateDiff(); if (_.size(stateDiff) === 0) { diff --git a/src/lib/system-info.ts b/src/lib/system-info.ts new file mode 100644 index 00000000..716737bc --- /dev/null +++ b/src/lib/system-info.ts @@ -0,0 +1,113 @@ +import * as systeminformation from 'systeminformation'; +import * as osUtils from 'os-utils'; +import { fs, child_process } from 'mz'; + +export function getCpuUsage() { + return new Promise((resolve) => { + osUtils.cpuUsage((percent) => { + resolve(Math.round(percent * 100)); + }); + }); +} + +const blockDeviceRegex = /(\/dev\/.*)p\d+/; +export async function getStorageInfo() { + const fsInfo = await systeminformation.fsSize(); + let mainFs: string | undefined; + let total = 0; + // First we find the block device which the data partition is part of + for (const partition of fsInfo) { + if (partition.mount === '/data') { + const match = partition.fs.match(blockDeviceRegex); + if (match == null) { + mainFs = undefined; + } else { + mainFs = match[1]; + total = partition.size; + } + break; + } + } + + if (!mainFs) { + return { + blockDevice: '', + storageUsed: undefined, + storageTotal: undefined, + }; + } + + let used = 0; + for (const partition of fsInfo) { + if (partition.fs.startsWith(mainFs)) { + used += partition.used; + } + } + + return { + blockDevice: mainFs, + storageUsed: bytesToMb(used), + storageTotal: bytesToMb(total), + }; +} + +export async function getMemoryInformation() { + const mem = await systeminformation.mem(); + return { + used: bytesToMb(mem.used), + total: bytesToMb(mem.total), + }; +} + +export async function getCpuTemp() { + return Math.round((await systeminformation.cpuTemperature()).main); +} + +export async function getCpuId() { + // Read /proc/device-tree/serial-number + // if it's not there, return undefined + try { + const buffer = await fs.readFile('/proc/device-tree/serial-number'); + // Remove the null byte at the end + return buffer.toString('utf-8').substr(0, buffer.length - 2); + } catch { + return undefined; + } +} + +const undervoltageRegex = /under.*voltage/; +export async function undervoltageDetected() { + try { + const [dmesgStdout] = await child_process.exec('dmesg'); + return undervoltageRegex.test(dmesgStdout.toString()); + } catch { + return false; + } +} + +export async function getSysInfoToReport() { + const [cpu, mem, temp, cpuid, storage, undervoltage] = await Promise.all([ + getCpuUsage(), + getMemoryInformation(), + getCpuTemp(), + getCpuId(), + getStorageInfo(), + undervoltageDetected(), + ]); + + return { + cpu_usage: cpu, + memory_usage: mem.used, + memory_total: mem.total, + storage_usage: storage.storageUsed, + storage_total: storage.storageTotal, + storage_block_device: storage.blockDevice, + cpu_temp: temp, + cpu_id: cpuid, + is_undervolted: undervoltage, + }; +} + +function bytesToMb(bytes: number) { + return Math.floor(bytes / 1024 / 1024); +}