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 issue #58 and issue #42: crashes on Windows 11 24H2 and wrong results on processes with spaces in name #62

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
112 changes: 85 additions & 27 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
'use strict';

var spawn = require('child_process').spawn,
es = require('event-stream');
const spawn = require('child_process').spawn;
const es = require('event-stream');

module.exports = function childrenOfPid(pid, callback) {
var headers = null;
let headers = null;

if (typeof callback !== 'function') {
throw new Error('childrenOfPid(pid, callback) expects callback');
Expand Down Expand Up @@ -35,20 +35,24 @@ module.exports = function childrenOfPid(pid, callback) {
// /usr/libexec/Use 1 43 Ss
//
// Win32:
// 1. wmic PROCESS WHERE ParentProcessId=4604 GET Name,ParentProcessId,ProcessId,Status)
// 2. The order of head columns is fixed
// 1. powershell Get-WmiObject -Class Win32_Process | Select-Object -Property Name,ProcessId,ParentProcessId,Status | Format-Table
// 2. Name column content may contain spaces; columns have a fixed width;
// status column ist usually empty. Output contains empty lines and dashes under the header.
// ```shell
// > wmic PROCESS GET Name,ProcessId,ParentProcessId,Status
// Name ParentProcessId ProcessId Status
// System Idle Process 0 0
// System 0 4
// smss.exe 4 228
// > powershell Get-WmiObject -Class Win32_Process | Select-Object -Property Name,ProcessId,ParentProcessId,Status | Format-Table
// Name ProcessId ParentProcessId Status
// ---- --------- --------------- ------
// System Idle Process 0 0
// System 4 0
// Secure System 188 4
// Registry 232 4
// smss.exe 760 4
// ```

var processLister;
let processLister;
if (process.platform === 'win32') {
// See also: https://github.com/nodejs/node-v0.x-archive/issues/2318
processLister = spawn('wmic.exe', ['PROCESS', 'GET', 'Name,ProcessId,ParentProcessId,Status']);
// WMIC is deprecated since 2016; using powershell 5.1
processLister = spawn('powershell.exe',['Get-WmiObject -Class Win32_Process | Select-Object -Property Name,ProcessId,ParentProcessId,Status | Format-Table']);
} else {
processLister = spawn('ps', ['-A', '-o', 'ppid,pid,stat,comm']);
}
Expand All @@ -57,23 +61,34 @@ module.exports = function childrenOfPid(pid, callback) {
// spawn('ps', ['-A', '-o', 'ppid,pid,stat,comm']).stdout,
processLister.stdout,
es.split(),
es.map(function (line, cb) { //this could parse alot of unix command output
var columns = line.trim().split(/\s+/);
if (!headers) {
headers = columns;

//
// Rename Win32 header name, to as same as the linux, for compatible.
//
headers = headers.map(normalizeHeader);
es.map(function (line, cb) { //this could parse a lot of unix command output
const trimmedLine = line.trim();

// Windows: remove unnecessary lines created by powershell
if ((trimmedLine.length === 0) || trimmedLine.includes('----')) {
return cb();
}

/**
* The first line contains the column headers. All columns have a fixed width.
* Windows: COMMANDs may contain white spaces; therefore we cannot simply
* split the lines using white spaces as separators.
*/
if (headers === null) {
headers = getColumnDefs(trimmedLine);

// For compatibility rename windows header names to the linux header names.
headers = headers.map(header => {
header.header = normalizeHeader(header.header);
return header;
});
return cb();
}

var row = {};
// For each header
var h = headers.slice();
while (h.length) {
row[h.shift()] = h.length ? columns.shift() : columns.join(' ');
let row = {};
for (const headerDef of headers) {
const columnValue = trimmedLine.substring(headerDef.start, headerDef.end).trim();
row[headerDef.header] = columnValue;
}

return cb(null, row);
Expand All @@ -95,6 +110,49 @@ module.exports = function childrenOfPid(pid, callback) {
).on('error', callback)
}

/**
* Extract the column definitions from the first line into an array of objects.
* Object structure:
* {
* start: index of the first character of the column,
* end: index of the first character after the column,
* header: text of the column header
* }
* On Linux: the first column header is right aligned.
* On Windows: the first column header is left aligned.
* @param {string} line - string with the headers of the columns
* @returns {Object[]} Array of objects containing the definition of 1 column
*/
function getColumnDefs(line) {
const columnDefinitions = [];
let startOfColumnIncl = 0;
let endOfColumnExcl = 0;
let foundStartOfHeader = false;
let foundEndOfHeader = false;
for (let i = 0; i < line.length; i++) {
const isWhitespace = line.substring(i, i + 1).trim() === '';
if (!foundStartOfHeader && !isWhitespace) {
// search for first header, if it is right aligned (on linux)
foundStartOfHeader = true;
} else if (foundStartOfHeader && isWhitespace) {
// search for end of header text
foundEndOfHeader = true;
} else if (foundEndOfHeader && !isWhitespace) {
endOfColumnExcl = i - 1;
const header = line.substring(startOfColumnIncl, endOfColumnExcl).trim();
columnDefinitions.push({start:startOfColumnIncl, end:endOfColumnExcl, header:header});
startOfColumnIncl = i;
foundStartOfHeader = true;
foundEndOfHeader = false;
}
}

// last column
const header = line.substring(startOfColumnIncl, line.length).trim();
columnDefinitions.push({start:startOfColumnIncl, end:line.length, header:header});
return columnDefinitions;
}

/**
* Normalizes the given header `str` from the Windows
* title to the *nix title.
Expand Down