Skip to content
Merged
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
26 changes: 25 additions & 1 deletion README-zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,12 @@ const osutils = new OSUtils({
debug: false,

// 监控器特定配置
cpu: { cacheTTL: 30000 },
cpu: {
cacheTTL: 30000,
// 是否将 iowait 从整体 CPU 使用率中排除(仅 Linux 生效)
// 默认 false:iowait 计入 overall,与传统监控工具行为一致
excludeIowait: false
},
memory: { cacheTTL: 5000 },
disk: { cacheTTL: 60000 }
});
Expand Down Expand Up @@ -303,6 +308,25 @@ if (loadAvg.success) {
| `getCacheInfo()` | `Promise<MonitorResult<any>>` | CPU 缓存层级信息 | ⚠️ 有限 |
| `coreCount()` | `Promise<MonitorResult<{ physical: number; logical: number }>>` | 物理/逻辑核心数量 | ✅ 全部 |

#### CPU 配置项

| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `excludeIowait` | `boolean` | `false` | 为 `true` 时,I/O 等待时间(iowait)将从 `overall` 使用率中剔除,适合 I/O 密集型场景下避免 CPU 使用率虚高。`iowait` 仍作为独立字段在 `usageDetailed()` 中返回。仅 Linux 生效。 |

```typescript
// 在 I/O 密集型 Linux 环境中排除 iowait
const osutils = new OSUtils({
cpu: { excludeIowait: true }
});

const result = await osutils.cpu.usageDetailed();
if (result.success) {
console.log('整体使用率(不含 iowait):', result.data.overall + '%');
console.log('iowait:', result.data.iowait + '%'); // 仍可单独读取
}
```

#### 实时 CPU 监控

```typescript
Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,12 @@ const osutils = new OSUtils({
debug: false,

// Monitor-specific configurations
cpu: { cacheTTL: 30000 },
cpu: {
cacheTTL: 30000,
// Exclude iowait from the overall CPU usage percentage (Linux only).
// Default: false (iowait is included, matching traditional tool behavior)
excludeIowait: false
},
memory: { cacheTTL: 5000 },
disk: { cacheTTL: 60000 }
});
Expand Down Expand Up @@ -303,6 +308,25 @@ if (loadAvg.success) {
| `getCacheInfo()` | `Promise<MonitorResult<any>>` | CPU cache hierarchy information | ⚠️ Limited |
| `coreCount()` | `Promise<MonitorResult<{ physical: number; logical: number }>>` | Physical/logical core counts | ✅ All |

#### CPU Configuration

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `excludeIowait` | `boolean` | `false` | When `true`, I/O wait time is excluded from the `overall` CPU usage percentage. Useful in I/O-heavy environments where iowait would otherwise inflate reported CPU usage. `iowait` is still available as a separate field in `usageDetailed()`. Linux only. |

```typescript
// Exclude iowait from overall CPU usage (Linux I/O-heavy workloads)
const osutils = new OSUtils({
cpu: { excludeIowait: true }
});

const result = await osutils.cpu.usageDetailed();
if (result.success) {
console.log('Overall (excl. iowait):', result.data.overall + '%');
console.log('iowait:', result.data.iowait + '%'); // still available
}
```

#### Real-time CPU Monitoring

```typescript
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "node-os-utils",
"version": "2.0.2",
"version": "2.0.3",
"description": "Advanced cross-platform operating system monitoring utilities with TypeScript support",
"type": "commonjs",
"main": "dist/src/index.js",
Expand Down
10 changes: 9 additions & 1 deletion src/adapters/linux-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,11 @@ export class LinuxAdapter extends BasePlatformAdapter {
async getDiskInfo(): Promise<any> {
try {
const result = await this.executeCommand('df -h');
this.validateCommandResult(result, 'df -h');
// df 遇到无权限挂载点(如 /run/user/1000/doc FUSE 挂载)会以 exit code 1 退出,
// 但 stdout 仍包含其余挂载点的完整数据。只要有可解析的输出就继续处理。
if (!result.stdout || result.stdout.trim().split('\n').length < 2) {
this.validateCommandResult(result, 'df -h');
}
return this.parseDiskInfo(result.stdout);
} catch (error) {
throw this.createCommandError('getDiskInfo', error);
Expand Down Expand Up @@ -967,6 +971,10 @@ export class LinuxAdapter extends BasePlatformAdapter {
async getDiskUsage(): Promise<any> {
try {
const result = await this.executeCommand('df -B1');
// 同 getDiskInfo:df 遇到无权限挂载点时 exit code 为 1,但 stdout 数据仍有效
if (!result.stdout || result.stdout.trim().split('\n').length < 2) {
this.validateCommandResult(result, 'df -B1');
}
return this.parseDiskUsage(result.stdout);
} catch (error) {
throw this.createCommandError('getDiskUsage', error);
Expand Down
10 changes: 8 additions & 2 deletions src/monitors/cpu-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,13 +300,19 @@ export class CPUMonitor extends BaseMonitor<CPUInfo> {
* 转换 CPU 使用率信息
*/
private transformCPUUsage(rawData: any): CPUUsage {
const iowait = this.safeParseNumber(rawData.iowait);
const rawOverall = this.safeParseNumber(rawData.overall || rawData.usage);
const overall = this.cpuConfig.excludeIowait
? Math.max(0, rawOverall - iowait)
: rawOverall;

return {
overall: this.safeParseNumber(rawData.overall || rawData.usage),
overall,
cores: rawData.cores || [],
user: this.safeParseNumber(rawData.user),
system: this.safeParseNumber(rawData.system || rawData.sys),
idle: this.safeParseNumber(rawData.idle),
iowait: this.safeParseNumber(rawData.iowait),
iowait,
irq: this.safeParseNumber(rawData.irq),
softirq: this.safeParseNumber(rawData.softirq)
};
Expand Down
8 changes: 8 additions & 0 deletions src/types/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ export interface CPUConfig extends MonitorConfig {
* 负载平均值时间窗口(分钟)
*/
loadAverageWindows?: number[];

/**
* 计算 overall CPU 使用率时是否排除 I/O 等待时间(iowait)
*
* - `false`(默认):iowait 计入 overall,与大多数传统监控工具行为一致
* - `true`:iowait 从 overall 中剔除,反映 CPU 实际计算负载(适合 I/O 密集型场景)
*/
excludeIowait?: boolean;
}

/**
Expand Down
87 changes: 87 additions & 0 deletions test/unit/adapters/linux-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,93 @@ describe('LinuxAdapter 内部解析逻辑', () => {
expect(result.model).to.be.a('string').and.to.have.length.greaterThan(0);
});

// ——— #37 修复:df 遇到无权限挂载点时不应整体失败 ———

it('getDiskInfo() 在 df 遇到权限错误但 stdout 有效时应正常返回磁盘列表', async () => {
const adapter = new LinuxAdapter();

(adapter as any).executeCommand = async () => ({
stdout: [
'Filesystem Size Used Avail Use% Mounted on',
'/dev/sda1 50G 20G 30G 40% /',
'/dev/sdb1 100G 60G 40G 60% /data'
].join('\n'),
stderr: 'df: /run/user/1000/doc: Operation not permitted',
exitCode: 1,
platform: 'linux',
executionTime: 5,
command: 'df -h'
});

const result = await adapter.getDiskInfo();
expect(result).to.be.an('array').with.lengthOf(2);
expect(result[0].mountpoint).to.equal('/');
expect(result[1].mountpoint).to.equal('/data');
});

it('getDiskInfo() 在 df stdout 为空时应抛出错误', async () => {
const adapter = new LinuxAdapter();

(adapter as any).executeCommand = async () => ({
stdout: '',
stderr: 'df: command not found',
exitCode: 127,
platform: 'linux',
executionTime: 0,
command: 'df -h'
});

try {
await adapter.getDiskInfo();
expect.fail('应该抛出 MonitorError');
} catch (error: any) {
expect(error).to.be.instanceOf(MonitorError);
}
});

it('getDiskUsage() 在 df 遇到权限错误但 stdout 有效时应正常返回磁盘列表', async () => {
const adapter = new LinuxAdapter();

(adapter as any).executeCommand = async () => ({
stdout: [
'Filesystem 1B-blocks Used Available Use% Mounted on',
'/dev/sda1 53687091200 21474836480 32212254720 40% /',
'/dev/sdb1 107374182400 64424509440 42949672960 60% /data'
].join('\n'),
stderr: 'df: /run/user/1000/doc: Operation not permitted',
exitCode: 1,
platform: 'linux',
executionTime: 5,
command: 'df -B1'
});

const result = await adapter.getDiskUsage();
expect(result).to.be.an('array').with.lengthOf(2);
expect(result[0].mountPoint).to.equal('/');
expect(result[1].mountPoint).to.equal('/data');
expect(result[0].usagePercentage).to.equal(40);
});

it('getDiskUsage() 在 df stdout 为空时应抛出错误', async () => {
const adapter = new LinuxAdapter();

(adapter as any).executeCommand = async () => ({
stdout: '',
stderr: 'df: command not found',
exitCode: 127,
platform: 'linux',
executionTime: 0,
command: 'df -B1'
});

try {
await adapter.getDiskUsage();
expect.fail('应该抛出 MonitorError');
} catch (error: any) {
expect(error).to.be.instanceOf(MonitorError);
}
});

it('应在 ss 不可用时回退到 netstat 解析连接', async () => {
const adapter = new LinuxAdapter();
const internal = adapter as any;
Expand Down
94 changes: 94 additions & 0 deletions test/unit/monitors/cpu-monitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,100 @@ describe('CPUMonitor', () => {
});
});

describe('CPUMonitor — excludeIowait 配置项', () => {
// adapter 返回 overall=50, iowait=5
const usageWithIowait = {
overall: 50,
cores: [],
user: 30,
system: 15,
idle: 50,
iowait: 5,
irq: 0,
softirq: 0
};

function createAdapter(): PlatformAdapter {
return {
getPlatform: () => 'linux',
isSupported: () => true,
executeCommand: async () => ({ stdout: '', stderr: '', exitCode: 0, platform: 'linux', executionTime: 0, command: '' }),
readFile: async () => '',
fileExists: async () => true,
getCPUInfo: async () => ({}),
getCPUUsage: async () => usageWithIowait,
getCPUTemperature: async () => [],
getMemoryInfo: async () => ({}),
getMemoryUsage: async () => ({}),
getDiskInfo: async () => ({}),
getDiskIO: async () => ({}),
getNetworkInterfaces: async () => ({}),
getNetworkStats: async () => ({}),
getProcesses: async () => [],
getProcessInfo: async () => ({}),
getSystemInfo: async () => ({}),
getSystemLoad: async () => ({ load1: 0, load5: 0, load15: 0 }),
getDiskUsage: async () => ({}),
getDiskStats: async () => ({}),
getMounts: async () => ({}),
getFileSystems: async () => ({}),
getNetworkConnections: async () => [],
getDefaultGateway: async () => ({}),
getProcessList: async () => [],
killProcess: async () => true,
getProcessOpenFiles: async () => [],
getProcessEnvironment: async () => ({}),
getSystemUptime: async () => ({}),
getSystemUsers: async () => [],
getSystemServices: async () => [],
getSupportedFeatures: () => ({
cpu: { info: true, usage: true, temperature: false, frequency: false, cache: false, perCore: false, cores: false },
memory: { info: true, usage: true, swap: false, pressure: false, detailed: false, virtual: false },
disk: { info: true, io: false, health: false, smart: false, filesystem: false, usage: true, stats: false, mounts: false, filesystems: false },
network: { interfaces: false, stats: false, connections: false, bandwidth: false, gateway: false },
process: { list: false, details: false, tree: false, monitor: false, info: false, kill: false, openFiles: false, environment: false },
system: { info: false, load: false, uptime: false, users: false, services: false }
})
} as PlatformAdapter;
}

it('默认(excludeIowait=false):overall 包含 iowait', async () => {
const monitor = new CPUMonitor(createAdapter());
const result = await monitor.usageDetailed();
expect(result.success).to.be.true;
if (!result.success) return;
expect(result.data.overall).to.equal(50);
expect(result.data.iowait).to.equal(5);
});

it('excludeIowait=true:overall 应减去 iowait', async () => {
const monitor = new CPUMonitor(createAdapter(), { excludeIowait: true });
const result = await monitor.usageDetailed();
expect(result.success).to.be.true;
if (!result.success) return;
expect(result.data.overall).to.equal(45); // 50 - 5
expect(result.data.iowait).to.equal(5); // iowait 字段本身不受影响
});

it('excludeIowait=true:usage() 简单接口也同步排除 iowait', async () => {
const monitor = new CPUMonitor(createAdapter(), { excludeIowait: true });
const result = await monitor.usage();
expect(result.success).to.be.true;
if (!result.success) return;
expect(result.data).to.equal(45);
});

it('excludeIowait=true:iowait 大于 overall 时 overall 不应为负数', async () => {
const adapter = createAdapter();
(adapter as any).getCPUUsage = async () => ({ overall: 3, iowait: 5, cores: [], user: 0, system: 0, idle: 97, irq: 0, softirq: 0 });
const monitor = new CPUMonitor(adapter, { excludeIowait: true });
const result = await monitor.usageDetailed();
expect(result.success).to.be.true;
if (!result.success) return;
expect(result.data.overall).to.equal(0);
});
});

describe('CPUMonitor — Deno 兼容性降级', () => {
function createFailingAdapter(): PlatformAdapter {
const base = {
Expand Down
Loading