@nocville/npc 0.2.2 → 0.2.3
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 +23 -5
- package/dist/cjs/cli.js +55 -34
- package/dist/cjs/core/base-npc.js +8 -0
- package/dist/cjs/core/config.js +19 -2
- package/dist/cjs/core/errors.js +23 -0
- package/dist/cjs/core/http.js +121 -0
- package/dist/cjs/core/pixel-art.js +110 -0
- package/dist/cjs/core/polling-npc.js +99 -14
- package/dist/cjs/core/text.js +13 -0
- package/dist/cjs/core/thresholds.js +29 -0
- package/dist/cjs/index.js +10 -2
- package/dist/cjs/npcs/agent-common/session.js +1 -0
- package/dist/cjs/npcs/github-watcher/defaults.js +7 -1
- package/dist/cjs/npcs/github-watcher/github-api.js +82 -45
- package/dist/cjs/npcs/github-watcher/index.js +82 -36
- package/dist/cjs/npcs/internet-connectivity/checks.js +70 -67
- package/dist/cjs/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/cjs/npcs/internet-connectivity/index.js +134 -131
- package/dist/cjs/npcs/log-watcher/buffer.js +61 -0
- package/dist/cjs/npcs/log-watcher/index.js +109 -103
- package/dist/cjs/npcs/log-watcher/tail.js +100 -53
- package/dist/cjs/npcs/process-watcher/index.js +61 -51
- package/dist/cjs/npcs/process-watcher/process-scanner.js +63 -69
- package/dist/cjs/npcs/receptionist/defaults.js +4 -0
- package/dist/cjs/npcs/receptionist/index.js +45 -19
- package/dist/cjs/npcs/receptionist/weather.js +42 -47
- package/dist/cjs/npcs/system-monitor/collectors.js +66 -17
- package/dist/cjs/npcs/system-monitor/index.js +52 -28
- package/dist/cjs/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/cjs/npcs/web-page-watcher/capture.js +23 -1
- package/dist/cjs/npcs/web-page-watcher/defaults.js +44 -2
- package/dist/cjs/npcs/web-page-watcher/index.js +119 -68
- package/dist/cjs/npcs/web-page-watcher/pixelate.js +2 -1
- package/dist/cli.js +56 -35
- package/dist/core/base-npc.d.ts +5 -0
- package/dist/core/base-npc.js +8 -0
- package/dist/core/config.d.ts +5 -0
- package/dist/core/config.js +18 -2
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +18 -0
- package/dist/core/http.d.ts +52 -0
- package/dist/core/http.js +111 -0
- package/dist/core/pixel-art.d.ts +38 -0
- package/dist/core/pixel-art.js +105 -0
- package/dist/core/polling-npc.d.ts +22 -1
- package/dist/core/polling-npc.js +98 -13
- package/dist/core/text.d.ts +6 -0
- package/dist/core/text.js +12 -0
- package/dist/core/thresholds.d.ts +9 -1
- package/dist/core/thresholds.js +27 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +4 -2
- package/dist/npcs/agent-common/session.js +1 -0
- package/dist/npcs/github-watcher/defaults.d.ts +6 -0
- package/dist/npcs/github-watcher/defaults.js +7 -1
- package/dist/npcs/github-watcher/github-api.d.ts +21 -7
- package/dist/npcs/github-watcher/github-api.js +80 -45
- package/dist/npcs/github-watcher/index.d.ts +6 -0
- package/dist/npcs/github-watcher/index.js +83 -37
- package/dist/npcs/internet-connectivity/checks.d.ts +15 -18
- package/dist/npcs/internet-connectivity/checks.js +70 -67
- package/dist/npcs/internet-connectivity/defaults.d.ts +3 -0
- package/dist/npcs/internet-connectivity/defaults.js +5 -2
- package/dist/npcs/internet-connectivity/index.d.ts +19 -6
- package/dist/npcs/internet-connectivity/index.js +135 -133
- package/dist/npcs/log-watcher/buffer.d.ts +22 -0
- package/dist/npcs/log-watcher/buffer.js +58 -0
- package/dist/npcs/log-watcher/index.d.ts +22 -21
- package/dist/npcs/log-watcher/index.js +112 -106
- package/dist/npcs/log-watcher/tail.d.ts +23 -3
- package/dist/npcs/log-watcher/tail.js +101 -54
- package/dist/npcs/process-watcher/index.d.ts +1 -1
- package/dist/npcs/process-watcher/index.js +62 -52
- package/dist/npcs/process-watcher/process-scanner.d.ts +29 -1
- package/dist/npcs/process-watcher/process-scanner.js +61 -71
- package/dist/npcs/receptionist/defaults.d.ts +4 -0
- package/dist/npcs/receptionist/defaults.js +4 -0
- package/dist/npcs/receptionist/index.d.ts +12 -2
- package/dist/npcs/receptionist/index.js +46 -20
- package/dist/npcs/receptionist/weather.d.ts +8 -6
- package/dist/npcs/receptionist/weather.js +39 -45
- package/dist/npcs/system-monitor/collectors.d.ts +15 -3
- package/dist/npcs/system-monitor/collectors.js +64 -17
- package/dist/npcs/system-monitor/defaults.d.ts +1 -1
- package/dist/npcs/system-monitor/index.d.ts +2 -1
- package/dist/npcs/system-monitor/index.js +54 -30
- package/dist/npcs/web-page-watcher/basic-mode.d.ts +2 -2
- package/dist/npcs/web-page-watcher/basic-mode.js +30 -14
- package/dist/npcs/web-page-watcher/capture.d.ts +18 -0
- package/dist/npcs/web-page-watcher/capture.js +21 -1
- package/dist/npcs/web-page-watcher/defaults.d.ts +23 -0
- package/dist/npcs/web-page-watcher/defaults.js +41 -1
- package/dist/npcs/web-page-watcher/index.d.ts +18 -1
- package/dist/npcs/web-page-watcher/index.js +121 -70
- package/dist/npcs/web-page-watcher/pixelate.js +2 -1
- package/package.json +2 -2
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { PollingNPC } from '../../core/polling-npc.js';
|
|
2
2
|
import { truncate } from '../../core/text.js';
|
|
3
|
+
import { moodForStatus } from '../../core/thresholds.js';
|
|
4
|
+
import { NpcConfigError } from '../../core/errors.js';
|
|
3
5
|
import { DEFAULTS } from './defaults.js';
|
|
4
|
-
import { fetchCommits, fetchDefaultBranch, newCommitsSince, commitsWithin } from './github-api.js';
|
|
6
|
+
import { fetchCommits, fetchDefaultBranch, newCommitsSince, commitsWithin, isValidRepo, } from './github-api.js';
|
|
5
7
|
import { relativeTime } from './format.js';
|
|
6
8
|
export class GitHubWatcherNPC extends PollingNPC {
|
|
7
9
|
repo;
|
|
@@ -12,12 +14,24 @@ export class GitHubWatcherNPC extends PollingNPC {
|
|
|
12
14
|
lastSeenSha = null;
|
|
13
15
|
commits = [];
|
|
14
16
|
rateRemaining = null;
|
|
17
|
+
/** While rate-limited, don't call GitHub again before this time (epoch ms). */
|
|
18
|
+
rateLimitedUntil = 0;
|
|
19
|
+
consecutiveFailures = 0;
|
|
15
20
|
newCommitPollsLeft = 0;
|
|
16
21
|
constructor(config) {
|
|
17
|
-
super({
|
|
22
|
+
super({
|
|
23
|
+
...config,
|
|
24
|
+
// Unauthenticated requests get 60/hour and 304s still count against
|
|
25
|
+
// them, so a 60s poll (plus the branch lookup) would hit the limit.
|
|
26
|
+
pollInterval: config.pollInterval ??
|
|
27
|
+
(config.token ? DEFAULTS.pollInterval : DEFAULTS.unauthenticatedPollInterval),
|
|
28
|
+
});
|
|
29
|
+
if (!isValidRepo(config.repo)) {
|
|
30
|
+
throw new NpcConfigError(`--repo must look like owner/name (got "${truncate(config.repo, 80)}")`);
|
|
31
|
+
}
|
|
18
32
|
this.repo = config.repo;
|
|
19
|
-
this.token = config.token;
|
|
20
|
-
this.branch = config.branch;
|
|
33
|
+
this.token = config.token || undefined;
|
|
34
|
+
this.branch = config.branch?.trim() || undefined;
|
|
21
35
|
this.fetchFn = config.fetchFn;
|
|
22
36
|
}
|
|
23
37
|
get name() {
|
|
@@ -39,7 +53,8 @@ export class GitHubWatcherNPC extends PollingNPC {
|
|
|
39
53
|
};
|
|
40
54
|
}
|
|
41
55
|
async onStart() {
|
|
42
|
-
// Resolve the default branch once if the user didn't pin one
|
|
56
|
+
// Resolve the default branch once if the user didn't pin one (it only
|
|
57
|
+
// labels the board; the commits API already defaults to that branch).
|
|
43
58
|
if (!this.branch) {
|
|
44
59
|
const def = await fetchDefaultBranch(this.repo, {
|
|
45
60
|
token: this.token,
|
|
@@ -54,6 +69,8 @@ export class GitHubWatcherNPC extends PollingNPC {
|
|
|
54
69
|
await super.onStart();
|
|
55
70
|
}
|
|
56
71
|
async collect() {
|
|
72
|
+
if (Date.now() < this.rateLimitedUntil)
|
|
73
|
+
return this.rateLimited();
|
|
57
74
|
const result = await fetchCommits(this.repo, {
|
|
58
75
|
token: this.token,
|
|
59
76
|
etag: this.etag,
|
|
@@ -64,39 +81,68 @@ export class GitHubWatcherNPC extends PollingNPC {
|
|
|
64
81
|
});
|
|
65
82
|
if (result.rateRemaining !== null)
|
|
66
83
|
this.rateRemaining = result.rateRemaining;
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
const failure = this.classifyFailure(result);
|
|
85
|
+
if (failure)
|
|
86
|
+
return failure;
|
|
87
|
+
this.consecutiveFailures = 0;
|
|
88
|
+
if (result.notModified)
|
|
89
|
+
return this.reportIdle();
|
|
90
|
+
if (result.status === 409)
|
|
91
|
+
return this.report('healthy', 'Repository is empty');
|
|
92
|
+
this.etag = result.etag ?? this.etag;
|
|
93
|
+
if (result.commits.length === 0)
|
|
94
|
+
return this.reportIdle();
|
|
95
|
+
const fresh = newCommitsSince(result.commits, this.lastSeenSha);
|
|
96
|
+
this.commits = result.commits;
|
|
97
|
+
const seeded = this.lastSeenSha !== null;
|
|
98
|
+
this.lastSeenSha = result.commits[0].sha;
|
|
99
|
+
if (!seeded || fresh.length === 0)
|
|
100
|
+
return this.reportIdle();
|
|
101
|
+
this.newCommitPollsLeft = DEFAULTS.newCommitPolls;
|
|
102
|
+
return this.reportNewCommits(fresh);
|
|
103
|
+
}
|
|
104
|
+
/** Map an error response to a report, or null when the fetch succeeded. */
|
|
105
|
+
classifyFailure(result) {
|
|
106
|
+
const { status } = result;
|
|
107
|
+
if (status === 304 || status === 409 || (status >= 200 && status < 300))
|
|
108
|
+
return null;
|
|
109
|
+
if (status === 429 || (status === 403 && result.rateRemaining === 0)) {
|
|
110
|
+
this.rateLimitedUntil = result.rateResetAt ?? Date.now() + 15 * 60_000;
|
|
111
|
+
if (!this.token) {
|
|
112
|
+
this.logger.warn('GitHub rate limit reached; set GITHUB_TOKEN for a higher limit');
|
|
113
|
+
}
|
|
114
|
+
return this.rateLimited();
|
|
69
115
|
}
|
|
70
|
-
if (
|
|
71
|
-
return this.report('warning', '
|
|
116
|
+
if (status === 401) {
|
|
117
|
+
return this.report('warning', 'GitHub token rejected (401)', 'auth-failed');
|
|
72
118
|
}
|
|
73
|
-
if (
|
|
74
|
-
return this.report('warning',
|
|
119
|
+
if (status === 403) {
|
|
120
|
+
return this.report('warning', `Access to ${this.repo} denied (403)`, 'forbidden');
|
|
75
121
|
}
|
|
76
|
-
if (
|
|
77
|
-
|
|
122
|
+
if (status === 404) {
|
|
123
|
+
const hint = this.token ? '' : ' (private repos need GITHUB_TOKEN)';
|
|
124
|
+
return this.report('warning', `Repo not found: ${this.repo}${hint}`, 'not-found');
|
|
78
125
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (this.lastSeenSha === null) {
|
|
84
|
-
this.lastSeenSha = result.commits[0].sha;
|
|
85
|
-
return this.reportIdle();
|
|
86
|
-
}
|
|
87
|
-
if (fresh.length > 0) {
|
|
88
|
-
this.lastSeenSha = result.commits[0].sha;
|
|
89
|
-
this.newCommitPollsLeft = DEFAULTS.newCommitPolls;
|
|
90
|
-
return this.reportNewCommits(fresh);
|
|
91
|
-
}
|
|
126
|
+
// Network error, timeout or 5xx: ride out blips while there's a board to show.
|
|
127
|
+
this.consecutiveFailures++;
|
|
128
|
+
if (this.commits.length > 0 && this.consecutiveFailures < DEFAULTS.failuresBeforeWarning) {
|
|
129
|
+
return this.reportIdle();
|
|
92
130
|
}
|
|
93
|
-
|
|
131
|
+
const what = status === 0 ? 'unreachable' : `error (HTTP ${status})`;
|
|
132
|
+
return this.report('warning', `GitHub API ${what}`, 'unreachable');
|
|
133
|
+
}
|
|
134
|
+
rateLimited() {
|
|
135
|
+
const resumes = new Date(this.rateLimitedUntil).toLocaleTimeString([], {
|
|
136
|
+
hour: '2-digit',
|
|
137
|
+
minute: '2-digit',
|
|
138
|
+
});
|
|
139
|
+
return this.report('warning', `GitHub API rate-limited until ${resumes}`, 'rate-limited');
|
|
94
140
|
}
|
|
95
141
|
reportNewCommits(fresh) {
|
|
96
142
|
const newest = fresh[0];
|
|
97
143
|
const latest = `${newest.author}: ${truncate(newest.message, 120)} (${newest.sha.slice(0, 7)})`;
|
|
98
144
|
const message = fresh.length > 1 ? `${fresh.length} new commits — latest: ${latest}` : latest;
|
|
99
|
-
return this.report('healthy',
|
|
145
|
+
return this.report('healthy', message, 'new-commit');
|
|
100
146
|
}
|
|
101
147
|
reportIdle() {
|
|
102
148
|
if (this.newCommitPollsLeft > 0)
|
|
@@ -106,16 +152,16 @@ export class GitHubWatcherNPC extends PollingNPC {
|
|
|
106
152
|
const message = newest
|
|
107
153
|
? `Last: ${truncate(newest.message, 80)} (${relativeTime(newest.date)})`
|
|
108
154
|
: `Watching ${this.repo}`;
|
|
109
|
-
return this.report('healthy',
|
|
155
|
+
return this.report('healthy', message, subStatus);
|
|
110
156
|
}
|
|
111
|
-
report(status,
|
|
112
|
-
const
|
|
113
|
-
const secondsSinceLast =
|
|
114
|
-
? Math.max(0, Math.round((Date.now() -
|
|
157
|
+
report(status, message, subStatus) {
|
|
158
|
+
const newestAt = Date.parse(this.commits[0]?.date ?? '');
|
|
159
|
+
const secondsSinceLast = Number.isFinite(newestAt)
|
|
160
|
+
? Math.max(0, Math.round((Date.now() - newestAt) / 1000))
|
|
115
161
|
: 0;
|
|
116
162
|
return {
|
|
117
163
|
status,
|
|
118
|
-
mood,
|
|
164
|
+
mood: moodForStatus(status),
|
|
119
165
|
message,
|
|
120
166
|
subStatus,
|
|
121
167
|
screenData: this.buildBoard(),
|
|
@@ -129,9 +175,9 @@ export class GitHubWatcherNPC extends PollingNPC {
|
|
|
129
175
|
buildBoard() {
|
|
130
176
|
const now = Date.now();
|
|
131
177
|
const header = this.branch ? `${this.repo}@${this.branch}` : this.repo;
|
|
132
|
-
const values = [{ label: 'repo', value: header }];
|
|
133
|
-
for (const c of this.commits.slice(0,
|
|
134
|
-
const recent =
|
|
178
|
+
const values = [{ label: 'repo', value: truncate(header, 60) }];
|
|
179
|
+
for (const c of this.commits.slice(0, DEFAULTS.boardRows)) {
|
|
180
|
+
const recent = now - Date.parse(c.date) < 3600_000;
|
|
135
181
|
values.push({
|
|
136
182
|
label: c.sha.slice(0, 7),
|
|
137
183
|
value: truncate(`${c.author}: ${c.message}`, 60),
|
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
import type { LatencyHost } from './defaults.js';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/** One probe's outcome. `timeMs` is only meaningful when `ok`. */
|
|
3
|
+
export interface CheckResult {
|
|
4
|
+
target: string;
|
|
4
5
|
ok: boolean;
|
|
5
6
|
timeMs: number;
|
|
6
7
|
error?: string;
|
|
7
8
|
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
export
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
timeMs: number;
|
|
21
|
-
error?: string;
|
|
22
|
-
}
|
|
23
|
-
export declare function checkLatency(hosts: LatencyHost[]): Promise<LatencyCheckResult[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Resolve each host's A records through a dedicated resolver with a bounded
|
|
11
|
+
* timeout (the default resolver retries for tens of seconds when DNS is down).
|
|
12
|
+
*/
|
|
13
|
+
export declare function checkDns(hosts: string[], timeoutMs: number): Promise<CheckResult[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Time to response headers for each URL. The body is discarded unread, so a
|
|
16
|
+
* large or endless response costs nothing. Only 2xx counts as reachable.
|
|
17
|
+
*/
|
|
18
|
+
export declare function checkHttp(urls: string[], timeoutMs: number): Promise<CheckResult[]>;
|
|
19
|
+
/** Time to establish a TCP connection to each host:port. */
|
|
20
|
+
export declare function checkLatency(hosts: LatencyHost[], timeoutMs: number): Promise<CheckResult[]>;
|
|
24
21
|
//# sourceMappingURL=checks.d.ts.map
|
|
@@ -1,84 +1,87 @@
|
|
|
1
1
|
import { promises as dns } from 'node:dns';
|
|
2
2
|
import net from 'node:net';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { NPC_USER_AGENT, discardBody, displayUrl } from '../../core/http.js';
|
|
4
|
+
function errorText(err) {
|
|
5
|
+
return err instanceof Error ? err.message : String(err);
|
|
5
6
|
}
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
export async function checkHttp(urls, timeout) {
|
|
22
|
-
return Promise.all(urls.map((url) => fetchSingleUrl(url, timeout)));
|
|
23
|
-
}
|
|
24
|
-
async function fetchSingleUrl(url, timeout) {
|
|
25
|
-
const controller = new AbortController();
|
|
26
|
-
const timer = setTimeout(() => controller.abort(), timeout);
|
|
27
|
-
const start = performance.now();
|
|
7
|
+
// ---------- DNS ----------
|
|
8
|
+
/**
|
|
9
|
+
* Resolve each host's A records through a dedicated resolver with a bounded
|
|
10
|
+
* timeout (the default resolver retries for tens of seconds when DNS is down).
|
|
11
|
+
*/
|
|
12
|
+
export async function checkDns(hosts, timeoutMs) {
|
|
13
|
+
const resolver = new dns.Resolver({
|
|
14
|
+
timeout: Math.max(250, Math.floor(timeoutMs / 2)),
|
|
15
|
+
tries: 2,
|
|
16
|
+
});
|
|
17
|
+
let timer;
|
|
18
|
+
const deadline = new Promise((resolve) => {
|
|
19
|
+
timer = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
20
|
+
});
|
|
28
21
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
url,
|
|
42
|
-
ok: false,
|
|
43
|
-
timeMs: performance.now() - start,
|
|
44
|
-
error: err instanceof Error ? err.message : String(err),
|
|
45
|
-
};
|
|
22
|
+
return await Promise.all(hosts.map(async (host) => {
|
|
23
|
+
const start = performance.now();
|
|
24
|
+
try {
|
|
25
|
+
const outcome = await Promise.race([resolver.resolve4(host), deadline]);
|
|
26
|
+
if (outcome === 'timeout')
|
|
27
|
+
return { target: host, ok: false, timeMs: 0, error: 'timeout' };
|
|
28
|
+
return { target: host, ok: true, timeMs: performance.now() - start };
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
return { target: host, ok: false, timeMs: 0, error: errorText(err) };
|
|
32
|
+
}
|
|
33
|
+
}));
|
|
46
34
|
}
|
|
47
35
|
finally {
|
|
48
36
|
clearTimeout(timer);
|
|
37
|
+
resolver.cancel();
|
|
49
38
|
}
|
|
50
39
|
}
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
// ---------- HTTP ----------
|
|
41
|
+
/**
|
|
42
|
+
* Time to response headers for each URL. The body is discarded unread, so a
|
|
43
|
+
* large or endless response costs nothing. Only 2xx counts as reachable.
|
|
44
|
+
*/
|
|
45
|
+
export async function checkHttp(urls, timeoutMs) {
|
|
46
|
+
return Promise.all(urls.map(async (url) => {
|
|
47
|
+
const target = displayUrl(url); // never echo credentials or query tokens
|
|
48
|
+
const start = performance.now();
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(url, {
|
|
51
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
52
|
+
headers: { 'User-Agent': NPC_USER_AGENT },
|
|
53
|
+
});
|
|
54
|
+
const timeMs = performance.now() - start;
|
|
55
|
+
await discardBody(res);
|
|
56
|
+
return res.ok
|
|
57
|
+
? { target, ok: true, timeMs }
|
|
58
|
+
: { target, ok: false, timeMs: 0, error: `HTTP ${res.status}` };
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
return { target, ok: false, timeMs: 0, error: errorText(err) };
|
|
62
|
+
}
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
// ---------- Latency (TCP connect) ----------
|
|
66
|
+
/** Time to establish a TCP connection to each host:port. */
|
|
67
|
+
export async function checkLatency(hosts, timeoutMs) {
|
|
68
|
+
return Promise.all(hosts.map((h) => measureTcpConnect(h, timeoutMs)));
|
|
53
69
|
}
|
|
54
|
-
function measureTcpConnect(target) {
|
|
70
|
+
function measureTcpConnect(target, timeoutMs) {
|
|
71
|
+
const label = target.host.includes(':')
|
|
72
|
+
? `[${target.host}]:${target.port}`
|
|
73
|
+
: `${target.host}:${target.port}`;
|
|
55
74
|
return new Promise((resolve) => {
|
|
56
75
|
const start = performance.now();
|
|
57
76
|
const socket = net.connect({ host: target.host, port: target.port });
|
|
58
|
-
const
|
|
77
|
+
const finish = (result) => {
|
|
78
|
+
clearTimeout(timer);
|
|
59
79
|
socket.destroy();
|
|
60
|
-
resolve(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
});
|
|
66
|
-
}, 5000);
|
|
67
|
-
socket.on('connect', () => {
|
|
68
|
-
clearTimeout(timeout);
|
|
69
|
-
const timeMs = performance.now() - start;
|
|
70
|
-
socket.destroy();
|
|
71
|
-
resolve({ host: target.host, ok: true, timeMs });
|
|
72
|
-
});
|
|
73
|
-
socket.on('error', (err) => {
|
|
74
|
-
clearTimeout(timeout);
|
|
75
|
-
resolve({
|
|
76
|
-
host: target.host,
|
|
77
|
-
ok: false,
|
|
78
|
-
timeMs: performance.now() - start,
|
|
79
|
-
error: err.message,
|
|
80
|
-
});
|
|
81
|
-
});
|
|
80
|
+
resolve(result);
|
|
81
|
+
};
|
|
82
|
+
const timer = setTimeout(() => finish({ target: label, ok: false, timeMs: 0, error: 'Connection timed out' }), timeoutMs);
|
|
83
|
+
socket.once('connect', () => finish({ target: label, ok: true, timeMs: performance.now() - start }));
|
|
84
|
+
socket.once('error', (err) => finish({ target: label, ok: false, timeMs: 0, error: err.message }));
|
|
82
85
|
});
|
|
83
86
|
}
|
|
84
87
|
//# sourceMappingURL=checks.js.map
|
|
@@ -8,7 +8,10 @@ export interface InternetConnectivityDefaults {
|
|
|
8
8
|
dnsHosts: string[];
|
|
9
9
|
httpUrls: string[];
|
|
10
10
|
latencyHosts: LatencyHost[];
|
|
11
|
+
/** Port for a `--latency-hosts` entry given without one. */
|
|
12
|
+
latencyPort: number;
|
|
11
13
|
slowThreshold: number;
|
|
14
|
+
/** Per-check timeout (DNS, HTTP and TCP) in ms. */
|
|
12
15
|
httpTimeout: number;
|
|
13
16
|
thresholds: {
|
|
14
17
|
dnsTime: ThresholdConfig;
|
|
@@ -4,10 +4,13 @@ export const DEFAULTS = {
|
|
|
4
4
|
// gstatic generate_204 is a stable, low-latency 204 endpoint used by many
|
|
5
5
|
// OSes for connectivity checks (replaces the flaky httpbin.org default).
|
|
6
6
|
httpUrls: ['https://1.1.1.1/cdn-cgi/trace', 'https://www.gstatic.com/generate_204'],
|
|
7
|
+
// 443, not 80: Google's 8.8.8.8 doesn't listen on port 80, so that default
|
|
8
|
+
// timed out on every poll and pinned the NPC at warning.
|
|
7
9
|
latencyHosts: [
|
|
8
|
-
{ host: '8.8.8.8', port:
|
|
9
|
-
{ host: '1.1.1.1', port:
|
|
10
|
+
{ host: '8.8.8.8', port: 443 },
|
|
11
|
+
{ host: '1.1.1.1', port: 443 },
|
|
10
12
|
],
|
|
13
|
+
latencyPort: 80,
|
|
11
14
|
slowThreshold: 1000, // ms
|
|
12
15
|
httpTimeout: 5000, // ms
|
|
13
16
|
thresholds: {
|
|
@@ -10,17 +10,20 @@ export interface ConnectivityThresholds {
|
|
|
10
10
|
export interface InternetConnectivityConfig extends PollingNpcConfig {
|
|
11
11
|
dnsHosts?: string[];
|
|
12
12
|
httpUrls?: string[];
|
|
13
|
+
/** `{host, port}` pairs, or "host" / "host:port" / "[v6]:port" strings. */
|
|
13
14
|
latencyHosts?: LatencyHost[] | string[];
|
|
14
15
|
slowThreshold?: number;
|
|
16
|
+
/** Per-check timeout in ms (DNS, HTTP and TCP). */
|
|
15
17
|
httpTimeout?: number;
|
|
16
18
|
thresholds?: ConnectivityThresholds;
|
|
17
19
|
}
|
|
18
20
|
interface ConnectivitySample {
|
|
19
21
|
allFailed: boolean;
|
|
20
22
|
anyFailed: boolean;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
/** Mean time of the successful checks of each kind; null when none succeeded. */
|
|
24
|
+
dnsAvg: number | null;
|
|
25
|
+
httpAvg: number | null;
|
|
26
|
+
latencyAvg: number | null;
|
|
24
27
|
}
|
|
25
28
|
/**
|
|
26
29
|
* Derive overall status from live per-check thresholds. All checks failing is
|
|
@@ -28,6 +31,11 @@ interface ConnectivitySample {
|
|
|
28
31
|
* escalate per their own thresholds.
|
|
29
32
|
*/
|
|
30
33
|
export declare function evaluateConnectivityStatus(sample: ConnectivitySample, thresholds: ConnectivityThresholds): Status;
|
|
34
|
+
/**
|
|
35
|
+
* Parse a latency target: "host", "host:port", "[ipv6]:port" or a bare IPv6
|
|
36
|
+
* address. Throws NpcConfigError on an out-of-range port.
|
|
37
|
+
*/
|
|
38
|
+
export declare function parseLatencyHost(input: string, defaultPort?: number): LatencyHost;
|
|
31
39
|
export declare class InternetConnectivityNPC extends PollingNPC {
|
|
32
40
|
private dnsHosts;
|
|
33
41
|
private httpUrls;
|
|
@@ -44,10 +52,15 @@ export declare class InternetConnectivityNPC extends PollingNPC {
|
|
|
44
52
|
get behavior(): BehaviorHints;
|
|
45
53
|
get companion(): CompanionDecl;
|
|
46
54
|
protected collect(): Promise<CollectedMetrics>;
|
|
47
|
-
private buildOnlineMessage;
|
|
48
|
-
private buildOfflineMessage;
|
|
49
55
|
/**
|
|
50
|
-
*
|
|
56
|
+
* One dashboard panel. A category whose checks all failed shows FAIL in red
|
|
57
|
+
* (and a 0 sparkline sample) rather than a misleading latency; a partial
|
|
58
|
+
* failure is amber.
|
|
59
|
+
*/
|
|
60
|
+
private panel;
|
|
61
|
+
/**
|
|
62
|
+
* Build config from CLI flags. List flags are comma-separated; latency
|
|
63
|
+
* hosts accept "host", "host:port" or "[ipv6]:port".
|
|
51
64
|
*/
|
|
52
65
|
static configFromFlags(flags: Record<string, string | boolean>): Partial<InternetConnectivityConfig>;
|
|
53
66
|
}
|