diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index 97ae6fd..b1e9d4d 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -1,28 +1,62 @@ -# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions - -name: Node.js CI +# Compiles the extension, lints it, and packages a VSIX to prove the published +# artifact would actually work. +name: CI on: push: branches: [ master ] pull_request: branches: [ master ] + workflow_dispatch: jobs: build: - runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - node-version: [10.x, 12.x] + node-version: [ 22.x, 24.x ] steps: - - uses: actions/checkout@v2 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - run: npm ci - - run: tsc -p $GITHUB_WORKSPACE + - uses: actions/checkout@v5 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v5 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - run: npm ci + + - name: Compile + run: npm run compile + + - name: Lint + run: npm run lint + + - name: Test + run: npm test + + - name: Package extension + run: npx --yes @vscode/vsce package --out system-vitals.vsix + + # systeminformation was once listed as both a dependency and a + # devDependency, so vsce pruned it and shipped a VSIX with no + # node_modules at all. That build compiled, linted and packaged + # cleanly, and failed at activation on require('systeminformation'). + # Only inspecting the artifact catches it. + - name: Verify the runtime dependency is bundled + run: | + if ! unzip -l system-vitals.vsix | grep -q 'node_modules/systeminformation/lib/'; then + echo "systeminformation is missing from the VSIX: the extension would fail on activation" + unzip -l system-vitals.vsix + exit 1 + fi + + - name: Upload VSIX + if: matrix.node-version == '22.x' + uses: actions/upload-artifact@v5 + with: + name: system-vitals-vsix + path: system-vitals.vsix diff --git a/.vscodeignore b/.vscodeignore index ab83bbc..d346de8 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,5 +1,7 @@ .vscode/** .vscode-test/** +.claude/** +.github/** out/test/** out/**/*.map src/** diff --git a/CHANGELOG.md b/CHANGELOG.md index fdec48c..b98b5f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,39 @@ # Change Log -## [1.0.7] +## [1.0.0] + +First release of System Vitals, forked from [resmon](https://github.com/Njanderson/resmon) 1.0.7. + +- Settings moved from the `resmon.*` namespace to `systemvitals.*`. If you are coming from resmon, your existing settings will not carry over and need to be set again. This keeps the two extensions independent when both are installed, rather than sharing one set of keys. +- Added GPU utilization and GPU memory monitoring on macOS, including Apple Silicon (M-series). Statistics are read from the IOKit registry via `ioreg`, which needs no elevated privileges. Both metrics hide themselves automatically on machines that do not report GPU statistics. +- Fixed `systeminformation` being pruned from the packaged extension, which produced a build that failed at activation once installed. +- Changed activation from `*` to `onStartupFinished`, so the extension no longer delays editor startup. This raises the minimum VS Code version to 1.53. +- Modernized the build: TypeScript 5, `@types/vscode` in place of the deprecated `vscode` module, and no `postinstall` hook. + +## Inherited history + +Releases below are from the upstream resmon project, retained for context. + +### [1.0.7] - Changed underlying CPU frequency API, added hiding battery/CPU temp information if the device lacks a battery/doesn't support CPU temp sensing, added some clarifications about CPU frequency behavior on Windows. -## [1.0.6] +### [1.0.6] - Added DiskSpace, CPU Temperature. Adjusted battery icon. -## [1.0.5] +### [1.0.5] - Refactored code heavily, addressed Github issue with memory.used versus memory.active. -## [1.0.4] +### [1.0.4] - Added icon for store. -## [1.0.3] +### [1.0.3] - Changed icons. Added choosable units. -## [1.0.2] +### [1.0.2] - Actually properly added systeminformation as a real dependency. -## [1.0.1] +### [1.0.1] - Properly added systeminformation as a real dependency -## [1.0.0] -- Initial release \ No newline at end of file +### [1.0.0] +- Initial release diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..ece8b0c --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +# License + +System Vitals is a fork of [resmon](https://github.com/Njanderson/resmon) by Nicholas Anderson, published to the VS Code Marketplace as `mutantdino.resourcemonitor`. + +## Status of the upstream work + +At the time this fork was made, the upstream project declared **no license**: it carries no `LICENSE` file, no `license` field in its `package.json`, and GitHub reports no detected license for the repository. Under default copyright, that means the original author retains all rights in the portions of this project that originate upstream, and grants no redistribution or modification rights. + +This file records that situation honestly rather than asserting a license over work this fork does not own. + +## Modifications + +Changes made in this fork — including the macOS GPU monitoring support in `src/appleGpu.ts` and the related additions to `src/extension.ts` — are copyright © 2026 Sachin Sachdeva. + +## What this means + +Until the upstream project adopts an explicit license, the redistribution terms for this project as a whole are unresolved. Anyone intending to redistribute it, or to rely on it in a context where licensing matters, should treat that as an open question. + +If you are the author of the upstream project and would like this fork changed, renamed, or taken down, please open an issue at +. diff --git a/README.md b/README.md index af53ce1..f5ecb97 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,63 @@ -# Resource Monitor +# System Vitals -## Features - -Display CPU frequency, usage, memory consumption, and battery percentage remaining within the VSCode status bar. - -## Screenshots - -![Disk space feature](images/disk_space_screenshot.png). - -## Requirements - -Just the system information node module. +Display CPU frequency and usage, GPU utilization, memory consumption, disk space, and battery percentage in the VS Code status bar — including **native Apple Silicon GPU monitoring**, which needs no elevated privileges. -## Extension Settings +> **A fork.** System Vitals is a fork of [resmon](https://github.com/Njanderson/resmon) by Nicholas Anderson, adding macOS GPU support and a modernized build. It is not affiliated with or endorsed by the original author. See [LICENSE.md](LICENSE.md) for the licensing situation, which is unresolved because the upstream project declares no license. -- `resmon.show.cpuusage`: Show CPU Usage. In Windows, this percentage is calculated with processor time, which doesn't quite match the task manager figure. -- `resmon.show.cpufreq`: Show CPU Frequency. This may just display a static frequency on Windows. -- `resmon.show.mem`: Show consumed and total memory as a fraction. -- `resmon.show.battery`: Show battery percentage remaining. -- `resmon.show.disk`: Show disk space information. -- `resmon.show.cputemp`: Show CPU temperature. May not work without the lm-sensors module on Linux. May require running VS Code as admin on Windows. -- `resmon.disk.format`: Configures how the disk space is displayed (percentage remaining/used, absolute remaining, used out of totel). -- `resmon.disk.drives`: Drives to show. For example, 'C:' on Windows, and '/dev/sda1' on Linux. -- `resmon.updatefrequencyms`: How frequently to query systeminformation. The minimum is 200 ms as to prevent accidentally updating so fast as to freeze up your machine. -- `resmon.freq.unit`: Unit used for the CPU frequency (GHz-Hz). -- `resmon.mem.unit`: Unit used for the RAM consumption (GB-B). -- `resmon.alignLeft`: Toggles the alignment of the status bar. -- `resmon.color`: Color of the status bar text in hex code (for example, #FFFFFF is white). The color must be in the format #RRGGBB, using hex digits. - -## Known Issues +## Features -A better solution for Windows CPU Usage would be great. I investigated alternatives to counting Processor Time, but none of them seemed to match the Task Manager percentage. +- **CPU** — usage percentage and current frequency +- **GPU** — utilization and memory in use (macOS, including M-series Apple Silicon) +- **Memory** — consumed out of total +- **Disk** — space remaining or used, per drive +- **Battery** — percentage remaining, hidden automatically on devices without a battery ---- +## Screenshot -## Change Log +![System Vitals in the VS Code status bar](images/statusbar.png) -### [1.0.7] -- Changed underlying CPU frequency API, added hiding battery/CPU temp information if the device lacks a battery/doesn't support CPU temp sensing, added some clarifications about CPU frequency behavior on Windows. +CPU usage, CPU frequency, GPU utilization and memory, running on an Apple Silicon Mac. GPU memory, disk space and CPU temperature are available as well, and are off by default. -### [1.0.6] +## Requirements -- Added DiskSpace, CPU Temperature. Adjusted battery icon. +None beyond VS Code 1.53 or newer. The `systeminformation` module is bundled with the extension, and macOS GPU statistics are read from the IOKit registry with `ioreg`, which requires no additional software and no elevated privileges. -### [1.0.5] +## Extension Settings -- Refactored code heavily, addressed Github issue with memory.used versus memory.active. +- `systemvitals.show.cpuusage`: Show CPU Usage. In Windows, this percentage is calculated with processor time, which doesn't quite match the task manager figure. +- `systemvitals.show.cpufreq`: Show CPU Frequency. This may just display a static frequency on Windows. +- `systemvitals.show.mem`: Show consumed and total memory as a fraction. +- `systemvitals.show.battery`: Show battery percentage remaining. +- `systemvitals.show.disk`: Show disk space information. +- `systemvitals.show.cputemp`: Show CPU temperature. May not work without the lm-sensors module on Linux. May require running VS Code as admin on Windows. +- `systemvitals.show.gpu`: Show GPU utilization. macOS only; see GPU Monitoring below. +- `systemvitals.show.gpumem`: Show GPU memory in use. macOS only; see GPU Monitoring below. +- `systemvitals.gpu.unit`: Unit used for GPU memory (GB-B). +- `systemvitals.disk.format`: Configures how the disk space is displayed (percentage remaining/used, absolute remaining, used out of totel). +- `systemvitals.disk.drives`: Drives to show. For example, 'C:' on Windows, and '/dev/sda1' on Linux. +- `systemvitals.updatefrequencyms`: How frequently to query systeminformation. The minimum is 200 ms as to prevent accidentally updating so fast as to freeze up your machine. +- `systemvitals.freq.unit`: Unit used for the CPU frequency (GHz-Hz). +- `systemvitals.mem.unit`: Unit used for the RAM consumption (GB-B). +- `systemvitals.alignLeft`: Toggles the alignment of the status bar. +- `systemvitals.color`: Color of the status bar text in hex code (for example, #FFFFFF is white). The color must be in the format #RRGGBB, using hex digits. -### [1.0.4] +## GPU Monitoring -- Added icon for store. +GPU statistics are **macOS only**, and work on Apple Silicon (M-series) as well as Intel Macs. They are read from the IOKit registry with `ioreg`, which requires no elevated privileges — unlike `powermetrics`, which needs `sudo`. On any machine that does not report GPU statistics, both GPU metrics hide themselves automatically rather than showing an error. -### [1.0.3] +- `systemvitals.show.gpu` displays GPU utilization as a percentage. This is the driver's instantaneous `Device Utilization %`, sampled at each update rather than averaged over the interval, so expect it to fluctuate the same way CPU usage does. +- `systemvitals.show.gpumem` displays GPU memory as *in use / allocated*. Note that this is **not** used-out-of-VRAM: Apple Silicon uses unified memory with no fixed GPU partition, so the second figure is how much system memory the GPU driver has currently claimed, not a fixed capacity. It moves over time. -- Changed icons. Added choosable units. +GPU frequency and GPU power are not available, as both require `sudo powermetrics` or private APIs. -### [1.0.2] +Linux and Windows GPU monitoring is not implemented; each would need a separate backend (`nvidia-smi` / `/sys/class/drm`, and WMI / NVML respectively). -- Actually properly added systeminformation as a real dependency. +## Known Issues -### [1.0.1] +A better solution for Windows CPU Usage would be great. I investigated alternatives to counting Processor Time, but none of them seemed to match the Task Manager percentage. -- Properly added systeminformation as a real dependency +--- -### [1.0.0] +## Change Log -- Initial release +See [CHANGELOG.md](CHANGELOG.md). diff --git a/images/disk_space_screenshot.png b/images/disk_space_screenshot.png deleted file mode 100644 index 20d495a..0000000 Binary files a/images/disk_space_screenshot.png and /dev/null differ diff --git a/images/example.png b/images/example.png deleted file mode 100644 index 7a37df0..0000000 Binary files a/images/example.png and /dev/null differ diff --git a/images/icon.png b/images/icon.png index 10000b2..db224e6 100644 Binary files a/images/icon.png and b/images/icon.png differ diff --git a/images/statusbar.png b/images/statusbar.png new file mode 100644 index 0000000..1a759cd Binary files /dev/null and b/images/statusbar.png differ diff --git a/package-lock.json b/package-lock.json index 1742e70..b5b7e68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,813 +1,389 @@ { - "name": "resourcemonitor", - "version": "1.0.7", - "lockfileVersion": 1, + "name": "system-vitals", + "version": "1.0.0", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@babel/code-frame": { + "packages": { + "": { + "name": "system-vitals", + "version": "1.0.0", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "systeminformation": "^4.27.11" + }, + "devDependencies": { + "@types/node": "^20.19.43", + "@types/vscode": "1.53.0", + "tslint": "^5.8.0", + "typescript": "^5.9.3" + }, + "engines": { + "vscode": "^1.53.0" + } + }, + "node_modules/@babel/code-frame": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz", "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==", "dev": true, - "requires": { + "dependencies": { "@babel/highlight": "^7.8.3" } }, - "@babel/helper-validator-identifier": { + "node_modules/@babel/helper-validator-identifier": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.0.tgz", "integrity": "sha512-6G8bQKjOh+of4PV/ThDm/rRqlU7+IGoJuofpagU5GlEl29Vv0RGqqt86ZGRV8ZuSOY3o+8yXl5y782SMcG7SHw==", "dev": true }, - "@babel/highlight": { + "node_modules/@babel/highlight": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", "js-tokens": "^4.0.0" } }, - "@types/mocha": { - "version": "2.2.48", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-2.2.48.tgz", - "integrity": "sha512-nlK/iyETgafGli8Zh9zJVCTicvU3iajSkRwOh3Hhiva598CMqNJ4NcVCGMTGKpGpTYj/9R8RLzS9NAykSSCqGw==", - "dev": true - }, - "@types/node": { - "version": "7.10.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-7.10.9.tgz", - "integrity": "sha512-usSpgoUsRtO5xNV5YEPU8PPnHisFx8u0rokj1BPVn/hDF7zwUDzVLiuKZM38B7z8V2111Fj6kd4rGtQFUZpNOw==", - "dev": true - }, - "agent-base": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", - "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", "dev": true, - "requires": { - "es6-promisify": "^5.0.0" + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" } }, - "ajv": { - "version": "6.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.0.tgz", - "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", + "node_modules/@types/vscode": { + "version": "1.53.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.53.0.tgz", + "integrity": "sha512-XjFWbSPOM0EKIT2XhhYm3D3cx3nn3lshMUcWNy1eqefk+oqRuBq8unVb6BYIZqXy9lQZyeUl7eaBCOZWv+LcXQ==", "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } + "license": "MIT" }, - "ansi-styles": { + "node_modules/ansi-styles": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, - "requires": { + "dependencies": { "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" } }, - "argparse": { + "node_modules/argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "requires": { + "dependencies": { "sprintf-js": "~1.0.2" } }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz", - "integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==", - "dev": true - }, - "balanced-match": { + "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", "dev": true }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "brace-expansion": { + "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "requires": { + "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "builtin-modules": { + "node_modules/builtin-modules": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "chalk": { + "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, - "requires": { + "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "color-convert": { + "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "dev": true, - "requires": { + "dependencies": { "color-name": "1.1.3" } }, - "color-name": { + "node_modules/color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { + "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, - "concat-map": { + "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "diff": { + "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "dev": true - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "dev": true - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", "dev": true, - "requires": { - "es6-promise": "^4.0.3" + "engines": { + "node": ">=0.3.1" } }, - "escape-string-regexp": { + "node_modules/escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.8.0" + } }, - "esprima": { + "node_modules/esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", - "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", - "dev": true - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" } }, - "fs.realpath": { + "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { + "node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "requires": { + "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.0.4", "once": "^1.3.0", "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has-flag": { + "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", - "dev": true, - "requires": { - "agent-base": "4", - "debug": "3.1.0" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-proxy-agent": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", - "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", "dev": true, - "requires": { - "agent-base": "^4.3.0", - "debug": "^3.1.0" + "engines": { + "node": ">=4" } }, - "inflight": { + "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, - "requires": { + "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, - "inherits": { + "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "js-tokens": { + "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, - "js-yaml": { + "node_modules/js-yaml": { "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", "dev": true, - "requires": { + "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "mime-db": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", - "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", - "dev": true - }, - "mime-types": { - "version": "2.1.26", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", - "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", - "dev": true, - "requires": { - "mime-db": "1.43.0" - } - }, - "minimatch": { + "node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "requires": { + "dependencies": { "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, - "minimist": { + "node_modules/minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, - "mkdirp": { + "node_modules/mkdirp": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.4.tgz", "integrity": "sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", "dev": true, - "requires": { + "dependencies": { "minimist": "^1.2.5" - } - }, - "mocha": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.2.0.tgz", - "integrity": "sha512-2IUgKDhc3J7Uug+FxMXuqIyYzH7gJjXECKe/w43IGgQHTSj3InJi+yAA7T24L9bQMRKiUEHxEX37G5JpVUGLcQ==", - "dev": true, - "requires": { - "browser-stdout": "1.3.1", - "commander": "2.15.1", - "debug": "3.1.0", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.2", - "growl": "1.10.5", - "he": "1.1.1", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "supports-color": "5.4.0" }, - "dependencies": { - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "supports-color": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.4.0.tgz", - "integrity": "sha512-zjaXglF5nnWpsq470jSv6P9DwPvgLkuapYmfDm3JWOm0vkNTVF2tI4UrN2r6jH1qM/uc/WtxYY1hYoA2dOKj5w==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } + "bin": { + "mkdirp": "bin/cmd.js" } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "once": { + "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "requires": { + "dependencies": { "wrappy": "1" } }, - "path-is-absolute": { + "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "path-parse": { + "node_modules/path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "dev": true }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "dev": true - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "querystringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", - "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", - "dev": true - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", - "dev": true - }, - "resolve": { + "node_modules/resolve": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.1.tgz", "integrity": "sha512-84oo6ZTtoTUpjgNEr5SJyzQhzL72gaRodsSfyxC/AXRvwu0Yse9H8eF9IpGo7b8YetZhlI6v7ZQ6bKBFV/6S7w==", "dev": true, - "requires": { + "dependencies": { "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "safe-buffer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", - "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", - "dev": true - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "semver": { + "node_modules/semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-support": { - "version": "0.5.16", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", - "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "bin": { + "semver": "bin/semver" } }, - "sprintf-js": { + "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "supports-color": { + "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, - "requires": { + "dependencies": { "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "systeminformation": { + "node_modules/systeminformation": { "version": "4.27.11", "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-4.27.11.tgz", "integrity": "sha512-U7bigXbOnsB8k1vNHS0Y13RCsRz5/UohiUmND+3mMUL6vfzrpbe/h4ZqewowB+B+tJNnmGFDj08Z8xGfYo45dQ==", - "dev": true - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=4.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" } }, - "tslib": { + "node_modules/tslib": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.11.1.tgz", "integrity": "sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA==", "dev": true }, - "tslint": { + "node_modules/tslint": { "version": "5.20.1", "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.20.1.tgz", "integrity": "sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==", "dev": true, - "requires": { + "dependencies": { "@babel/code-frame": "^7.0.0", "builtin-modules": "^1.1.1", "chalk": "^2.3.0", @@ -821,100 +397,51 @@ "semver": "^5.3.0", "tslib": "^1.8.0", "tsutils": "^2.29.0" + }, + "bin": { + "tslint": "bin/tslint" + }, + "engines": { + "node": ">=4.8.0" + }, + "peerDependencies": { + "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev" } }, - "tsutils": { + "node_modules/tsutils": { "version": "2.29.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", "dev": true, - "requires": { + "dependencies": { "tslib": "^1.8.1" + }, + "peerDependencies": { + "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" } }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true - }, - "typescript": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", - "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "url-parse": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", - "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", - "dev": true, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "vscode": { - "version": "1.1.36", - "resolved": "https://registry.npmjs.org/vscode/-/vscode-1.1.36.tgz", - "integrity": "sha512-cGFh9jmGLcTapCpPCKvn8aG/j9zVQ+0x5hzYJq5h5YyUXVGa1iamOaB2M2PZXoumQPES4qeAP1FwkI0b6tL4bQ==", + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, - "requires": { - "glob": "^7.1.2", - "mocha": "^5.2.0", - "request": "^2.88.0", - "semver": "^5.4.1", - "source-map-support": "^0.5.0", - "url-parse": "^1.4.4", - "vscode-test": "^0.4.1" + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" } }, - "vscode-test": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-0.4.3.tgz", - "integrity": "sha512-EkMGqBSefZH2MgW65nY05rdRSko15uvzq4VAPM5jVmwYuFQKE7eikKXNJDRxL+OITXHB6pI+a3XqqD32Y3KC5w==", + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, - "requires": { - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.1" - } + "license": "MIT" }, - "wrappy": { + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", diff --git a/package.json b/package.json index ade4438..3b37531 100644 --- a/package.json +++ b/package.json @@ -1,60 +1,89 @@ { - "name": "resourcemonitor", - "displayName": "Resource Monitor", - "description": "Displays current CPU stats, memory/disk consumption, and battery percentage remaining.", - "version": "1.0.7", - "publisher": "mutantdino", + "name": "system-vitals", + "displayName": "System Vitals", + "description": "CPU, GPU, memory, disk and battery monitoring in the VS Code status bar, with native Apple Silicon GPU support.", + "version": "1.0.0", + "publisher": "sachinsachdeva", + "license": "SEE LICENSE IN LICENSE.md", "engines": { - "vscode": "^1.25.0" + "vscode": "^1.53.0" }, "categories": [ "Other" ], + "keywords": [ + "monitor", + "cpu", + "gpu", + "memory", + "apple silicon" + ], "icon": "images/icon.png", "galleryBanner": { - "color": "#008080", + "color": "#0F172A", "theme": "dark" }, "activationEvents": [ - "*" + "onStartupFinished" ], "main": "./out/extension", "contributes": { "configuration": { "type": "object", - "title": "Resource Monitor", + "title": "System Vitals", "properties": { - "resmon.show.cpuusage": { + "systemvitals.show.cpuusage": { "type": "boolean", "default": true, "description": "Toggles the display of CPU Usage." }, - "resmon.show.cpufreq": { + "systemvitals.show.cpufreq": { "type": "boolean", "default": true, "description": "Toggles the display of CPU frequency. For Windows, this displays the maximum clock speed." }, - "resmon.show.mem": { + "systemvitals.show.mem": { "type": "boolean", "default": true, "description": "Toggles the display of memory consumption." }, - "resmon.show.battery": { + "systemvitals.show.battery": { "type": "boolean", "default": true, "description": "Toggles the display of battery percentage remaining if a device with a battery. If the device reports not having a battery, this setting will always be treated as false." }, - "resmon.show.disk": { + "systemvitals.show.disk": { "type": "boolean", "default": false, "description": "Toggles the display of disk space information." }, - "resmon.show.cputemp": { + "systemvitals.show.cputemp": { "type": "boolean", "default": false, "description": "Toggles the display of CPU temperature. May not work without the lm-sensors module on Linux. May require running VS Code as admin on Windows" }, - "resmon.disk.format": { + "systemvitals.show.gpu": { + "type": "boolean", + "default": true, + "description": "Toggles the display of GPU utilization. macOS only; automatically hidden on machines that do not report GPU statistics." + }, + "systemvitals.show.gpumem": { + "type": "boolean", + "default": false, + "description": "Toggles the display of GPU memory in use. macOS only; automatically hidden on machines that do not report GPU statistics." + }, + "systemvitals.gpu.unit": { + "type": "string", + "enum": [ + "GB", + "MB", + "KB", + "B" + ], + "default": "GB", + "description": "Unit used for GPU memory." + }, + "systemvitals.disk.format": { "type": "string", "enum": [ "PercentRemaining", @@ -64,7 +93,7 @@ ], "description": "Configures how the disk space is displayed (percentage remaining/used, absolute remaining, used out of totel)." }, - "resmon.disk.drives": { + "systemvitals.disk.drives": { "type": "array", "items": { "type": "string" @@ -73,13 +102,13 @@ "default": [], "description": "Drives to show. For example, 'C:' on Windows, and '/dev/sda1' on Linux." }, - "resmon.updatefrequencyms": { + "systemvitals.updatefrequencyms": { "type": "number", "default": 2000, "minimum": 200, "description": "How long in ms between updates to resources." }, - "resmon.freq.unit": { + "systemvitals.freq.unit": { "type": "string", "enum": [ "GHz", @@ -89,7 +118,7 @@ ], "default": "GHz" }, - "resmon.mem.unit": { + "systemvitals.mem.unit": { "type": "string", "enum": [ "GB", @@ -99,19 +128,19 @@ ], "default": "GB" }, - "resmon.show.precision": { + "systemvitals.show.precision": { "type": "number", "default": 2, "minimum": 0, "maximum": 2, - "description": "How long in ms between updates to resources." + "description": "Number of decimal places shown for each resource." }, - "resmon.alignLeft": { + "systemvitals.alignLeft": { "type": "boolean", "default": true, "description": "Toggles the alignment of the status bar." }, - "resmon.color": { + "systemvitals.color": { "type": "string", "default": "#FFFFFF", "description": "Color of the status bar text in hex code (for example, #FFFFFF is white). The color must be in the format #RRGGBB, using hex digits." @@ -120,23 +149,27 @@ } }, "repository": { - "url": "https://github.com/Njanderson/resmon.git" + "type": "git", + "url": "https://github.com/sachinsachdeva/resmon.git" + }, + "bugs": { + "url": "https://github.com/sachinsachdeva/resmon/issues" }, + "homepage": "https://github.com/sachinsachdeva/resmon#readme", "scripts": { "vscode:prepublish": "npm run compile", "compile": "tsc -p ./", - "postinstall": "node ./node_modules/vscode/bin/install", + "lint": "tslint -c tslint.json 'src/**/*.ts'", + "test": "npm run compile && node --test out/test/*.test.js", "watch": "tsc -watch -p ./" }, "dependencies": { "systeminformation": "^4.27.11" }, "devDependencies": { - "systeminformation": "^4.27.11", - "@types/mocha": "^2.2.42", - "@types/node": "^7.0.43", + "@types/node": "^20.19.43", + "@types/vscode": "1.53.0", "tslint": "^5.8.0", - "typescript": "^2.6.1", - "vscode": "^1.1.6" + "typescript": "^5.9.3" } } diff --git a/src/appleGpu.ts b/src/appleGpu.ts new file mode 100644 index 0000000..39bb32c --- /dev/null +++ b/src/appleGpu.ts @@ -0,0 +1,155 @@ +'use strict'; + +import { execFile } from 'child_process'; + +/** + * GPU statistics scraped from the IOKit registry on macOS. + * + * On Apple Silicon there is no fixed VRAM partition: the GPU shares system + * memory, so the two memory figures are "currently mapped" and "claimed from + * the system by the driver" rather than used/total of a dedicated pool. + */ +export interface GpuStats { + /** "Device Utilization %", 0-100. An instantaneous snapshot, not an interval average. */ + utilization: number; + /** "In use system memory", in bytes. */ + inUseMemory: number; + /** "Alloc system memory", in bytes. */ + allocatedMemory: number; +} + +// Recurse one level from each IOAccelerator node so we pick up its properties, +// with unlimited line width so the PerformanceStatistics dict isn't wrapped. +const IOREG_ARGS: string[] = ['-r', '-d', '1', '-w', '0', '-c', 'IOAccelerator']; + +// The whole registry dump for one node is ~46 KB on an M4; allow generous room +// for machines that expose several accelerators. +const IOREG_MAX_BUFFER: number = 4 * 1024 * 1024; + +// Two resources each ask whether they are shown and then what to display, so a +// single tick reads the sampler four times. Collapsing those into one ioreg +// invocation keeps the cost at ~20ms per tick. The 200ms floor on +// systemvitals.updatefrequencyms guarantees this window never spans two ticks. +const CACHE_WINDOW_MS: number = 100; + +const PERFORMANCE_STATISTICS_PATTERN = /"PerformanceStatistics" = \{([^}]*)\}/; + +/** + * Extracts GPU statistics from the output of + * `ioreg -r -d 1 -w 0 -c IOAccelerator`. + * + * Pure: takes text, returns data, never touches the system. Returns null if + * the output holds no readable statistics, which is how this feature detects + * that a machine cannot report GPU utilization. + */ +export function parsePerformanceStatistics(ioregOutput: string): GpuStats | null { + // On a multi-GPU Mac ioreg emits several nodes; read the first one that + // carries statistics. + let statisticsBlock = PERFORMANCE_STATISTICS_PATTERN.exec(ioregOutput); + if (statisticsBlock === null) { + return null; + } + let statistics: string = statisticsBlock[1]; + + // Utilization is the one value the feature cannot do without. + let utilization: number | null = readStatistic(statistics, "Device Utilization %"); + if (utilization === null) { + return null; + } + + return { + utilization: utilization, + inUseMemory: readStatistic(statistics, "In use system memory") || 0, + allocatedMemory: readStatistic(statistics, "Alloc system memory") || 0, + }; +} + +/** + * Reads a single `"key"=` entry out of a PerformanceStatistics dict. + * + * The closing quote in the pattern is load-bearing: without it "In use system + * memory" would also match its sibling "In use system memory (driver)", which + * is a different (and usually zero) value. + */ +function readStatistic(statistics: string, key: string): number | null { + let escapedKey: string = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + let match = new RegExp(`"${escapedKey}"=(\\d+)`).exec(statistics); + if (match === null) { + return null; + } + return parseInt(match[1], 10); +} + +/** + * Samples GPU statistics from the IOKit registry, caching briefly so that + * several resources can share one `ioreg` invocation per update tick. + * + * Reading IOAccelerator requires no elevated privileges, unlike powermetrics. + */ +export class AppleGpuSampler { + private _cachedStats: GpuStats | null; + private _cachedAt: number; + private _pending: Promise | null; + + constructor() { + this._cachedStats = null; + this._cachedAt = Number.NEGATIVE_INFINITY; + this._pending = null; + } + + /** + * Returns the current GPU statistics, or null when they are unavailable: + * off macOS, on a Mac whose driver exposes no statistics, or when ioreg + * fails. + * + * Never rejects. ResMon.update() gathers every resource with Promise.all + * and has no error handling, so a single rejection would break the update + * loop and freeze the whole status bar. + */ + public sample(): Promise { + if (process.platform !== 'darwin') { + return Promise.resolve(null); + } + + if (Date.now() - this._cachedAt < CACHE_WINDOW_MS) { + return Promise.resolve(this._cachedStats); + } + + // Fold callers that arrive mid-flight into the running invocation. + if (this._pending !== null) { + return this._pending; + } + + this._pending = this.readRegistry().then(stats => { + this._cachedStats = stats; + this._cachedAt = Date.now(); + this._pending = null; + return stats; + }); + + return this._pending; + } + + /** + * Runs ioreg and parses its output, resolving to null on any failure. + * + * execFile is wrapped by hand rather than with util.promisify because the + * pinned @types/node predates promisify's typings. + */ + private readRegistry(): Promise { + return new Promise(resolve => { + execFile('ioreg', IOREG_ARGS, { maxBuffer: IOREG_MAX_BUFFER }, (error, stdout) => { + if (error) { + resolve(null); + return; + } + + try { + resolve(parsePerformanceStatistics(stdout.toString())); + } catch (parseError) { + resolve(null); + } + }); + }); + } +} diff --git a/src/constants.ts b/src/constants.ts index b24f6b2..efef580 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -36,6 +36,15 @@ export var FreqMappings: UnitLookup = { "Hz": Units.None, }; +// Units[Units.G] yields "G", and Units[Units.None] yields "None", neither of +// which is a unit anyone wants to read in a status bar. +export var UnitLabels: { [unit: number]: string } = { + [Units.None]: "B", + [Units.K]: "KB", + [Units.M]: "MB", + [Units.G]: "GB", +}; + export var MemMappings: UnitLookup = { "GB": Units.G, "MB": Units.M, diff --git a/src/extension.ts b/src/extension.ts index 50791b9..302e519 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,7 @@ 'use strict'; -import { window, ExtensionContext, StatusBarAlignment, StatusBarItem, workspace, WorkspaceConfiguration } from 'vscode'; -import { Units, DiskSpaceFormat, DiskSpaceFormatMappings, FreqMappings, MemMappings } from './constants'; +import { window, Disposable, ExtensionContext, StatusBarAlignment, StatusBarItem, workspace, WorkspaceConfiguration } from 'vscode'; +import { Units, UnitLabels, DiskSpaceFormat, DiskSpaceFormatMappings, FreqMappings, MemMappings } from './constants'; +import { AppleGpuSampler } from './appleGpu'; var si = require('systeminformation'); @@ -11,6 +12,9 @@ export function activate(context: ExtensionContext) { } abstract class Resource { + // A WorkspaceConfiguration is an immutable snapshot, so this is replaced + // via setConfig() on every update tick. Holding the object handed over at + // construction would freeze every resource-level setting at activation. protected _config: WorkspaceConfiguration; protected _isShownByDefault: boolean; protected _configKey: string; @@ -23,6 +27,26 @@ abstract class Resource { this._maxWidth = 0; } + /** + * Points this resource at a freshly read configuration, so that changed + * settings apply on the next tick rather than at the next window reload. + */ + public setConfig(config: WorkspaceConfiguration) { + this._config = config; + } + + /** + * Clears the padding high water mark. + * + * _maxWidth only ever grows, which is what stops the status bar jittering + * as digits come and go. It does mean a setting that shortens the display, + * such as switching memory from B to GB, would otherwise leave a gap at the + * old width for the rest of the session. + */ + public resetWidth() { + this._maxWidth = 0; + } + public async getResourceDisplay(): Promise { if (await this.isShown()) { @@ -36,22 +60,22 @@ abstract class Resource { return null; } - protected async abstract getDisplay(): Promise; + protected abstract getDisplay(): Promise; protected async isShown(): Promise { - return Promise.resolve(this._config.get(`show.${this._configKey}`, false)); + return Promise.resolve(this._config.get(`show.${this._configKey}`, this._isShownByDefault)); } public getPrecision(): number { return this._config.get("show.precision", 2); } - protected convertBytesToLargestUnit(bytes: number, precision: number = 2): string { + protected convertBytesToLargestUnit(bytes: number): string { let unit: Units = Units.None; while (bytes/unit >= 1024 && unit < Units.G) { unit *= 1024; } - return `${(bytes/unit).toFixed(this.getPrecision())} ${Units[unit]}`; + return `${(bytes/unit).toFixed(this.getPrecision())} ${UnitLabels[unit]}`; } } @@ -71,14 +95,14 @@ class CpuUsage extends Resource { class CpuTemp extends Resource { constructor(config: WorkspaceConfiguration) { - super(config, true, "cputemp"); + super(config, false, "cputemp"); } protected async isShown(): Promise { // If the CPU temp sensor cannot retrieve a valid temperature, disallow its reporting. var cpuTemp = (await si.cpuTemperature()).main; let hasCpuTemp = cpuTemp !== -1; - return Promise.resolve(hasCpuTemp && this._config.get("show.cputemp", true)); + return hasCpuTemp && await super.isShown(); } async getDisplay(): Promise { @@ -107,15 +131,74 @@ class CpuFreq extends Resource { } } +/** + * Base for the macOS GPU metrics, which all come from one IOKit registry read. + * + * Availability is decided by whether the machine actually reports statistics + * rather than by checking the architecture, so this covers Apple Silicon and + * Intel Macs alike and stays hidden anywhere the data is missing. + */ +abstract class AppleGpuResource extends Resource { + protected _sampler: AppleGpuSampler; + + constructor(config: WorkspaceConfiguration, isShownByDefault: boolean, configKey: string, sampler: AppleGpuSampler) { + super(config, isShownByDefault, configKey); + this._sampler = sampler; + } + + protected async isShown(): Promise { + // The sampler caches briefly, so this read and the one in getDisplay() + // share a single ioreg invocation. + let stats = await this._sampler.sample(); + return stats !== null && await super.isShown(); + } +} + +class GpuUsage extends AppleGpuResource { + + constructor(config: WorkspaceConfiguration, sampler: AppleGpuSampler) { + super(config, true, "gpu", sampler); + } + + async getDisplay(): Promise { + let stats = await this._sampler.sample(); + if (stats === null) { + return ""; + } + return `$(circuit-board) ${(stats.utilization).toFixed(this.getPrecision())}%`; + } +} + +class GpuMemory extends AppleGpuResource { + + constructor(config: WorkspaceConfiguration, sampler: AppleGpuSampler) { + super(config, false, "gpumem", sampler); + } + + async getDisplay(): Promise { + let stats = await this._sampler.sample(); + if (stats === null) { + return ""; + } + let unit = this._config.get('gpu.unit', "GB"); + var memDivisor = MemMappings[unit]; + // Apple Silicon shares system memory with the CPU, so this is memory + // currently mapped out of memory the driver has claimed, not VRAM. + let inUseWithUnits = stats.inUseMemory / memDivisor; + let allocatedWithUnits = stats.allocatedMemory / memDivisor; + return `$(server) ${(inUseWithUnits).toFixed(this.getPrecision())}/${(allocatedWithUnits).toFixed(this.getPrecision())} ${unit}`; + } +} + class Battery extends Resource { constructor(config: WorkspaceConfiguration) { - super(config, false, "battery"); + super(config, true, "battery"); } protected async isShown(): Promise { let hasBattery = (await si.battery()).hasbattery; - return Promise.resolve(hasBattery && this._config.get("show.battery", false)); + return hasBattery && await super.isShown(); } async getDisplay(): Promise { @@ -132,7 +215,7 @@ class Memory extends Resource { } async getDisplay() : Promise { - let unit = this._config.get('memunit', "GB"); + let unit = this._config.get('mem.unit', "GB"); var memDivisor = MemMappings[unit]; let memoryData = await si.mem(); let memoryUsedWithUnits = memoryData.active / memDivisor; @@ -141,31 +224,6 @@ class Memory extends Resource { } } -class Network extends Resource { - - constructor(config: WorkspaceConfiguration) { - super(config, true, "net"); - - // Network stats are requested through returning the delta between - // multiple invocations - this.getInterfaceStats(); - } - - async getInterfaceStats() : Promise { - let networkInterfaces = await si.networkInterfaces(); - for (let networkInterface in networkInterfaces) { - console.log(networkInterface); - let networkStats = await si.networkStats(networkInterface); - console.log(networkStats); - } - } - - async getDisplay(): Promise { - // Not implemented - return ""; - } -} - class DiskSpace extends Resource { constructor(config: WorkspaceConfiguration) { @@ -225,24 +283,38 @@ class ResMon { private _delimiter: string; private _updating: boolean; private _resources: Resource[]; + private _configListener: Disposable; constructor() { - this._config = workspace.getConfiguration('resmon'); + this._config = workspace.getConfiguration('systemvitals'); this._delimiter = " "; this._updating = false; this._statusBarItem = window.createStatusBarItem(this._config.get('alignLeft') ? StatusBarAlignment.Left : StatusBarAlignment.Right); this._statusBarItem.color = this._getColor(); this._statusBarItem.show(); + // The GPU resources share one sampler so that a tick costs a single + // read of the IOKit registry rather than one per resource. + let gpuSampler = new AppleGpuSampler(); + // Add all resources to monitor this._resources = []; this._resources.push(new CpuUsage(this._config)); this._resources.push(new CpuFreq(this._config)); + this._resources.push(new GpuUsage(this._config, gpuSampler)); + this._resources.push(new GpuMemory(this._config, gpuSampler)); this._resources.push(new Battery(this._config)); this._resources.push(new Memory(this._config)); this._resources.push(new DiskSpace(this._config)); this._resources.push(new CpuTemp(this._config)); - this._resources.push(new Network(this._config)); + + // Settings that shorten a resource's display would otherwise leave the + // status bar padded out at the old width for the rest of the session. + this._configListener = workspace.onDidChangeConfiguration(event => { + if (event.affectsConfiguration('systemvitals')) { + this._resources.forEach(resource => resource.resetWidth()); + } + }); } public StartUpdating() { @@ -270,8 +342,12 @@ class ResMon { private async update() { if (this._updating) { - // Update the configuration in case it has changed - this._config = workspace.getConfiguration('resmon'); + // Update the configuration in case it has changed. A + // WorkspaceConfiguration is a snapshot, so the resources need to be + // pointed at the new one or they would keep reading activation-time + // values. + this._config = workspace.getConfiguration('systemvitals'); + this._resources.forEach(resource => resource.setConfig(this._config)); // Update the status bar item's styling let proposedAlignment = this._config.get('alignLeft') ? StatusBarAlignment.Left : StatusBarAlignment.Right; @@ -299,6 +375,7 @@ class ResMon { dispose() { this.StopUpdating(); + this._configListener.dispose(); this._statusBarItem.dispose(); } } diff --git a/src/test/appleGpu.test.ts b/src/test/appleGpu.test.ts new file mode 100644 index 0000000..a337e56 --- /dev/null +++ b/src/test/appleGpu.test.ts @@ -0,0 +1,91 @@ +import * as assert from 'assert'; +import { test } from 'node:test'; +import { parsePerformanceStatistics, AppleGpuSampler } from '../appleGpu'; + +// Trimmed from real `ioreg -r -d 1 -w 0 -c IOAccelerator` output on an Apple M4. +// The "In use system memory (driver)" sibling key is kept deliberately: it is +// the trap the parser has to avoid. +const M4_SAMPLE = `+-o AGXAcceleratorG16G + { + "IOMatchedAtBoot" = Yes + "PerformanceStatistics" = {"In use system memory (driver)"=0,"Alloc system memory"=2181693440,"Tiler Utilization %"=30,"recoveryCount"=0,"Renderer Utilization %"=29,"Device Utilization %"=30,"In use system memory"=356089856} + "model" = "Apple M4" + "gpu-core-count" = 10 + } +`; + +test('parses utilization and memory from real ioreg output', () => { + let stats = parsePerformanceStatistics(M4_SAMPLE); + + assert.ok(stats !== null); + assert.strictEqual(stats!.utilization, 30); + assert.strictEqual(stats!.inUseMemory, 356089856); + assert.strictEqual(stats!.allocatedMemory, 2181693440); +}); + +test('does not mistake the "(driver)" sibling for in-use memory', () => { + // "In use system memory (driver)" is 0 and appears first, so a pattern + // without the closing quote would report 0 bytes in use. + let stats = parsePerformanceStatistics(M4_SAMPLE); + + assert.notStrictEqual(stats!.inUseMemory, 0); + assert.strictEqual(stats!.inUseMemory, 356089856); +}); + +test('returns null rather than throwing on unusable input', () => { + assert.strictEqual(parsePerformanceStatistics(''), null); + assert.strictEqual(parsePerformanceStatistics('not ioreg output at all'), null); + // Truncated before the statistics block closes. + assert.strictEqual(parsePerformanceStatistics('"PerformanceStatistics" = {"Alloc'), null); +}); + +test('returns null when utilization is absent, even if memory is present', () => { + // Utilization is the one value the feature cannot do without, so a machine + // reporting only memory counts as unsupported. + let stats = parsePerformanceStatistics('"PerformanceStatistics" = {"Alloc system memory"=123}'); + + assert.strictEqual(stats, null); +}); + +test('defaults missing memory values to zero', () => { + let stats = parsePerformanceStatistics('"PerformanceStatistics" = {"Device Utilization %"=42}'); + + assert.strictEqual(stats!.utilization, 42); + assert.strictEqual(stats!.inUseMemory, 0); + assert.strictEqual(stats!.allocatedMemory, 0); +}); + +test('reads the first accelerator when a machine reports several', () => { + let dualGpu = '"PerformanceStatistics" = {"Device Utilization %"=7}\n' + + '"PerformanceStatistics" = {"Device Utilization %"=88}'; + + assert.strictEqual(parsePerformanceStatistics(dualGpu)!.utilization, 7); +}); + +test('sampler resolves instead of rejecting, on every platform', async () => { + // ResMon.update() gathers resources with Promise.all and has no error + // handling, so a rejection here would freeze the whole status bar. + let stats = await new AppleGpuSampler().sample(); + + if (process.platform !== 'darwin') { + assert.strictEqual(stats, null, 'GPU statistics must be unavailable off macOS'); + return; + } + + // On macOS the values are real, so assert their shape rather than exact numbers. + if (stats !== null) { + assert.ok(stats.utilization >= 0 && stats.utilization <= 100); + assert.ok(stats.inUseMemory >= 0); + assert.ok(stats.allocatedMemory >= 0); + } +}); + +test('sampler serves concurrent callers from one sample', async () => { + // Two resources each call isShown() and getDisplay() per tick. Without the + // cache that is four ioreg invocations where one will do. + let sampler = new AppleGpuSampler(); + let results = await Promise.all([sampler.sample(), sampler.sample(), sampler.sample(), sampler.sample()]); + + let distinct = new Set(results.map(r => JSON.stringify(r))); + assert.strictEqual(distinct.size, 1, 'concurrent callers should observe one identical sample'); +}); diff --git a/src/test/constants.test.ts b/src/test/constants.test.ts new file mode 100644 index 0000000..41b5544 --- /dev/null +++ b/src/test/constants.test.ts @@ -0,0 +1,38 @@ +import * as assert from 'assert'; +import { test } from 'node:test'; +import { Units, UnitLabels, MemMappings, FreqMappings, DiskSpaceFormat, DiskSpaceFormatMappings } from '../constants'; + +test('unit labels read as byte units, not enum member names', () => { + // Units[Units.G] is "G" and Units[Units.None] is "None", which is what the + // disk display used to render. + assert.strictEqual(UnitLabels[Units.None], 'B'); + assert.strictEqual(UnitLabels[Units.K], 'KB'); + assert.strictEqual(UnitLabels[Units.M], 'MB'); + assert.strictEqual(UnitLabels[Units.G], 'GB'); +}); + +test('every unit the settings offer has a divisor', () => { + // These keys are the enums contributed in package.json; a missing entry + // would make the divisor undefined and render NaN in the status bar. + ['GB', 'MB', 'KB', 'B'].forEach(unit => { + assert.strictEqual(typeof MemMappings[unit], 'number', `no divisor for mem unit ${unit}`); + }); + ['GHz', 'MHz', 'KHz', 'Hz'].forEach(unit => { + assert.strictEqual(typeof FreqMappings[unit], 'number', `no divisor for freq unit ${unit}`); + }); +}); + +test('unit divisors are binary multiples', () => { + assert.strictEqual(MemMappings['B'], 1); + assert.strictEqual(MemMappings['KB'], 1024); + assert.strictEqual(MemMappings['MB'], 1024 * 1024); + assert.strictEqual(MemMappings['GB'], 1024 * 1024 * 1024); +}); + +test('every disk format the settings offer maps to an enum member', () => { + ['PercentRemaining', 'PercentUsed', 'Remaining', 'UsedOutOfTotal'].forEach(format => { + assert.strictEqual( + typeof DiskSpaceFormatMappings[format], 'number', `no mapping for disk format ${format}`); + }); + assert.strictEqual(DiskSpaceFormatMappings['PercentUsed'], DiskSpaceFormat.PercentUsed); +});