about-system 0.10.9 → 0.10.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/dist/about-system-cli.js +69 -54
- package/dist/about-system-cli.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +1 -1
- package/dist/system-info-api-D85U-_0W.js +2473 -0
- package/dist/system-info-api-D85U-_0W.js.map +1 -0
- package/dist/system-info-api.d.ts +15 -25
- package/dist/system-info-api.js +4 -5
- package/dist/system-info-api.js.map +1 -1
- package/package.json +1 -1
- package/src/about-system-cli.ts +42 -3
- package/src/cache/cache-config.ts +3 -3
- package/src/info/network.ts +14 -47
- package/src/system-info-api.test.ts +106 -0
- package/src/system-info-api.ts +68 -94
- package/src/systeminfo-types.d.ts +6 -0
- package/src/utils/network.ts +142 -23
- package/src/utils/utils.test.ts +136 -60
- package/dist/system-info-api-NgTm8Gzo.js +0 -2428
- package/dist/system-info-api-NgTm8Gzo.js.map +0 -1
|
@@ -27,13 +27,6 @@ declare interface Cache {
|
|
|
27
27
|
[key: string]: CacheEntry;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
/**
|
|
31
|
-
* Cache storage structure
|
|
32
|
-
*/
|
|
33
|
-
declare interface Cache_2 {
|
|
34
|
-
[key: string]: CacheEntry_2;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
30
|
/**
|
|
38
31
|
* Cache management utilities
|
|
39
32
|
* @module cache
|
|
@@ -49,21 +42,13 @@ declare interface CacheEntry {
|
|
|
49
42
|
timestamp: number;
|
|
50
43
|
}
|
|
51
44
|
|
|
52
|
-
/**
|
|
53
|
-
* Represents a cached value with timestamp
|
|
54
|
-
*/
|
|
55
|
-
declare interface CacheEntry_2 {
|
|
56
|
-
value: any;
|
|
57
|
-
timestamp: number;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
45
|
/**
|
|
61
46
|
* Gets geographic city based on public IP
|
|
62
47
|
* @param context - Info context with IP geolocation data
|
|
63
48
|
* @returns City name or empty string
|
|
64
49
|
* @example "San Francisco", "New York", "London"
|
|
65
50
|
*/
|
|
66
|
-
declare function city(context: InfoContext):
|
|
51
|
+
declare function city(context: InfoContext): string;
|
|
67
52
|
|
|
68
53
|
/**
|
|
69
54
|
* Gets running Docker container names
|
|
@@ -131,10 +116,14 @@ declare function disk_used(context: InfoContext): string;
|
|
|
131
116
|
* @returns Domain with http:// prefix or empty string
|
|
132
117
|
* @example "http://example.com", "http://host-203-0-113-42.example.net"
|
|
133
118
|
*/
|
|
134
|
-
declare function domain(context: InfoContext):
|
|
119
|
+
declare function domain(context: InfoContext): string;
|
|
135
120
|
|
|
136
121
|
/**
|
|
137
|
-
* Get
|
|
122
|
+
* Get system information as a clean JSON object.
|
|
123
|
+
*
|
|
124
|
+
* `options.keys` limits collection to those blocks (all by default).
|
|
125
|
+
* `options.signal` stops collection early: whatever was gathered before the
|
|
126
|
+
* abort is returned, and the remaining blocks are left out.
|
|
138
127
|
*/
|
|
139
128
|
export declare function getSystemInfo(options?: SystemInfoOptions): Promise<SystemInfo>;
|
|
140
129
|
|
|
@@ -225,12 +214,13 @@ export declare const infoFunctions: {
|
|
|
225
214
|
};
|
|
226
215
|
|
|
227
216
|
/**
|
|
228
|
-
* Gets public IP address
|
|
229
|
-
*
|
|
230
|
-
* @
|
|
217
|
+
* Gets public IP address. The ip, city, domain and isp blocks all read the one
|
|
218
|
+
* shared lookup in `context.ipInfo`, which `getSystemInfo` caches for 10 minutes.
|
|
219
|
+
* @param context - Info context with IP info
|
|
220
|
+
* @returns Public IP address or empty string
|
|
231
221
|
* @example "203.0.113.42"
|
|
232
222
|
*/
|
|
233
|
-
declare function ip(context: InfoContext):
|
|
223
|
+
declare function ip(context: InfoContext): string;
|
|
234
224
|
|
|
235
225
|
/**
|
|
236
226
|
* Network utilities for IP information fetching
|
|
@@ -266,7 +256,7 @@ declare function iplocal(): string;
|
|
|
266
256
|
* @returns ISP name or empty string
|
|
267
257
|
* @example "Comcast Cable", "Verizon Business", "Cloudflare Inc"
|
|
268
258
|
*/
|
|
269
|
-
declare function isp(context: InfoContext):
|
|
259
|
+
declare function isp(context: InfoContext): string;
|
|
270
260
|
|
|
271
261
|
/**
|
|
272
262
|
* Gets kernel version string
|
|
@@ -289,7 +279,7 @@ declare function load_average(context: InfoContext): string;
|
|
|
289
279
|
/**
|
|
290
280
|
* Loads cache from disk
|
|
291
281
|
*/
|
|
292
|
-
export declare function loadCache():
|
|
282
|
+
export declare function loadCache(): Cache;
|
|
293
283
|
|
|
294
284
|
/**
|
|
295
285
|
* Gets available memory in gigabytes
|
|
@@ -361,7 +351,7 @@ declare function ram_used(context: InfoContext): string;
|
|
|
361
351
|
/**
|
|
362
352
|
* Saves cache to disk
|
|
363
353
|
*/
|
|
364
|
-
export declare function saveCache(cache:
|
|
354
|
+
export declare function saveCache(cache: Cache): void;
|
|
365
355
|
|
|
366
356
|
/**
|
|
367
357
|
* Gets screen resolution
|
package/dist/system-info-api.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import "os";
|
|
2
2
|
import "fs";
|
|
3
|
-
import "
|
|
3
|
+
import { g as i, i as m, l as r, s as n } from "./system-info-api-D85U-_0W.js";
|
|
4
4
|
import "path";
|
|
5
|
-
import { g as m, i as r, l as p, s as n } from "./system-info-api-NgTm8Gzo.js";
|
|
6
5
|
export {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
i as getSystemInfo,
|
|
7
|
+
m as infoFunctions,
|
|
8
|
+
r as loadCache,
|
|
10
9
|
n as saveCache
|
|
11
10
|
};
|
|
12
11
|
//# sourceMappingURL=system-info-api.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"system-info-api.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"system-info-api.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "about-system",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.11",
|
|
4
4
|
"description": "A Node.js script to display key system information with emojis. Cross-platform support for Windows, macOS, and Linux with customizable output and caching.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/src/about-system-cli.ts
CHANGED
|
@@ -146,14 +146,51 @@ function wrapAnsiText(text: string, maxLength: number): string[] {
|
|
|
146
146
|
return lines;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Collects system info, stopping early when Esc is pressed. On Esc the blocks
|
|
151
|
+
* gathered so far are returned and the rest are skipped. Only listens when
|
|
152
|
+
* stdin is an interactive terminal; raw mode swallows Ctrl+C, so that is
|
|
153
|
+
* handled here too.
|
|
154
|
+
*/
|
|
155
|
+
async function collectSystemInfo(keys?: string[]): Promise<SystemInfo> {
|
|
156
|
+
const controller = new AbortController();
|
|
157
|
+
const stdin = process.stdin;
|
|
158
|
+
const listen = stdin.isTTY && typeof stdin.setRawMode === "function";
|
|
159
|
+
|
|
160
|
+
const stopListening = () => {
|
|
161
|
+
stdin.off("data", onKey);
|
|
162
|
+
stdin.setRawMode(false);
|
|
163
|
+
stdin.pause();
|
|
164
|
+
};
|
|
165
|
+
const onKey = (data: Buffer) => {
|
|
166
|
+
// A lone ESC byte; arrow keys etc. arrive as longer ESC sequences.
|
|
167
|
+
if (data.length === 1 && data[0] === 0x1b) controller.abort();
|
|
168
|
+
else if (data.includes(0x03)) {
|
|
169
|
+
stopListening();
|
|
170
|
+
process.exit(130);
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (listen) {
|
|
175
|
+
stdin.setRawMode(true);
|
|
176
|
+
stdin.on("data", onKey);
|
|
177
|
+
stdin.resume();
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
return await getSystemInfo({ keys, signal: controller.signal });
|
|
181
|
+
} finally {
|
|
182
|
+
if (listen) stopListening();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
149
186
|
async function displaySystemInfo(
|
|
150
187
|
customDisplayOrder: string[][] | null = null
|
|
151
188
|
): Promise<void> {
|
|
152
189
|
const settings = loadSettings();
|
|
153
190
|
const displayOrder = customDisplayOrder || settings.display_order;
|
|
154
191
|
|
|
155
|
-
// Get
|
|
156
|
-
const info = await
|
|
192
|
+
// Get only the blocks that will be shown
|
|
193
|
+
const info = await collectSystemInfo(displayOrder.flat());
|
|
157
194
|
|
|
158
195
|
// Single line mode
|
|
159
196
|
if (settings.display.single_line) {
|
|
@@ -446,6 +483,8 @@ Options:
|
|
|
446
483
|
--set <key> <value> Set a configuration value (use dot notation)
|
|
447
484
|
--json Output as JSON
|
|
448
485
|
|
|
486
|
+
Press Esc while it runs to stop and print only what was collected so far.
|
|
487
|
+
|
|
449
488
|
Examples:
|
|
450
489
|
about-system # Show all info (default)
|
|
451
490
|
about-system cpu,os # Show only CPU and OS info
|
|
@@ -507,7 +546,7 @@ async function main(): Promise<void> {
|
|
|
507
546
|
}
|
|
508
547
|
|
|
509
548
|
if (args.includes("--json")) {
|
|
510
|
-
const info = await
|
|
549
|
+
const info = await collectSystemInfo();
|
|
511
550
|
console.log(JSON.stringify(info, null, 2));
|
|
512
551
|
return;
|
|
513
552
|
}
|
|
@@ -17,7 +17,7 @@ export const CACHE_FILE = path.join(os.tmpdir(), "systeminfo-cache.json");
|
|
|
17
17
|
* Values are in milliseconds
|
|
18
18
|
*
|
|
19
19
|
* @constant
|
|
20
|
-
* @property {number}
|
|
20
|
+
* @property {number} ipInfo - Public IP / city / ISP lookup (10 minutes)
|
|
21
21
|
* @property {number} cpu - CPU information cache (24 hours)
|
|
22
22
|
* @property {number} gpu - GPU information cache (24 hours)
|
|
23
23
|
* @property {number} os - OS information cache (24 hours)
|
|
@@ -36,7 +36,7 @@ export const CACHE_FILE = path.join(os.tmpdir(), "systeminfo-cache.json");
|
|
|
36
36
|
* @property {number} mount_points - Mount points cache (10 minutes)
|
|
37
37
|
*/
|
|
38
38
|
export const CACHE_DURATION = {
|
|
39
|
-
|
|
39
|
+
ipInfo: 10 * 60 * 1000,
|
|
40
40
|
cpu: 24 * 60 * 60 * 1000,
|
|
41
41
|
gpu: 24 * 60 * 60 * 1000,
|
|
42
42
|
os: 24 * 60 * 60 * 1000,
|
|
@@ -65,4 +65,4 @@ export const DEFAULT_IPINFO_TOKEN = "da2d6cc4baa5d1";
|
|
|
65
65
|
* Default network request timeout in milliseconds
|
|
66
66
|
* @constant
|
|
67
67
|
*/
|
|
68
|
-
export const DEFAULT_NETWORK_TIMEOUT =
|
|
68
|
+
export const DEFAULT_NETWORK_TIMEOUT = 3000;
|
package/src/info/network.ts
CHANGED
|
@@ -10,22 +10,14 @@ import { execCommand } from "../utils/command";
|
|
|
10
10
|
import { getCachedValue, setCachedValue } from "../cache/cache";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* Gets public IP address
|
|
14
|
-
*
|
|
15
|
-
* @
|
|
13
|
+
* Gets public IP address. The ip, city, domain and isp blocks all read the one
|
|
14
|
+
* shared lookup in `context.ipInfo`, which `getSystemInfo` caches for 10 minutes.
|
|
15
|
+
* @param context - Info context with IP info
|
|
16
|
+
* @returns Public IP address or empty string
|
|
16
17
|
* @example "203.0.113.42"
|
|
17
18
|
*/
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
if (cached) return cached;
|
|
21
|
-
|
|
22
|
-
if (!context.ipInfo) {
|
|
23
|
-
setCachedValue(context.cache, "ip", "");
|
|
24
|
-
return "";
|
|
25
|
-
}
|
|
26
|
-
const ip = context.ipInfo.ip || "";
|
|
27
|
-
setCachedValue(context.cache, "ip", ip);
|
|
28
|
-
return ip;
|
|
19
|
+
export function ip(context: InfoContext): string {
|
|
20
|
+
return context.ipInfo?.ip || "";
|
|
29
21
|
}
|
|
30
22
|
|
|
31
23
|
/**
|
|
@@ -81,17 +73,8 @@ export function iplocal(): string {
|
|
|
81
73
|
* @returns City name or empty string
|
|
82
74
|
* @example "San Francisco", "New York", "London"
|
|
83
75
|
*/
|
|
84
|
-
export
|
|
85
|
-
|
|
86
|
-
if (cached !== null) return cached;
|
|
87
|
-
|
|
88
|
-
if (!context.ipInfo || !context.ipInfo.city) {
|
|
89
|
-
setCachedValue(context.cache, "city", "");
|
|
90
|
-
return "";
|
|
91
|
-
}
|
|
92
|
-
const city = context.ipInfo.city;
|
|
93
|
-
setCachedValue(context.cache, "city", city);
|
|
94
|
-
return city;
|
|
76
|
+
export function city(context: InfoContext): string {
|
|
77
|
+
return context.ipInfo?.city || "";
|
|
95
78
|
}
|
|
96
79
|
|
|
97
80
|
/**
|
|
@@ -100,17 +83,9 @@ export async function city(context: InfoContext): Promise<string> {
|
|
|
100
83
|
* @returns Domain with http:// prefix or empty string
|
|
101
84
|
* @example "http://example.com", "http://host-203-0-113-42.example.net"
|
|
102
85
|
*/
|
|
103
|
-
export
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (!context.ipInfo || !context.ipInfo.hostname) {
|
|
108
|
-
setCachedValue(context.cache, "domain", "");
|
|
109
|
-
return "";
|
|
110
|
-
}
|
|
111
|
-
const domain = `http://${context.ipInfo.hostname}`;
|
|
112
|
-
setCachedValue(context.cache, "domain", domain);
|
|
113
|
-
return domain;
|
|
86
|
+
export function domain(context: InfoContext): string {
|
|
87
|
+
const hostname = context.ipInfo?.hostname;
|
|
88
|
+
return hostname ? `http://${hostname}` : "";
|
|
114
89
|
}
|
|
115
90
|
|
|
116
91
|
/**
|
|
@@ -120,17 +95,9 @@ export async function domain(context: InfoContext): Promise<string> {
|
|
|
120
95
|
* @returns ISP name or empty string
|
|
121
96
|
* @example "Comcast Cable", "Verizon Business", "Cloudflare Inc"
|
|
122
97
|
*/
|
|
123
|
-
export
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (!context.ipInfo || !context.ipInfo.org) {
|
|
128
|
-
setCachedValue(context.cache, "isp", "");
|
|
129
|
-
return "";
|
|
130
|
-
}
|
|
131
|
-
const isp = context.ipInfo.org.split(" ").slice(1).join(" ");
|
|
132
|
-
setCachedValue(context.cache, "isp", isp);
|
|
133
|
-
return isp;
|
|
98
|
+
export function isp(context: InfoContext): string {
|
|
99
|
+
const org = context.ipInfo?.org;
|
|
100
|
+
return org ? org.split(" ").slice(1).join(" ") : "";
|
|
134
101
|
}
|
|
135
102
|
|
|
136
103
|
/**
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Covers how getSystemInfo spends its time: only the requested
|
|
3
|
+
* blocks run, an abort returns what was gathered so far, and the public IP
|
|
4
|
+
* lookup hits the network at most once per 10 minutes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
8
|
+
|
|
9
|
+
const fetchIPInfo = vi.hoisted(() => vi.fn());
|
|
10
|
+
vi.mock("./utils/network", () => ({ fetchIPInfo }));
|
|
11
|
+
|
|
12
|
+
// Keep the on-disk cache in memory so tests don't touch the real temp file.
|
|
13
|
+
const disk = vi.hoisted(() => ({ cache: undefined as string | undefined }));
|
|
14
|
+
vi.mock("fs", async (importOriginal) => {
|
|
15
|
+
const real = await importOriginal<typeof import("fs")>();
|
|
16
|
+
const fake = {
|
|
17
|
+
...real,
|
|
18
|
+
existsSync: (p: string) =>
|
|
19
|
+
String(p).endsWith("systeminfo-cache.json") ? disk.cache !== undefined : real.existsSync(p),
|
|
20
|
+
readFileSync: (p: string, ...rest: any[]) =>
|
|
21
|
+
String(p).endsWith("systeminfo-cache.json") ? disk.cache : (real.readFileSync as any)(p, ...rest),
|
|
22
|
+
writeFileSync: (p: string, data: string) => {
|
|
23
|
+
if (String(p).endsWith("systeminfo-cache.json")) disk.cache = data;
|
|
24
|
+
},
|
|
25
|
+
mkdirSync: () => undefined,
|
|
26
|
+
};
|
|
27
|
+
return { ...fake, default: fake };
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
import { getSystemInfo } from "./system-info-api";
|
|
31
|
+
|
|
32
|
+
const TEN_MINUTES = 10 * 60 * 1000;
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
disk.cache = undefined;
|
|
36
|
+
fetchIPInfo.mockResolvedValue({ ip: "203.0.113.7", city: "Oslo", org: "AS1 Example Net" });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
fetchIPInfo.mockReset();
|
|
41
|
+
vi.useRealTimers();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("getSystemInfo", () => {
|
|
45
|
+
it("collects only the requested blocks", async () => {
|
|
46
|
+
const info = await getSystemInfo({ keys: ["user", "hostname"] });
|
|
47
|
+
|
|
48
|
+
expect(Object.keys(info).sort()).toEqual(["hostname", "platform", "timestamp", "user"]);
|
|
49
|
+
expect(fetchIPInfo).not.toHaveBeenCalled();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("derives ip, city and isp from one lookup", async () => {
|
|
53
|
+
const info = await getSystemInfo({ keys: ["ip", "city", "isp"] });
|
|
54
|
+
|
|
55
|
+
expect(info).toMatchObject({ ip: "203.0.113.7", city: "Oslo", isp: "Example Net" });
|
|
56
|
+
expect(fetchIPInfo).toHaveBeenCalledTimes(1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("reuses the IP lookup for 10 minutes, then refreshes it", async () => {
|
|
60
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
61
|
+
|
|
62
|
+
await getSystemInfo({ keys: ["ip"] });
|
|
63
|
+
vi.advanceTimersByTime(TEN_MINUTES - 1000);
|
|
64
|
+
await getSystemInfo({ keys: ["ip"] });
|
|
65
|
+
expect(fetchIPInfo).toHaveBeenCalledTimes(1);
|
|
66
|
+
|
|
67
|
+
vi.advanceTimersByTime(2000);
|
|
68
|
+
fetchIPInfo.mockResolvedValue({ ip: "198.51.100.9" });
|
|
69
|
+
const info = await getSystemInfo({ keys: ["ip"] });
|
|
70
|
+
expect(fetchIPInfo).toHaveBeenCalledTimes(2);
|
|
71
|
+
expect(info.ip).toBe("198.51.100.9");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("keeps the last good IP when a refresh fails, and waits 10 minutes to retry", async () => {
|
|
75
|
+
vi.useFakeTimers({ toFake: ["Date"] });
|
|
76
|
+
|
|
77
|
+
await getSystemInfo({ keys: ["ip"] });
|
|
78
|
+
vi.advanceTimersByTime(TEN_MINUTES + 1000);
|
|
79
|
+
fetchIPInfo.mockResolvedValue({});
|
|
80
|
+
|
|
81
|
+
expect((await getSystemInfo({ keys: ["ip"] })).ip).toBe("203.0.113.7");
|
|
82
|
+
await getSystemInfo({ keys: ["ip"] });
|
|
83
|
+
expect(fetchIPInfo).toHaveBeenCalledTimes(2);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("returns no blocks when aborted before starting", async () => {
|
|
87
|
+
const controller = new AbortController();
|
|
88
|
+
controller.abort();
|
|
89
|
+
|
|
90
|
+
const info = await getSystemInfo({ keys: ["user", "hostname", "cpu"], signal: controller.signal });
|
|
91
|
+
|
|
92
|
+
expect(Object.keys(info).sort()).toEqual(["platform", "timestamp"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("stops at the abort and returns what was gathered", async () => {
|
|
96
|
+
const controller = new AbortController();
|
|
97
|
+
const pending = getSystemInfo({ keys: ["user", "hostname", "cpu", "kernel"], signal: controller.signal });
|
|
98
|
+
// Blocks yield to the event loop between each other; abort after the first.
|
|
99
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
100
|
+
controller.abort();
|
|
101
|
+
|
|
102
|
+
const info = await pending;
|
|
103
|
+
expect(info.user).toBeDefined();
|
|
104
|
+
expect(info.kernel).toBeUndefined();
|
|
105
|
+
});
|
|
106
|
+
});
|
package/src/system-info-api.ts
CHANGED
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
|
|
12
12
|
import os from "os";
|
|
13
13
|
import fs from "fs";
|
|
14
|
-
import
|
|
14
|
+
import { fetchIPInfo } from "./utils/network";
|
|
15
|
+
import type { IPInfo } from "./utils/network";
|
|
15
16
|
import path from "path";
|
|
16
17
|
import type { SystemInfo, SystemInfoOptions } from "./systeminfo-types";
|
|
18
|
+
import type { Cache } from "./cache/cache";
|
|
19
|
+
import { isCacheValid } from "./cache/cache";
|
|
17
20
|
import { CACHE_FILE } from "./info/settings"; // Using CACHE_FILE from settings to ensure consistency
|
|
18
21
|
|
|
19
22
|
// Import info functions from modules
|
|
@@ -50,7 +53,7 @@ import { shell, packages, containers } from "./info/software";
|
|
|
50
53
|
* Values are in milliseconds
|
|
51
54
|
*/
|
|
52
55
|
const CACHE_DURATION = {
|
|
53
|
-
ip:
|
|
56
|
+
ip: 10 * 60 * 1000,
|
|
54
57
|
cpu: 24 * 60 * 60 * 1000,
|
|
55
58
|
gpu: 24 * 60 * 60 * 1000,
|
|
56
59
|
bench: 24 * 60 * 60 * 1000,
|
|
@@ -81,41 +84,6 @@ const IS_WINDOWS = os.platform() === "win32";
|
|
|
81
84
|
const IS_MAC = os.platform() === "darwin";
|
|
82
85
|
const IS_LINUX = os.platform() === "linux";
|
|
83
86
|
|
|
84
|
-
/**
|
|
85
|
-
* Default IPInfo.io API token for geolocation
|
|
86
|
-
*/
|
|
87
|
-
const DEFAULT_IPINFO_TOKEN = "da2d6cc4baa5d1";
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Default network request timeout in milliseconds
|
|
91
|
-
*/
|
|
92
|
-
const DEFAULT_NETWORK_TIMEOUT = 5000;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Represents a cached value with timestamp
|
|
96
|
-
*/
|
|
97
|
-
interface CacheEntry {
|
|
98
|
-
value: any;
|
|
99
|
-
timestamp: number;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Cache storage structure
|
|
104
|
-
*/
|
|
105
|
-
interface Cache {
|
|
106
|
-
[key: string]: CacheEntry;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* IP information from ipinfo.io API
|
|
111
|
-
*/
|
|
112
|
-
interface IPInfo {
|
|
113
|
-
ip?: string;
|
|
114
|
-
city?: string;
|
|
115
|
-
hostname?: string;
|
|
116
|
-
org?: string;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
87
|
/**
|
|
120
88
|
* Context object passed to info collection functions
|
|
121
89
|
*/
|
|
@@ -153,36 +121,6 @@ function saveCache(cache: Cache): void {
|
|
|
153
121
|
}
|
|
154
122
|
}
|
|
155
123
|
|
|
156
|
-
/**
|
|
157
|
-
* Fetches IP geolocation information from ipinfo.io API
|
|
158
|
-
*/
|
|
159
|
-
async function fetchIPInfo(
|
|
160
|
-
token: string = DEFAULT_IPINFO_TOKEN,
|
|
161
|
-
timeout: number = DEFAULT_NETWORK_TIMEOUT
|
|
162
|
-
): Promise<IPInfo> {
|
|
163
|
-
return new Promise((resolve) => {
|
|
164
|
-
const url = `https://ipinfo.io/json${token ? `?token=${token}` : ""}`;
|
|
165
|
-
|
|
166
|
-
const req = https.get(url, (res) => {
|
|
167
|
-
let data = "";
|
|
168
|
-
res.on("data", (chunk) => (data += chunk));
|
|
169
|
-
res.on("end", () => {
|
|
170
|
-
try {
|
|
171
|
-
resolve(JSON.parse(data));
|
|
172
|
-
} catch {
|
|
173
|
-
resolve({});
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
req.on("error", () => resolve({}));
|
|
179
|
-
req.setTimeout(timeout, () => {
|
|
180
|
-
req.destroy();
|
|
181
|
-
resolve({});
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
|
|
186
124
|
/**
|
|
187
125
|
* System information collection functions map
|
|
188
126
|
*/
|
|
@@ -224,34 +162,78 @@ export const infoFunctions = {
|
|
|
224
162
|
screen_resolution,
|
|
225
163
|
};
|
|
226
164
|
|
|
165
|
+
/** Blocks derived from the public IP lookup. */
|
|
166
|
+
const IP_BLOCKS = new Set(["ip", "city", "domain", "isp"]);
|
|
167
|
+
|
|
168
|
+
/** Lets pending I/O callbacks (keypresses, HTTP responses) run between blocks. */
|
|
169
|
+
const yieldToEventLoop = () => new Promise<void>((resolve) => setImmediate(resolve));
|
|
170
|
+
|
|
227
171
|
/**
|
|
228
|
-
*
|
|
172
|
+
* Returns the cached public IP lookup, or fetches it when the cache entry is
|
|
173
|
+
* older than 10 minutes. A failed fetch keeps the previous value and is still
|
|
174
|
+
* timestamped, so an offline machine is not re-queried on every launch. An
|
|
175
|
+
* aborted fetch leaves the cache untouched.
|
|
176
|
+
*/
|
|
177
|
+
async function getIPInfo(cache: Cache, signal?: AbortSignal): Promise<IPInfo> {
|
|
178
|
+
const entry = cache["ipInfo"];
|
|
179
|
+
if (entry && isCacheValid(entry, "ipInfo")) return entry.value || {};
|
|
180
|
+
|
|
181
|
+
const fresh = await fetchIPInfo(undefined, undefined, signal);
|
|
182
|
+
const value = fresh.ip ? fresh : entry?.value || {};
|
|
183
|
+
if (!signal?.aborted) {
|
|
184
|
+
cache["ipInfo"] = { value, timestamp: Date.now() };
|
|
185
|
+
}
|
|
186
|
+
return value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Get system information as a clean JSON object.
|
|
191
|
+
*
|
|
192
|
+
* `options.keys` limits collection to those blocks (all by default).
|
|
193
|
+
* `options.signal` stops collection early: whatever was gathered before the
|
|
194
|
+
* abort is returned, and the remaining blocks are left out.
|
|
229
195
|
*/
|
|
230
196
|
export async function getSystemInfo(
|
|
231
197
|
options: SystemInfoOptions = {}
|
|
232
198
|
): Promise<SystemInfo> {
|
|
199
|
+
const { keys, signal } = options;
|
|
233
200
|
const cache = loadCache();
|
|
234
201
|
const context: InfoContext = { cache };
|
|
235
202
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
// Optimization: check if we actually need to run those functions based on options if provided
|
|
240
|
-
// But current implementation fetches it always if missing from cache
|
|
203
|
+
const wanted = Object.keys(infoFunctions).filter(
|
|
204
|
+
(key) => !keys || keys.includes(key)
|
|
205
|
+
);
|
|
241
206
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
207
|
+
// Start the IP lookup first so it overlaps with the local blocks.
|
|
208
|
+
const ipInfoPromise = wanted.some((key) => IP_BLOCKS.has(key))
|
|
209
|
+
? getIPInfo(cache, signal)
|
|
210
|
+
: undefined;
|
|
211
|
+
|
|
212
|
+
const collected: Record<string, unknown> = {};
|
|
213
|
+
const run = async (key: string) => {
|
|
214
|
+
try {
|
|
215
|
+
collected[key] = await infoFunctions[key as keyof typeof infoFunctions](context);
|
|
216
|
+
} catch {
|
|
217
|
+
collected[key] = "";
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
for (const key of wanted) {
|
|
222
|
+
if (IP_BLOCKS.has(key)) continue;
|
|
223
|
+
await yieldToEventLoop();
|
|
224
|
+
if (signal?.aborted) break;
|
|
225
|
+
await run(key);
|
|
252
226
|
}
|
|
253
227
|
|
|
254
|
-
//
|
|
228
|
+
// On abort the lookup resolves at once with whatever was cached.
|
|
229
|
+
if (ipInfoPromise) {
|
|
230
|
+
context.ipInfo = await ipInfoPromise;
|
|
231
|
+
for (const key of wanted) {
|
|
232
|
+
if (IP_BLOCKS.has(key)) await run(key);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Keep the infoFunctions key order regardless of collection order.
|
|
255
237
|
const info: Partial<SystemInfo> = {
|
|
256
238
|
timestamp: new Date().toISOString(),
|
|
257
239
|
platform: IS_WINDOWS
|
|
@@ -262,16 +244,8 @@ export async function getSystemInfo(
|
|
|
262
244
|
? "linux"
|
|
263
245
|
: "unknown",
|
|
264
246
|
};
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
for (const [key, fn] of Object.entries(infoFunctions)) {
|
|
268
|
-
try {
|
|
269
|
-
// most functions are sync but some are async (ip related)
|
|
270
|
-
const value = await fn(context);
|
|
271
|
-
info[key as keyof SystemInfo] = value as any;
|
|
272
|
-
} catch (error) {
|
|
273
|
-
info[key as keyof SystemInfo] = "" as any;
|
|
274
|
-
}
|
|
247
|
+
for (const key of wanted) {
|
|
248
|
+
if (key in collected) info[key as keyof SystemInfo] = collected[key] as any;
|
|
275
249
|
}
|
|
276
250
|
|
|
277
251
|
saveCache(cache);
|
|
@@ -475,6 +475,12 @@ export interface SystemInfoOptions {
|
|
|
475
475
|
|
|
476
476
|
/** Return data object instead of printing to console (module usage) */
|
|
477
477
|
returnData?: boolean;
|
|
478
|
+
|
|
479
|
+
/** Only collect these blocks (e.g. ["cpu", "ip"]); all blocks when omitted */
|
|
480
|
+
keys?: string[];
|
|
481
|
+
|
|
482
|
+
/** Abort to stop collecting early and return only the blocks gathered so far */
|
|
483
|
+
signal?: AbortSignal;
|
|
478
484
|
}
|
|
479
485
|
|
|
480
486
|
/**
|