Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(wmic): add gwmi command to be compatible with deleted wmic #143

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
### 3.0.1

- fix wmic removed on Windows 11 and add gwmi support

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is awesome, I'll suggest however to target a 4.x as its a new feature and we follow semver. I'll merge some pending patches but I'd love to see this being added, many thanks for the work!

P.S.: I had a windows environment at the time of coding the first version but now I don't anymore so its hard for me to test this.

### 3.0

- removes node 8 support
Expand Down
135 changes: 135 additions & 0 deletions lib/gwmi.js
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions lib/stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const fs = require('fs')
const os = require('os')
const spawn = require('child_process').spawn

const platformToMethod = {
aix: 'ps',
Expand All @@ -17,6 +18,7 @@ const platformToMethod = {
}

const ps = require('./ps')
const gwmi = require('./gwmi')
let platform = os.platform()

if (fs.existsSync('/etc/alpine-release')) {
Expand Down Expand Up @@ -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
}
Comment on lines +56 to +62
Copy link
Owner

@soyuka soyuka Oct 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like this, isn't there another way to try and see if wmic is installed? I'd even say that we should check if gwmi exists and use it if possible (wmic is old).

Copy link

@paescuj paescuj Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if this PR still wants to be pursued, but leaving a comment in any case:

On Windows there's the where 1 command (~ equivalent to UNIX which). This could be used to check whether gwmi / wmic is available.
Though, this still requires a spawn call.

Footnotes

  1. https://learn.microsoft.com/en-us/windows-server/administration/windows-commands/where

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to be able to continue, but I haven't been following this for a long time and have been a little busy lately with my new job, so I may tackle this again in the next two weeks!

}

if (stat === undefined) {
return callback(new Error(os.platform() + ' is not supported yet, please open an issue (https://github.com/soyuka/pidusage)'))
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions test/gwmi.js
Original file line number Diff line number Diff line change
@@ -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')
})