@bridge4dev/runner 0.70.0 → 0.71.0
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/dist/host-load.d.ts +53 -5
- package/dist/host-load.js +39 -5
- package/dist/supervisor.js +20 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/host-load.d.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* is on its knees», so it said nothing, and the first symptom a user saw was a
|
|
7
7
|
* session that had gone quiet (plan `agent-sessions-host-resources.md` §5.3).
|
|
8
8
|
*
|
|
9
|
-
* Two files and nothing else. That is a security
|
|
10
|
-
* implementation detail: this runner reports to a server, so
|
|
11
|
-
* for telemetry is listed here in full — `/proc/loadavg` and
|
|
12
|
-
* both world-readable, neither containing a path, a command
|
|
13
|
-
* environment variable or a process name
|
|
9
|
+
* Two files and one filesystem call, and nothing else. That is a security
|
|
10
|
+
* property, not an implementation detail: this runner reports to a server, so
|
|
11
|
+
* what it may read for telemetry is listed here in full — `/proc/loadavg` and
|
|
12
|
+
* `/proc/meminfo`, both world-readable, neither containing a path, a command
|
|
13
|
+
* line, an environment variable or a process name; and `statfs('/')` (0.71.0,
|
|
14
|
+
* #485), which answers four sizes of the root filesystem and nothing about what
|
|
15
|
+
* is on it. No `/proc/<pid>` walk, no `ps`, no directory listing, no `du`.
|
|
14
16
|
*
|
|
15
17
|
* **One module is allowed past that line, and it is named here so the rule and
|
|
16
18
|
* the code cannot drift apart:** `session-stall.ts` reads `/proc/<pid>` for the
|
|
@@ -89,6 +91,13 @@ export interface HostLoadFrame {
|
|
|
89
91
|
/** Zero on a machine with no swap; consumers must not divide blindly. */
|
|
90
92
|
swapTotalBytes: number;
|
|
91
93
|
swapFreeBytes: number;
|
|
94
|
+
/**
|
|
95
|
+
* The root filesystem (0.71.0, #485) – or `null` when it could not be read,
|
|
96
|
+
* which is «disk unknown» and never a reason to hold the rest of the frame
|
|
97
|
+
* back. It rides the same minute-long heartbeat as everything else here: a
|
|
98
|
+
* disk fills over hours, and a trigger of its own would buy nothing.
|
|
99
|
+
*/
|
|
100
|
+
disk: HostDiskFrame | null;
|
|
92
101
|
/**
|
|
93
102
|
* When THIS MACHINE measured it, ISO.
|
|
94
103
|
*
|
|
@@ -98,6 +107,31 @@ export interface HostLoadFrame {
|
|
|
98
107
|
*/
|
|
99
108
|
at: string;
|
|
100
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* Free space on the root filesystem – what a full disk stops the machine at.
|
|
112
|
+
*
|
|
113
|
+
* Mirror of `HostDiskFrameSchema` in `@devbridge/shared`. The same arithmetic as
|
|
114
|
+
* the DevBridge host's own reading (`admin-host.service.ts`), so «free» means one
|
|
115
|
+
* thing on the Fleet screen whichever machine it describes: `freeBytes` is
|
|
116
|
+
* `bavail` (what an unprivileged writer can still use, not `bfree`), `usedBytes`
|
|
117
|
+
* is total − `bfree` (what `df` calls used).
|
|
118
|
+
*/
|
|
119
|
+
export interface HostDiskFrame {
|
|
120
|
+
/** Always {@link HOST_DISK_PATH} today; carried so the reading describes itself. */
|
|
121
|
+
path: string;
|
|
122
|
+
totalBytes: number;
|
|
123
|
+
freeBytes: number;
|
|
124
|
+
usedBytes: number;
|
|
125
|
+
}
|
|
126
|
+
/** The filesystem measured: the root, where running out stops everything. */
|
|
127
|
+
export declare const HOST_DISK_PATH = "/";
|
|
128
|
+
/** What `fs.statfsSync` answers, as far as a size is concerned. */
|
|
129
|
+
export interface DiskStats {
|
|
130
|
+
bsize: number;
|
|
131
|
+
blocks: number;
|
|
132
|
+
bfree: number;
|
|
133
|
+
bavail: number;
|
|
134
|
+
}
|
|
101
135
|
/** Test seams. Production calls `readHostLoad()` with nothing. */
|
|
102
136
|
export interface HostLoadSources {
|
|
103
137
|
/**
|
|
@@ -111,7 +145,21 @@ export interface HostLoadSources {
|
|
|
111
145
|
cpuCount?: number;
|
|
112
146
|
/** The clock behind `at`. */
|
|
113
147
|
now?: () => Date;
|
|
148
|
+
/**
|
|
149
|
+
* `statfs`, for the same reason as `procDir`: a full disk, a filesystem that
|
|
150
|
+
* refuses the call and a reading that does not add up are exactly the states a
|
|
151
|
+
* test machine is not in.
|
|
152
|
+
*/
|
|
153
|
+
statfs?: (target: string) => DiskStats;
|
|
114
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Measure the root filesystem, or say honestly that we cannot.
|
|
157
|
+
*
|
|
158
|
+
* Every number has to be a whole, non-negative, safely representable count of
|
|
159
|
+
* bytes that fits inside the total – the API's schema holds the same line, and a
|
|
160
|
+
* reading it would refuse is better not sent than sent. Any doubt is `null`.
|
|
161
|
+
*/
|
|
162
|
+
export declare function readRootDisk(statfs?: (target: string) => DiskStats): HostDiskFrame | null;
|
|
115
163
|
/**
|
|
116
164
|
* Measure this machine, or say honestly that we cannot.
|
|
117
165
|
*
|
package/dist/host-load.js
CHANGED
|
@@ -9,11 +9,13 @@ import path from 'node:path';
|
|
|
9
9
|
* is on its knees», so it said nothing, and the first symptom a user saw was a
|
|
10
10
|
* session that had gone quiet (plan `agent-sessions-host-resources.md` §5.3).
|
|
11
11
|
*
|
|
12
|
-
* Two files and nothing else. That is a security
|
|
13
|
-
* implementation detail: this runner reports to a server, so
|
|
14
|
-
* for telemetry is listed here in full — `/proc/loadavg` and
|
|
15
|
-
* both world-readable, neither containing a path, a command
|
|
16
|
-
* environment variable or a process name
|
|
12
|
+
* Two files and one filesystem call, and nothing else. That is a security
|
|
13
|
+
* property, not an implementation detail: this runner reports to a server, so
|
|
14
|
+
* what it may read for telemetry is listed here in full — `/proc/loadavg` and
|
|
15
|
+
* `/proc/meminfo`, both world-readable, neither containing a path, a command
|
|
16
|
+
* line, an environment variable or a process name; and `statfs('/')` (0.71.0,
|
|
17
|
+
* #485), which answers four sizes of the root filesystem and nothing about what
|
|
18
|
+
* is on it. No `/proc/<pid>` walk, no `ps`, no directory listing, no `du`.
|
|
17
19
|
*
|
|
18
20
|
* **One module is allowed past that line, and it is named here so the rule and
|
|
19
21
|
* the code cannot drift apart:** `session-stall.ts` reads `/proc/<pid>` for the
|
|
@@ -66,7 +68,36 @@ export const HOST_LOAD_HEARTBEAT_MS = 60_000;
|
|
|
66
68
|
export const HOST_LOAD_MIN_DELTA_LOAD1 = 0.5;
|
|
67
69
|
/** The same idea for memory, as a fraction — see `hostLoadChangedEnough`. */
|
|
68
70
|
export const HOST_LOAD_MIN_DELTA_MEM_RATIO = 0.05;
|
|
71
|
+
/** The filesystem measured: the root, where running out stops everything. */
|
|
72
|
+
export const HOST_DISK_PATH = '/';
|
|
69
73
|
const PROC_DIR = '/proc';
|
|
74
|
+
/**
|
|
75
|
+
* Measure the root filesystem, or say honestly that we cannot.
|
|
76
|
+
*
|
|
77
|
+
* Every number has to be a whole, non-negative, safely representable count of
|
|
78
|
+
* bytes that fits inside the total – the API's schema holds the same line, and a
|
|
79
|
+
* reading it would refuse is better not sent than sent. Any doubt is `null`.
|
|
80
|
+
*/
|
|
81
|
+
export function readRootDisk(statfs = fs.statfsSync) {
|
|
82
|
+
try {
|
|
83
|
+
const stat = statfs(HOST_DISK_PATH);
|
|
84
|
+
const totalBytes = stat.blocks * stat.bsize;
|
|
85
|
+
const freeBytes = stat.bavail * stat.bsize;
|
|
86
|
+
const usedBytes = Math.max(0, totalBytes - stat.bfree * stat.bsize);
|
|
87
|
+
const whole = (value) => Number.isSafeInteger(value) && value >= 0;
|
|
88
|
+
if (!whole(totalBytes) || totalBytes === 0 || !whole(freeBytes) || !whole(usedBytes)) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
if (freeBytes > totalBytes || usedBytes > totalBytes)
|
|
92
|
+
return null;
|
|
93
|
+
return { path: HOST_DISK_PATH, totalBytes, freeBytes, usedBytes };
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// ENOSYS in an odd container, EACCES under a locked-down profile: the disk
|
|
97
|
+
// is unknown, and the load beside it is still worth sending.
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
70
101
|
/** kB in `/proc/meminfo` means kibibytes, and has since the field was added. */
|
|
71
102
|
const MEMINFO_UNIT_BYTES = 1024;
|
|
72
103
|
/**
|
|
@@ -158,6 +189,9 @@ export function readHostLoad(sources = {}) {
|
|
|
158
189
|
memAvailableBytes,
|
|
159
190
|
swapTotalBytes,
|
|
160
191
|
swapFreeBytes,
|
|
192
|
+
// Its own failure is `null` inside the frame, never a missing frame: a
|
|
193
|
+
// machine whose filesystem refuses `statfs` still has a load worth showing.
|
|
194
|
+
disk: readRootDisk(sources.statfs),
|
|
161
195
|
at,
|
|
162
196
|
};
|
|
163
197
|
}
|
package/dist/supervisor.js
CHANGED
|
@@ -1915,6 +1915,16 @@ export class Supervisor {
|
|
|
1915
1915
|
existing.stopRequested = true;
|
|
1916
1916
|
existing.session?.stop('session_stopped');
|
|
1917
1917
|
}
|
|
1918
|
+
// The descriptor also knows the feed better than this life does (#472).
|
|
1919
|
+
// The API writes lines of its own into a feed this runner numbers: the
|
|
1920
|
+
// refusal service puts «Continuing the session by itself» at the next
|
|
1921
|
+
// free seq and resumes a moment later, while this life is still going
|
|
1922
|
+
// down. The words that come with the resume are echoed by THIS life, and
|
|
1923
|
+
// its counter knew nothing of that line – the echo reused its number and
|
|
1924
|
+
// the API dropped it as a duplicate. Before the `await` below: the words
|
|
1925
|
+
// arrive right behind this frame. A no-op whenever this runner wrote the
|
|
1926
|
+
// last row itself.
|
|
1927
|
+
existing.journal.ensureSeqAbove(descriptor.lastSeq);
|
|
1918
1928
|
// The descriptor is newer than what this session was built from, so its
|
|
1919
1929
|
// clock is too (QA-149 MAJOR-1). Applied on the way out of every early
|
|
1920
1930
|
// return, not just the reconnect one — a descriptor re-sent for any
|
|
@@ -2936,9 +2946,18 @@ export class Supervisor {
|
|
|
2936
2946
|
}
|
|
2937
2947
|
// Keep the journal while offline: the recorded terminal status is what
|
|
2938
2948
|
// reconcile replays after the next reconnect (QA-96 F1).
|
|
2949
|
+
//
|
|
2950
|
+
// …and while a resume is queued behind this life (#472). The next life
|
|
2951
|
+
// opens this same journal, and two things it needs live only here: the
|
|
2952
|
+
// words parked for it (`acceptUserMessage` holds a message that reached a
|
|
2953
|
+
// life already on its way out) and the seq counter. Deleted, the resume
|
|
2954
|
+
// started from an empty file – the platform's «continue» after a refusal
|
|
2955
|
+
// was gone, and «Session resumed» reused a seq the old life had already
|
|
2956
|
+
// sent, so the API dropped it. The session sat idle with nothing on screen.
|
|
2939
2957
|
if (this.ws.connected &&
|
|
2940
2958
|
running.journal.unacked().length === 0 &&
|
|
2941
|
-
isTerminal(running.lastReported)
|
|
2959
|
+
isTerminal(running.lastReported) &&
|
|
2960
|
+
!running.pendingRestart) {
|
|
2942
2961
|
this.journals.closeAndDelete(descriptor.id);
|
|
2943
2962
|
}
|
|
2944
2963
|
// The map just lost an entry, and this is the one place where that happens
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const RUNNER_VERSION = "0.
|
|
1
|
+
export declare const RUNNER_VERSION = "0.71.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
package/package.json
CHANGED