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
41 changes: 41 additions & 0 deletions .github/workflows/deno-smoke-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Deno Compatibility Smoke Test

on:
push:
branches: [master, develop, '003-fix-deno-compat']
pull_request:
branches: [master]

jobs:
deno-smoke-test:
name: Deno Smoke Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
# 非阻断性:Deno 兼容层问题不应阻止合并,仅作可观测性指标
continue-on-error: true

strategy:
matrix:
os: [ubuntu-latest, windows-latest]

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm install

- name: Build
run: npm run build

- name: Setup Deno
uses: denoland/setup-deno@v2
with:
deno-version: v2.x

- name: Run Deno smoke test
run: deno run --allow-read --allow-env --allow-sys test/deno/smoke-test.ts
timeout-minutes: 2
33 changes: 33 additions & 0 deletions .github/workflows/unit-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Unit Tests

on:
push:
branches: [master, develop, '003-fix-deno-compat']
pull_request:
branches: [master]

jobs:
test:
name: Node.js ${{ matrix.node-version }}
runs-on: ubuntu-latest

strategy:
matrix:
node-version: ['18', '20', '22']

steps:
- uses: actions/checkout@v4

- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Install dependencies
run: npm install

- name: Build
run: npm run build

- name: Run unit tests
run: npm run test:unit
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,8 @@ docs/
*.swo
*~
.history
.specify
CLAUDE.md
AGENTS.md
.claude
specs
34 changes: 0 additions & 34 deletions AGENTS.md

This file was deleted.

36 changes: 36 additions & 0 deletions README-zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,42 @@ const config = {
- [node-machine-id](https://github.com/automation-stack/node-machine-id) - 唯一机器标识
- [cpu-features](https://github.com/mscdex/cpu-features) - CPU 特性检测

## 🦕 Deno 兼容性

`node-os-utils` 支持在 Deno 的 Node.js 兼容层(`deno run --node-modules-dir`)下运行。当 Deno 的兼容层无法执行原生 shell 命令(如 Windows 上的 PowerShell),库会**优雅降级**而非抛出异常:

| 操作 | 降级行为 |
|------|---------|
| `cpu.info()` | 降级到 `os.cpus()` 基础数据 |
| `memory.info()` | 降级到 `os.totalmem()` / `os.freemem()` 基础数据 |
| `disk.info()`、`network.stats()`、`process.list()` | 返回 `success: false` 的 `MonitorResult` |

首次降级时会输出一次性警告:

```
[node-os-utils] cpu degraded: Windows PowerShell/WMI unavailable, falling back to os.cpus() data. Some features may not be available in the current runtime environment.
```

**示例:**
```ts
// deno run --allow-read --allow-env --allow-sys app.ts
import { createOSUtils } from 'node-os-utils';

const utils = createOSUtils();
const cpu = await utils.cpu.info();
if (cpu.success) {
console.log(cpu.data.threads); // Deno 下也能正常工作
} else {
console.log('CPU 信息不可用:', cpu.error.message);
}
```

## 特性标志同步说明

当监控器启用降级模式时,适配器的 `getSupportedFeatures()` 返回的特性标志可能仍显示 `true`,
但实际上某些功能已降级。建议在捕获到 `MonitorResult.success === false` 时以结果为准,
而非依赖特性标志进行预检查。

## ❓ 常见问题

**问:为什么某些功能在 Windows 上不工作?**
Expand Down
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,36 @@ const config = {
- [node-machine-id](https://github.com/automation-stack/node-machine-id) - Unique machine identification
- [cpu-features](https://github.com/mscdex/cpu-features) - CPU feature detection

## 🦕 Deno Compatibility

`node-os-utils` works under Deno's Node.js compatibility layer (`deno run --node-modules-dir`). When Deno's compat layer cannot execute native shell commands (e.g. PowerShell on Windows), the library **degrades gracefully** rather than throwing:

| Operation | Degraded Behavior |
|-----------|-------------------|
| `cpu.info()` | Falls back to `os.cpus()` data |
| `memory.info()` | Falls back to `os.totalmem()` / `os.freemem()` |
| `disk.info()`, `network.stats()`, `process.list()` | Returns `MonitorResult` with `success: false` |

A one-time warning is emitted on first degradation:

```
[node-os-utils] cpu degraded: Windows PowerShell/WMI unavailable, falling back to os.cpus() data. Some features may not be available in the current runtime environment.
```

**Example:**
```ts
// deno run --allow-read --allow-env --allow-sys app.ts
import { createOSUtils } from 'node-os-utils';

const utils = createOSUtils();
const cpu = await utils.cpu.info();
if (cpu.success) {
console.log(cpu.data.threads); // works even in Deno
} else {
console.log('CPU info not available:', cpu.error.message);
}
```

## ❓ FAQ

**Q: Why does some functionality not work on Windows?**
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"name": "node-os-utils",
"version": "2.0.1",
"version": "2.0.2",
"description": "Advanced cross-platform operating system monitoring utilities with TypeScript support",
"type": "commonjs",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
"exports": {
Expand Down
41 changes: 37 additions & 4 deletions src/adapters/linux-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import os from 'os';
import { promises as fs } from 'fs';
import * as fsSync from 'fs';
import { BasePlatformAdapter } from '../core/platform-adapter';
import { BaseMonitor } from '../core/base-monitor';
import { CommandExecutor } from '../utils/command-executor';
import { CommandResult, SupportedFeatures } from '../types/platform';
import { ExecuteOptions } from '../types/config';
Expand Down Expand Up @@ -118,8 +119,25 @@ export class LinuxAdapter extends BasePlatformAdapter {
try {
const cpuinfoContent = await this.readFile(this.paths.cpuinfo);
return this.parseCPUInfo(cpuinfoContent);
} catch (error) {
throw this.createCommandError('getCPUInfo', error);
} catch {
// /proc/cpuinfo 不可访问(如 Deno 兼容层),降级到 os.cpus() 基础数据
BaseMonitor.warnDegradation(
'cpu.command_failed',
'Linux /proc/cpuinfo unreadable, falling back to os.cpus() data'
);
const cpus = os.cpus();
const logicalCores = cpus.length || 1;
return {
model: cpus[0]?.model || 'Unknown',
manufacturer: 'Unknown',
architecture: os.arch(),
cores: Math.max(1, Math.floor(logicalCores / 2)),
threads: logicalCores,
baseFrequency: cpus[0]?.speed || 0,
maxFrequency: cpus[0]?.speed || 0,
cache: {},
features: []
};
}
}

Expand Down Expand Up @@ -186,8 +204,23 @@ export class LinuxAdapter extends BasePlatformAdapter {
try {
const meminfoContent = await this.readFile(this.paths.meminfo);
return this.parseMemoryInfo(meminfoContent);
} catch (error) {
throw this.createCommandError('getMemoryInfo', error);
} catch {
// /proc/meminfo 不可访问(如 Deno 兼容层权限限制),降级到 os 模块基础数据
BaseMonitor.warnDegradation(
'memory.command_failed',
'Linux /proc/meminfo unreadable, falling back to os.totalmem()/os.freemem() data'
);
const total = os.totalmem();
const free = os.freemem();
return {
total: Math.round(total / 1024),
free: Math.round(free / 1024),
used: Math.round((total - free) / 1024),
shared: 0,
buffers: 0,
cached: 0,
available: Math.round(free / 1024)
};
}
}

Expand Down
21 changes: 20 additions & 1 deletion src/adapters/macos-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os from 'os';

import { BasePlatformAdapter } from '../core/platform-adapter';
import { BaseMonitor } from '../core/base-monitor';
import { CommandExecutor } from '../utils/command-executor';
import { CommandResult, SupportedFeatures } from '../types/platform';
import { ExecuteOptions } from '../types/config';
Expand Down Expand Up @@ -80,7 +81,25 @@ export class MacOSAdapter extends BasePlatformAdapter {
results[3].status === 'fulfilled' ? results[3].value : null
]);

return this.parseCPUInfo(brand?.stdout || '', cores?.stdout || '', threads?.stdout || '', freq?.stdout || '');
const info = this.parseCPUInfo(brand?.stdout || '', cores?.stdout || '', threads?.stdout || '', freq?.stdout || '');

// 若 sysctl 命令全部失败,降级到 os.cpus() 基础数据
if (!info.cores || !info.threads) {
BaseMonitor.warnDegradation(
'cpu.command_failed',
'macOS sysctl unavailable, falling back to os.cpus() data'
);
const cpus = os.cpus();
const logicalCores = cpus.length || 1;
info.model = cpus[0]?.model || info.model || 'Unknown';
info.cores = Math.max(1, Math.floor(logicalCores / 2));
info.threads = logicalCores;
info.baseFrequency = cpus[0]?.speed || 0;
info.maxFrequency = cpus[0]?.speed || 0;
info.architecture = os.arch();
}

return info;
} catch (error) {
throw this.createCommandError('getCPUInfo', error);
}
Expand Down
23 changes: 18 additions & 5 deletions src/adapters/windows-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import os from 'os';
import { promises as fs } from 'fs';

import { BasePlatformAdapter } from '../core/platform-adapter';
import { BaseMonitor } from '../core/base-monitor';
import { CommandExecutor } from '../utils/command-executor';
import { CommandResult, SupportedFeatures } from '../types/platform';
import { ExecuteOptions } from '../types/config';
Expand Down Expand Up @@ -100,7 +101,11 @@ export class WindowsAdapter extends BasePlatformAdapter {
maxFrequency = this.safeParseInt(info.MaxClockSpeed, maxFrequency);
}
} catch {
// 忽略 WMI 错误,使用 Node.js 信息
// PowerShell/WMI 不可用(如 Deno 兼容层),降级到纯 os 模块数据
BaseMonitor.warnDegradation(
'cpu.command_failed',
'Windows PowerShell/WMI unavailable, falling back to os.cpus() data'
);
}

return {
Expand Down Expand Up @@ -176,7 +181,11 @@ export class WindowsAdapter extends BasePlatformAdapter {
};
}
} catch {
// 兼容性问题时忽略,保留基础信息
// PowerShell/WMI 不可用(如 Deno 兼容层),降级到纯 os 模块数据
BaseMonitor.warnDegradation(
'memory.command_failed',
'Windows PowerShell/WMI unavailable, falling back to os.totalmem()/os.freemem() data'
);
}

return memoryInfo;
Expand Down Expand Up @@ -269,10 +278,14 @@ export class WindowsAdapter extends BasePlatformAdapter {
}));
} catch (error: any) {
if (error instanceof MonitorError) {
return [];
throw error;
}

return [];
throw new MonitorError(
`getNetworkStats failed: ${error?.message || String(error)}`,
ErrorCode.COMMAND_FAILED,
'win32',
{ originalError: error }
);
}
}

Expand Down
19 changes: 19 additions & 0 deletions src/core/base-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,25 @@ export abstract class BaseMonitor<T> extends EventEmitter {
protected cache: CacheManager;
protected subscriptions: Set<MonitorSubscription> = new Set();

/** 已输出过降级警告的 key 集合(进程级别去重) */
private static readonly warnedDegradations = new Set<string>();

/**
* 输出首次降级警告(相同 key 仅警告一次)
* 供适配器和监控器在命令执行失败并降级时调用
* @param key 降级标识,格式 "{monitor}.{type}",如 "cpu.command_failed"
* @param reason 人类可读的降级原因
*/
static warnDegradation(key: string, reason: string): void {
if (!BaseMonitor.warnedDegradations.has(key)) {
BaseMonitor.warnedDegradations.add(key);
const monitor = key.split('.')[0];
console.warn(
`[node-os-utils] ${monitor} degraded: ${reason}. Some features may not be available in the current runtime environment.`
);
}
}

constructor(
adapter: PlatformAdapter,
config: MonitorConfig = {},
Expand Down
Loading
Loading