Skip to content

Commit

Permalink
Add memoryUsage for Windows and owner.path and memoryUsage for …
Browse files Browse the repository at this point in the history
…Linux (#42)
  • Loading branch information
Yanis Benson authored and sindresorhus committed May 20, 2019
1 parent 97951b8 commit a704448
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 76 deletions.
40 changes: 14 additions & 26 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ declare namespace activeWin {
Process identifier
*/
processId: number;

/**
Path to the app.
*/
path: string;
}

interface BaseResult {
Expand Down Expand Up @@ -38,45 +43,32 @@ declare namespace activeWin {
App that owns the window.
*/
owner: BaseOwner;

/**
Memory usage by the window.
*/
memoryUsage: number;
}

interface MacOSOwner extends BaseOwner {
/**
Bundle identifier.
*/
bundleId: number;

/**
Path to the app.
*/
path: string;
}

interface MacOSResult extends BaseResult {
platform: 'macos';

owner: MacOSOwner;

/**
Memory usage by the window.
*/
memoryUsage: number;
}

interface LinuxResult extends BaseResult {
platform: 'linux';
}

interface WindowsOwner extends BaseOwner {
/**
Path to the app.
*/
path: string;
}

interface WindowsResult extends BaseResult {
platform: 'windows';
owner: WindowsOwner;
}

type Result = MacOSResult | LinuxResult | WindowsResult;
Expand All @@ -100,14 +92,12 @@ declare const activeWin: {
}
if (result.platform === 'macos') {
// Among other fields, result.owner.bundleId, result.owner.path, and result.memoryUsage are available on macOS.
// Among other fields, result.owner.bundleId is available on macOS.
console.log(`Process title is ${result.title} with bundle id ${result.owner.bundleId}.`);
} else if (result.platform === 'windows') {
// Among other fields, result.owner.path, and result.memoryUsage are available on Windows.
console.log(`Process title is ${result.title} with path ${result.owner.path}.`);
} else {
// Only common fields are available on Linux.
console.log(`Process title is ${result.title}.`);
console.log(`Process title is ${result.title} with path ${result.owner.path}.`);
}
})();
```
Expand All @@ -127,14 +117,12 @@ declare const activeWin: {
if (result) {
if (result.platform === 'macos') {
// Among other fields, result.owner.bundleId, result.owner.path, and result.memoryUsage are available on macOS.
// Among other fields, result.owner.bundleId is available on macOS.
console.log(`Process title is ${result.title} with bundle id ${result.owner.bundleId}.`);
} else if (result.platform === 'windows') {
// Among other fields, result.owner.path, and result.memoryUsage are available on Windows.
console.log(`Process title is ${result.title} with path ${result.owner.path}.`);
} else {
// Only common fields are available on Linux.
console.log(`Process title is ${result.title}.`);
console.log(`Process title is ${result.title} with path ${result.owner.path}.`);
}
}
```
Expand Down
26 changes: 11 additions & 15 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,25 @@ const result = activeWin.sync();
expectType<Result | undefined>(result);

if (result) {
expectType<string>(result.platform);
expectType<string>(result.title);
expectType<number>(result.id);
expectType<number>(result.bounds.x);
expectType<number>(result.bounds.y);
expectType<number>(result.bounds.width);
expectType<number>(result.bounds.height);
expectType<string>(result.owner.name);
expectType<number>(result.owner.processId);
expectType<string>(result.owner.path);
expectType<number>(result.memoryUsage);
if (result.platform === 'macos') {
expectType<MacOSResult>(result);

expectType<string>(result.title);
expectType<number>(result.id);
expectType<number>(result.bounds.x);
expectType<number>(result.bounds.y);
expectType<number>(result.bounds.width);
expectType<number>(result.bounds.height);
expectType<string>(result.owner.name);
expectType<number>(result.owner.processId);
expectType<number>(result.owner.bundleId);
expectType<string>(result.owner.path);
expectType<number>(result.memoryUsage);
} else if (result.platform === 'linux') {
expectType<LinuxResult>(result);
expectError(result.owner.path);
expectError(result.owner.bundleId);
expectError(result.memoryUsage);
} else {
expectType<WindowsResult>(result);
expectType<string>(result.owner.path);
expectError(result.owner.bundleId);
expectError(result.memoryUsage);
}
}
107 changes: 77 additions & 30 deletions lib/linux.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
'use strict';
const {promisify} = require('util');
const fs = require('fs');
const childProcess = require('child_process');

const execFile = promisify(childProcess.execFile);
const readFile = promisify(fs.readFile);
const readlink = promisify(fs.readlink);

const xpropBin = 'xprop';
const xwininfoBin = 'xwininfo';
const xpropActiveArgs = ['-root', '\t$0', '_NET_ACTIVE_WINDOW'];
const xpropDetailsArgs = ['-id'];

const processOutput = output => {
const result = {
platform: 'linux'
};
const result = {};

for (const row of output.trim().split('\n')) {
if (row.includes('=')) {
Expand All @@ -35,12 +37,19 @@ const parseLinux = ({stdout, boundsStdout, activeWindowId}) => {
const windowId = (resultKeys.indexOf(windowIdProperty) > 0 &&
parseInt(result[windowIdProperty].split('#').pop(), 16)) || activeWindowId;

const processId = parseInt(result['_NET_WM_PID(CARDINAL)'], 10);

if (Number.isNaN(processId)) {
throw new Error('Failed to parse process ID'); // eslint-disable-line unicorn/prefer-type-error
}

return {
platform: 'linux',
title: JSON.parse(result['_NET_WM_NAME(UTF8_STRING)']) || null,
id: windowId,
owner: {
name: JSON.parse(result['WM_CLASS(STRING)'].split(',').pop()),
processId: parseInt(result['_NET_WM_PID(CARDINAL)'], 10)
processId
},
bounds: {
x: parseInt(bounds['Absolute upper-left X'], 10),
Expand All @@ -53,38 +62,76 @@ const parseLinux = ({stdout, boundsStdout, activeWindowId}) => {

const getActiveWindowId = activeWindowIdStdout => parseInt(activeWindowIdStdout.split('\t')[1], 16);

const getMemoryUsageByPid = async pid => {
const statm = await readFile(`/proc/${pid}/statm`, 'utf8');
return parseInt(statm.split(' ')[1], 10) * 4096;
};

const getMemoryUsageByPidSync = pid => {
const statm = require('fs').readFileSync(`/proc/${pid}/statm`, 'utf8');
return parseInt(statm.split(' ')[1], 10) * 4096;
};

const getPathByPid = pid => {
return readlink(`/proc/${pid}/exe`);
};

const getPathByPidSync = pid => {
return fs.readlinkSync(`/proc/${pid}/exe`);
};

module.exports = async () => {
const {stdout: activeWindowIdStdout} = await execFile(xpropBin, xpropActiveArgs);
const activeWindowId = getActiveWindowId(activeWindowIdStdout);
if (!activeWindowId) {
return;
}
try {
const {stdout: activeWindowIdStdout} = await execFile(xpropBin, xpropActiveArgs);
const activeWindowId = getActiveWindowId(activeWindowIdStdout);

if (!activeWindowId) {
return;
}

const [{stdout}, {stdout: boundsStdout}] = await Promise.all([
execFile(xpropBin, xpropDetailsArgs.concat([activeWindowId])),
execFile(xwininfoBin, xpropDetailsArgs.concat([activeWindowId]))
]);
const [{stdout}, {stdout: boundsStdout}] = await Promise.all([
execFile(xpropBin, xpropDetailsArgs.concat([activeWindowId])),
execFile(xwininfoBin, xpropDetailsArgs.concat([activeWindowId]))
]);

return parseLinux({
activeWindowId,
boundsStdout,
stdout
});
const data = parseLinux({
activeWindowId,
boundsStdout,
stdout
});
const [memoryUsage, path] = await Promise.all([
getMemoryUsageByPid(data.owner.processId),
getPathByPid(data.owner.processId)
]);
data.memoryUsage = memoryUsage;
data.owner.path = path;
return data;
} catch (_) {
return undefined;
}
};

module.exports.sync = () => {
const activeWindowIdStdout = childProcess.execFileSync(xpropBin, xpropActiveArgs, {encoding: 'utf8'});
const activeWindowId = getActiveWindowId(activeWindowIdStdout);
if (!activeWindowId) {
return;
}
try {
const activeWindowIdStdout = childProcess.execFileSync(xpropBin, xpropActiveArgs, {encoding: 'utf8'});
const activeWindowId = getActiveWindowId(activeWindowIdStdout);

const stdout = childProcess.execFileSync(xpropBin, xpropDetailsArgs.concat(activeWindowId), {encoding: 'utf8'});
const boundsStdout = childProcess.execFileSync(xwininfoBin, xpropDetailsArgs.concat([activeWindowId]), {encoding: 'utf8'});
if (!activeWindowId) {
return;
}

return parseLinux({
activeWindowId,
boundsStdout,
stdout
});
const stdout = childProcess.execFileSync(xpropBin, xpropDetailsArgs.concat(activeWindowId), {encoding: 'utf8'});
const boundsStdout = childProcess.execFileSync(xwininfoBin, xpropDetailsArgs.concat([activeWindowId]), {encoding: 'utf8'});

const data = parseLinux({
activeWindowId,
boundsStdout,
stdout
});
data.memoryUsage = getMemoryUsageByPidSync(data.owner.processId);
data.owner.path = getPathByPidSync(data.owner.processId);
return data;
} catch (_) {
return undefined;
}
};
53 changes: 51 additions & 2 deletions lib/windows.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,30 @@ const user32 = new ffi.Library('User32.dll', {
GetWindowRect: ['bool', ['pointer', RectPointer]]
});

const SIZE_T = 'uint64';

// https://docs.microsoft.com/en-us/windows/desktop/api/psapi/ns-psapi-_process_memory_counters
const ProcessMemoryCounters = struct({
cb: 'uint32',
PageFaultCount: 'uint32',
PeakWorkingSetSize: SIZE_T,
WorkingSetSize: SIZE_T,
QuotaPeakPagedPoolUsage: SIZE_T,
QuotaPagedPoolUsage: SIZE_T,
QuotaPeakNonPagedPoolUsage: SIZE_T,
QuotaNonPagedPoolUsage: SIZE_T,
PagefileUsage: SIZE_T,
PeakPagefileUsage: SIZE_T
});

const ProcessMemoryCountersPointer = ref.refType(ProcessMemoryCounters);

// Create FFI declarations for the C++ library and functions needed (psapi.dll)
const psapi = new ffi.Library('psapi', {
// https://docs.microsoft.com/en-us/windows/desktop/api/psapi/nf-psapi-getprocessmemoryinfo
GetProcessMemoryInfo: ['int', ['pointer', ProcessMemoryCountersPointer, 'uint32']]
});

// Create FFI declarations for the C++ library and functions needed (Kernel32.dll), using their "Unicode" (UTF-16) version
const kernel32 = new ffi.Library('kernel32', {
// https://msdn.microsoft.com/en-us/library/windows/desktop/ms684320(v=vs.85).aspx
Expand All @@ -49,6 +73,11 @@ function windows() {

// Get a "handle" of the active window
const activeWindowHandle = user32.GetForegroundWindow();

if (ref.isNull(activeWindowHandle)) {
return undefined; // Failed to get active window handle
}

// Get memory address of the window handle as the "window ID"
const windowId = ref.address(activeWindowHandle);
// Get the window text length in "characters" to create the buffer
Expand All @@ -72,6 +101,11 @@ function windows() {
const processId = ref.get(processIdBuffer);
// Get a "handle" of the process
const processHandle = kernel32.OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, processId);

if (ref.isNull(processHandle)) {
return undefined; // Failed to get process handle
}

// Set the path length to more than the Windows extended-length MAX_PATH length
const pathLengthBytes = 66000;
// Path length in "characters"
Expand All @@ -88,12 +122,26 @@ function windows() {
const processPath = wchar.toString(processFileNameBufferClean);
// Get process file name from path
const processName = path.basename(processPath);

// Get process memory counters
const memoryCounters = new ProcessMemoryCounters();
memoryCounters.cb = ProcessMemoryCounters.size;
const getProcessMemoryInfoResult = psapi.GetProcessMemoryInfo(processHandle, memoryCounters.ref(), ProcessMemoryCounters.size);

// Close the "handle" of the process
kernel32.CloseHandle(processHandle);
// Create a new instance of Rect, the struct required by the `GetWindowRect` method
const bounds = new Rect();
// Get the window bounds and save it into the `bounds` variable
user32.GetWindowRect(activeWindowHandle, bounds.ref());
const getWindowRectResult = user32.GetWindowRect(activeWindowHandle, bounds.ref());

if (getProcessMemoryInfoResult === 0) {
return undefined; // Failed to get process memory
}

if (getWindowRectResult === 0) {
return undefined; // Failed to get window rect
}

return {
platform: 'windows',
Expand All @@ -109,7 +157,8 @@ function windows() {
y: bounds.top,
width: bounds.right - bounds.left,
height: bounds.bottom - bounds.top
}
},
memoryUsage: memoryCounters.WorkingSetSize
};

/* eslint-enable new-cap */
Expand Down
6 changes: 3 additions & 3 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const activeWin = require('active-win');

### activeWin()

Returns a `Promise<Object>` with the result, or `Promise<undefined>` if there is no active window.
Returns a `Promise<Object>` with the result, or `Promise<undefined>` if there is no active window or if the information is not available.

### activeWin.sync()

Expand All @@ -67,8 +67,8 @@ Returns an `Object` with the result, or `undefined` if there is no active window
- `name` *(string)* - Name of the app
- `processId` *(number)* - Process identifier
- `bundleId` *(string)* - Bundle identifier *(macOS only)*
- `path` *(string)* - Path to the app *(macOS and Windows only)*
- `memoryUsage` *(number)* - Memory usage by the window *(macOS only)*
- `path` *(string)* - Path to the app
- `memoryUsage` *(number)* - Memory usage by the window owner process


## OS support
Expand Down

0 comments on commit a704448

Please sign in to comment.