diff --git a/CHANGELOG.md b/CHANGELOG.md index 53f0b0e..55cce04 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### 3.0.1 + +- fix wmic removed on Windows 11 and add gwmi support + ### 3.0 - removes node 8 support diff --git a/lib/gwmi.js b/lib/gwmi.js new file mode 100644 index 0000000..d33a670 --- /dev/null +++ b/lib/gwmi.js @@ -0,0 +1,135 @@ +'use strict' + +const os = require('os') +const bin = require('./bin') +const history = require('./history') + +function parseDate (datestr) { + const year = datestr.substring(0, 4) + const month = datestr.substring(4, 6) + const day = datestr.substring(6, 8) + const hour = datestr.substring(8, 10) + const minutes = datestr.substring(10, 12) + const seconds = datestr.substring(12, 14) + const useconds = datestr.substring(15, 21) + const sign = datestr.substring(21, 22) + const tmz = parseInt(datestr.substring(22, 25), 10) + const tmzh = Math.floor(tmz / 60) + const tmzm = tmz % 60 + + return new Date( + year + '-' + month + '-' + day + 'T' + hour + + ':' + minutes + ':' + seconds + + '.' + useconds + + sign + (tmzh > 9 ? tmzh : '0' + tmzh) + '' + (tmzm > 9 ? tmzm : '0' + tmzm) + ) +} + +function gwmi (pids, options, done) { + let whereClause = 'ProcessId=' + pids[0] + for (let i = 1; i < pids.length; i++) { + whereClause += ' or ' + 'ProcessId=' + pids[i] + } + + const property = 'CreationDate,KernelModeTime,ParentProcessId,ProcessId,UserModeTime,WorkingSetSize' + const args = ['win32_process', '-Filter', '\'' + whereClause + '\'', '| select ' + property, '| format-table'] + + bin('gwmi', args, { windowsHide: true, windowsVerbatimArguments: true, shell: 'powershell.exe' }, function (err, stdout, code) { + if (err) { + if (err.message.indexOf('No Instance(s) Available.') !== -1) { + const error = new Error('No matching pid found') + error.code = 'ENOENT' + return done(error) + } + return done(err) + } + if (code !== 0) { + return done(new Error('pidusage gwmi command exited with code ' + code)) + } + const date = Date.now() + + // Note: On Windows the returned value includes fractions of a second. + // Use Math.floor() to get whole seconds. + // Fallback on current date when uptime is not allowed (see https://github.com/soyuka/pidusage/pull/130) + const uptime = Math.floor(os.uptime() || (date / 1000)) + + // Example of stdout on Windows 10 + // CreationDate: is in the format yyyymmddHHMMSS.mmmmmmsUUU + // KernelModeTime: is in units of 100 ns + // UserModeTime: is in units of 100 ns + // WorkingSetSize: is in bytes + // + // Refs: https://superuser.com/a/937401/470946 + // Refs: https://msdn.microsoft.com/en-us/library/aa394372(v=vs.85).aspx + // NB: The columns are returned in lexicographical order + // + // Stdout Format + // + // Active code page: 936 + // + // CreationDate KernelModeTime ParentProcessId ProcessId UserModeTime WorkingSetSize + // ------------ -------------- --------------- --------- ------------ -------------- + // 20220220185531.619182+480 981406250 18940 2804 572656250 61841408 + + stdout = stdout.split(os.EOL).slice(1) + const index = stdout.findIndex(v => !!v) + stdout = stdout.slice(index + 2) + + if (!stdout.length) { + const error = new Error('No matching pid found') + error.code = 'ENOENT' + return done(error) + } + + let again = false + const statistics = {} + for (let i = 0; i < stdout.length; i++) { + const line = stdout[i].trim().split(/\s+/) + + if (!line || line.length === 1) { + continue + } + + const creation = parseDate(line[0]) + const ppid = parseInt(line[2], 10) + const pid = parseInt(line[3], 10) + const kerneltime = Math.round(parseInt(line[1], 10) / 10000) + const usertime = Math.round(parseInt(line[4], 10) / 10000) + const memory = parseInt(line[5], 10) + + let hst = history.get(pid, options.maxage) + if (hst === undefined) { + again = true + hst = { ctime: kerneltime + usertime, uptime: uptime } + } + + // process usage since last call + const total = (kerneltime + usertime - hst.ctime) / 1000 + // time elapsed between calls in seconds + const seconds = uptime - hst.uptime + const cpu = seconds > 0 ? (total / seconds) * 100 : 0 + + history.set(pid, { ctime: usertime + kerneltime, uptime: uptime }, options.maxage) + + statistics[pid] = { + cpu: cpu, + memory: memory, + ppid: ppid, + pid: pid, + ctime: usertime + kerneltime, + elapsed: date - creation.getTime(), + timestamp: date + } + } + + if (again) { + return gwmi(pids, options, function (err, stats) { + if (err) return done(err) + done(null, Object.assign(statistics, stats)) + }) + } + done(null, statistics) + }) +} + +module.exports = gwmi diff --git a/lib/stats.js b/lib/stats.js index b9f83d6..92c3701 100644 --- a/lib/stats.js +++ b/lib/stats.js @@ -2,6 +2,7 @@ const fs = require('fs') const os = require('os') +const spawn = require('child_process').spawn const platformToMethod = { aix: 'ps', @@ -17,6 +18,7 @@ const platformToMethod = { } const ps = require('./ps') +const gwmi = require('./gwmi') let platform = os.platform() if (fs.existsSync('/etc/alpine-release')) { @@ -50,6 +52,15 @@ function get (pids, options, callback) { if (platform !== 'win' && options.usePs === true) { fn = ps } + if (platform === 'win') { + try { + spawn('wmic', function (err) { + if (err) throw new Error(err) + }) + } catch (err) { + fn = gwmi + } + } if (stat === undefined) { return callback(new Error(os.platform() + ' is not supported yet, please open an issue (https://github.com/soyuka/pidusage)')) diff --git a/package-lock.json b/package-lock.json index 6db03b8..8773872 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,6 +5,7 @@ "requires": true, "packages": { "": { + "name": "pidusage", "version": "3.0.0", "license": "MIT", "dependencies": { diff --git a/test/gwmi.js b/test/gwmi.js new file mode 100644 index 0000000..ff87b94 --- /dev/null +++ b/test/gwmi.js @@ -0,0 +1,77 @@ +const mockery = require('mockery') +const test = require('ava') +const os = require('os') +const mockdate = require('mockdate') +const pify = require('pify') + +const mocks = require('./helpers/_mocks') + +const timeout = ms => new Promise((resolve, reject) => setTimeout(resolve, ms)) + +test.before(() => { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockdate.set(new Date(1427749200000)) +}) + +test.beforeEach(() => { + mockery.resetCache() +}) + +test.after(() => { + mockery.disable() + mockdate.reset() +}) + +test('should parse gwmi output on Windows', async t => { + const stdout = '' + + 'Active code page: 936' + os.EOL + + '' + os.EOL + + '' + os.EOL + + 'CreationDate KernelModeTime ParentProcessId ProcessId UserModeTime WorkingSetSize' + os.EOL + + '------------ ----------- ----------- ------------ -------- --------' + os.EOL + + '20150329221650.080654+060 153750000 0 777 8556250000 110821376' + + let calls = 0 + + mockery.registerMock('child_process', { + spawn: () => { + calls++ + return mocks.spawn(stdout, '', null, 0, null) + } + }) + + const gwmi = require('../lib/gwmi') + + let result = await pify(gwmi)([6456], { maxage: 1000 }) + t.deepEqual(result, { + 777: { + cpu: 0, + memory: 110821376, + ppid: 0, + pid: 777, + ctime: (855625 + 15375), + elapsed: 1427749200000 - new Date('2015-03-29T22:16:50.080654+0100').getTime(), + timestamp: 1427749200000 + } + }) + + result = await pify(gwmi)([6456], { maxage: 1000 }) + + t.is(calls, 3, '2 first calls to put in history + 1') + + mockdate.set(new Date(1427749202000)) + + // wait 1 second, it should do 2 calls again + await timeout(1000) + + calls = 0 + result = await pify(gwmi)([6456], { maxage: 1000 }) + + t.is(calls, 2, '2 first calls') + + mockery.deregisterMock('child_process') +})