@yourfam/yf-vitals 0.1.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/LICENSE +21 -0
- package/README.md +63 -0
- package/bin/yf-vitals.js +5 -0
- package/package.json +50 -0
- package/src/args.js +104 -0
- package/src/ascii.js +12 -0
- package/src/bar.js +83 -0
- package/src/color.js +24 -0
- package/src/dashboard.js +125 -0
- package/src/disk.js +110 -0
- package/src/errors.js +11 -0
- package/src/format.js +86 -0
- package/src/history.js +58 -0
- package/src/index.js +92 -0
- package/src/rates.js +21 -0
- package/src/render.js +153 -0
- package/src/sample.js +411 -0
- package/src/tty.js +35 -0
package/src/sample.js
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { createWindowsDiskReader } from "./disk.js";
|
|
3
|
+
import { rateFromCounters } from "./rates.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {object} CpuSample
|
|
7
|
+
* @property {number} percent
|
|
8
|
+
*
|
|
9
|
+
* @typedef {object} RamSample
|
|
10
|
+
* @property {number} percent
|
|
11
|
+
* @property {number} used
|
|
12
|
+
* @property {number} total
|
|
13
|
+
*
|
|
14
|
+
* @typedef {object} DiskSample
|
|
15
|
+
* @property {number | null} readBps
|
|
16
|
+
* @property {number | null} writeBps
|
|
17
|
+
*
|
|
18
|
+
* @typedef {object} NetSample
|
|
19
|
+
* @property {number | null} txBps
|
|
20
|
+
* @property {number | null} rxBps
|
|
21
|
+
*
|
|
22
|
+
* @typedef {object} GpuSample
|
|
23
|
+
* @property {number | null} percent
|
|
24
|
+
* @property {number | null} used
|
|
25
|
+
* @property {number | null} total
|
|
26
|
+
*
|
|
27
|
+
* @typedef {object} Snapshot
|
|
28
|
+
* @property {number} ts
|
|
29
|
+
* @property {string} hostname
|
|
30
|
+
* @property {string} osName
|
|
31
|
+
* @property {number} physical
|
|
32
|
+
* @property {number} logical
|
|
33
|
+
* @property {number} ramTotal
|
|
34
|
+
* @property {CpuSample | null} cpu
|
|
35
|
+
* @property {RamSample | null} ram
|
|
36
|
+
* @property {DiskSample | null} disk
|
|
37
|
+
* @property {NetSample | null} net
|
|
38
|
+
* @property {GpuSample | null} gpu
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @param {number | null | undefined} n
|
|
43
|
+
*/
|
|
44
|
+
function num(n) {
|
|
45
|
+
const v = Number(n);
|
|
46
|
+
return Number.isFinite(v) ? v : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {import("systeminformation").OsData | null} info
|
|
51
|
+
*/
|
|
52
|
+
export function shortOsName(info) {
|
|
53
|
+
const distro = String(info?.distro || "").trim();
|
|
54
|
+
if (/windows\s*11/i.test(distro) || (info == null && os.type() === "Windows_NT" && windowsLabel(os.release()) === "Windows 11")) {
|
|
55
|
+
return "Windows 11";
|
|
56
|
+
}
|
|
57
|
+
if (/windows\s*10/i.test(distro)) return "Windows 10";
|
|
58
|
+
if (/windows/i.test(distro) || os.type() === "Windows_NT") {
|
|
59
|
+
return windowsLabel(os.release());
|
|
60
|
+
}
|
|
61
|
+
if (!info) return os.type() || "unknown";
|
|
62
|
+
const release = String(info.release || "").trim();
|
|
63
|
+
if (distro && release && !distro.includes(release)) {
|
|
64
|
+
const combined = `${distro} ${release}`.trim();
|
|
65
|
+
if (combined.length <= 24) return combined;
|
|
66
|
+
}
|
|
67
|
+
if (distro) return distro.length <= 24 ? distro : distro.slice(0, 24);
|
|
68
|
+
if (info.platform) return String(info.platform);
|
|
69
|
+
return "unknown";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} release
|
|
74
|
+
*/
|
|
75
|
+
function windowsLabel(release) {
|
|
76
|
+
const major = Number(String(release).split(".")[0]);
|
|
77
|
+
if (major >= 10) {
|
|
78
|
+
const build = Number(String(release).split(".")[2] || 0);
|
|
79
|
+
if (build >= 22000) return "Windows 11";
|
|
80
|
+
return "Windows 10";
|
|
81
|
+
}
|
|
82
|
+
return "Windows";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* GPU memory fields in systeminformation are sometimes MiB, sometimes bytes.
|
|
87
|
+
* @param {number | null} n
|
|
88
|
+
* @param {number | null} vramMb
|
|
89
|
+
*/
|
|
90
|
+
export function gpuBytes(n, vramMb) {
|
|
91
|
+
if (n != null && n >= 256 * 1024 * 1024) return n;
|
|
92
|
+
if (n != null && n > 0) return n * 1024 * 1024;
|
|
93
|
+
if (vramMb != null && vramMb > 0) return vramMb * 1024 * 1024;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {Array<{ utilizationGpu?: number, memoryUsed?: number, memoryTotal?: number, vram?: number }>} controllers
|
|
99
|
+
* @returns {GpuSample | null}
|
|
100
|
+
*/
|
|
101
|
+
export function pickGpu(controllers) {
|
|
102
|
+
if (!Array.isArray(controllers) || controllers.length === 0) return null;
|
|
103
|
+
const usable = controllers.filter((c) => {
|
|
104
|
+
const util = num(c.utilizationGpu);
|
|
105
|
+
const total = gpuBytes(num(c.memoryTotal), num(c.vram));
|
|
106
|
+
const used = gpuBytes(num(c.memoryUsed), null);
|
|
107
|
+
return util != null || (total != null && total > 0) || used != null;
|
|
108
|
+
});
|
|
109
|
+
if (usable.length === 0) return null;
|
|
110
|
+
const c =
|
|
111
|
+
usable.find((x) => num(x.utilizationGpu) != null) || usable[0];
|
|
112
|
+
const percent = num(c.utilizationGpu);
|
|
113
|
+
const total = gpuBytes(num(c.memoryTotal), num(c.vram));
|
|
114
|
+
const used = gpuBytes(num(c.memoryUsed), null);
|
|
115
|
+
return {
|
|
116
|
+
percent,
|
|
117
|
+
used,
|
|
118
|
+
total,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Keep last good per-row values when a metric throws or returns null.
|
|
124
|
+
* GPU stays null (omit row) until a usable sample appears.
|
|
125
|
+
*
|
|
126
|
+
* @param {Snapshot | null} last
|
|
127
|
+
* @param {Partial<Snapshot> & { ts: number }} next
|
|
128
|
+
* @returns {Snapshot}
|
|
129
|
+
*/
|
|
130
|
+
export function mergeLastGood(last, next) {
|
|
131
|
+
const base = last || {
|
|
132
|
+
ts: next.ts,
|
|
133
|
+
hostname: "localhost",
|
|
134
|
+
osName: "unknown",
|
|
135
|
+
physical: 0,
|
|
136
|
+
logical: 0,
|
|
137
|
+
ramTotal: 0,
|
|
138
|
+
cpu: null,
|
|
139
|
+
ram: null,
|
|
140
|
+
disk: null,
|
|
141
|
+
net: null,
|
|
142
|
+
gpu: null,
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
ts: next.ts,
|
|
146
|
+
hostname: next.hostname ?? base.hostname,
|
|
147
|
+
osName: next.osName ?? base.osName,
|
|
148
|
+
physical: next.physical ?? base.physical,
|
|
149
|
+
logical: next.logical ?? base.logical,
|
|
150
|
+
ramTotal: next.ramTotal ?? base.ramTotal,
|
|
151
|
+
cpu: next.cpu ?? base.cpu,
|
|
152
|
+
ram: next.ram ?? base.ram,
|
|
153
|
+
disk: mergePair(next.disk, base.disk, "readBps", "writeBps"),
|
|
154
|
+
net: mergePair(next.net, base.net, "txBps", "rxBps"),
|
|
155
|
+
gpu: next.gpu ?? base.gpu,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* @param {Record<string, number | null> | null | undefined} next
|
|
161
|
+
* @param {Record<string, number | null> | null | undefined} last
|
|
162
|
+
* @param {string} a
|
|
163
|
+
* @param {string} b
|
|
164
|
+
*/
|
|
165
|
+
function mergePair(next, last, a, b) {
|
|
166
|
+
if (!next && !last) return null;
|
|
167
|
+
if (!next) return last;
|
|
168
|
+
return {
|
|
169
|
+
[a]: next[a] ?? last?.[a] ?? null,
|
|
170
|
+
[b]: next[b] ?? last?.[b] ?? null,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @param {os.CpuInfo[]} cpus
|
|
176
|
+
*/
|
|
177
|
+
export function cpuIdleTotal(cpus) {
|
|
178
|
+
let idle = 0;
|
|
179
|
+
let total = 0;
|
|
180
|
+
for (const c of cpus) {
|
|
181
|
+
const t = c.times;
|
|
182
|
+
const i = t.idle || 0;
|
|
183
|
+
const sum = (t.user || 0) + (t.nice || 0) + (t.sys || 0) + (t.irq || 0) + i;
|
|
184
|
+
idle += i;
|
|
185
|
+
total += sum;
|
|
186
|
+
}
|
|
187
|
+
return { idle, total };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* @param {{ idle: number, total: number } | null} prev
|
|
192
|
+
* @param {{ idle: number, total: number }} curr
|
|
193
|
+
*/
|
|
194
|
+
export function cpuPercentFromDelta(prev, curr) {
|
|
195
|
+
if (!prev) return null;
|
|
196
|
+
const idle = curr.idle - prev.idle;
|
|
197
|
+
const total = curr.total - prev.total;
|
|
198
|
+
if (total <= 0) return null;
|
|
199
|
+
const pct = (1 - idle / total) * 100;
|
|
200
|
+
if (!Number.isFinite(pct)) return null;
|
|
201
|
+
return Math.min(100, Math.max(0, pct));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* @param {Array<{ iface?: string, rx_bytes?: number, tx_bytes?: number, operstate?: string, internal?: boolean }>} stats
|
|
206
|
+
*/
|
|
207
|
+
export function sumNetBytes(stats) {
|
|
208
|
+
let rx = 0;
|
|
209
|
+
let tx = 0;
|
|
210
|
+
let any = false;
|
|
211
|
+
if (!Array.isArray(stats)) return { rx: null, tx: null };
|
|
212
|
+
for (const n of stats) {
|
|
213
|
+
const name = String(n.iface || "");
|
|
214
|
+
if (n.internal || name === "lo" || name === "lo0" || /^Loopback/i.test(name)) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (n.operstate && n.operstate !== "up" && n.operstate !== "unknown") {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const r = num(n.rx_bytes);
|
|
221
|
+
const t = num(n.tx_bytes);
|
|
222
|
+
if (r == null && t == null) continue;
|
|
223
|
+
any = true;
|
|
224
|
+
rx += r ?? 0;
|
|
225
|
+
tx += t ?? 0;
|
|
226
|
+
}
|
|
227
|
+
if (!any) return { rx: null, tx: null };
|
|
228
|
+
return { rx, tx };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* @param {{ cpu?: Function, currentLoad?: Function, mem?: Function, osInfo?: Function, fsStats?: Function, networkStats?: Function, graphics?: Function }} [si]
|
|
233
|
+
*/
|
|
234
|
+
export function createSampler(si) {
|
|
235
|
+
/** @type {null | { idle: number, total: number }} */
|
|
236
|
+
let prevCpu = null;
|
|
237
|
+
/** @type {null | { rx: number, wx: number }} */
|
|
238
|
+
let prevDisk = null;
|
|
239
|
+
/** @type {null | { rx: number, tx: number }} */
|
|
240
|
+
let prevNet = null;
|
|
241
|
+
/** @type {number | null} */
|
|
242
|
+
let prevTs = null;
|
|
243
|
+
/** @type {Snapshot | null} */
|
|
244
|
+
let last = null;
|
|
245
|
+
/** @type {Promise<{ hostname: string, osName: string, physical: number, logical: number, ramTotal: number }> | null} */
|
|
246
|
+
let staticP = null;
|
|
247
|
+
const winDisk = process.platform === "win32" ? createWindowsDiskReader() : null;
|
|
248
|
+
|
|
249
|
+
async function loadSi() {
|
|
250
|
+
if (si) return si;
|
|
251
|
+
const mod = await import("systeminformation");
|
|
252
|
+
return mod.default ?? mod;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function loadStatic() {
|
|
256
|
+
const lib = await loadSi();
|
|
257
|
+
const hostname = os.hostname();
|
|
258
|
+
let osName = shortOsName(null);
|
|
259
|
+
let logical = Math.max(os.cpus().length, 1);
|
|
260
|
+
let physical = logical;
|
|
261
|
+
let ramTotal = os.totalmem();
|
|
262
|
+
try {
|
|
263
|
+
const info = await lib.osInfo();
|
|
264
|
+
osName = shortOsName(info);
|
|
265
|
+
} catch {
|
|
266
|
+
// keep fallback
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const cpu = await lib.cpu();
|
|
270
|
+
if (cpu?.physicalCores) physical = cpu.physicalCores;
|
|
271
|
+
if (cpu?.cores) logical = cpu.cores;
|
|
272
|
+
} catch {
|
|
273
|
+
// keep os.cpus() counts
|
|
274
|
+
}
|
|
275
|
+
try {
|
|
276
|
+
const mem = await lib.mem();
|
|
277
|
+
if (mem?.total) ramTotal = mem.total;
|
|
278
|
+
} catch {
|
|
279
|
+
// keep os.totalmem
|
|
280
|
+
}
|
|
281
|
+
return { hostname, osName, physical, logical, ramTotal };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function sampleCpu(lib) {
|
|
285
|
+
const curr = cpuIdleTotal(os.cpus());
|
|
286
|
+
if (!prevCpu) {
|
|
287
|
+
prevCpu = curr;
|
|
288
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
289
|
+
const curr2 = cpuIdleTotal(os.cpus());
|
|
290
|
+
const pctFast = cpuPercentFromDelta(prevCpu, curr2);
|
|
291
|
+
prevCpu = curr2;
|
|
292
|
+
if (pctFast != null) return { percent: pctFast };
|
|
293
|
+
} else {
|
|
294
|
+
const pct = cpuPercentFromDelta(prevCpu, curr);
|
|
295
|
+
prevCpu = curr;
|
|
296
|
+
if (pct != null) return { percent: pct };
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const load = await lib.currentLoad();
|
|
300
|
+
const pct = num(load?.currentLoad);
|
|
301
|
+
if (pct != null) return { percent: Math.min(100, Math.max(0, pct)) };
|
|
302
|
+
} catch {
|
|
303
|
+
// keep n/a
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function sampleRam(lib, ramTotal) {
|
|
309
|
+
try {
|
|
310
|
+
const mem = await lib.mem();
|
|
311
|
+
const total = num(mem?.total) ?? ramTotal;
|
|
312
|
+
const used = num(mem?.used) ?? num(mem?.active);
|
|
313
|
+
if (used == null || total == null || total <= 0) return null;
|
|
314
|
+
return {
|
|
315
|
+
percent: Math.min(100, Math.max(0, (used / total) * 100)),
|
|
316
|
+
used,
|
|
317
|
+
total,
|
|
318
|
+
};
|
|
319
|
+
} catch {
|
|
320
|
+
const total = ramTotal || os.totalmem();
|
|
321
|
+
const free = os.freemem();
|
|
322
|
+
const used = total - free;
|
|
323
|
+
if (total <= 0) return null;
|
|
324
|
+
return {
|
|
325
|
+
percent: Math.min(100, Math.max(0, (used / total) * 100)),
|
|
326
|
+
used,
|
|
327
|
+
total,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function sampleDisk(lib, ts) {
|
|
333
|
+
let rx = null;
|
|
334
|
+
let wx = null;
|
|
335
|
+
try {
|
|
336
|
+
const fs = await lib.fsStats();
|
|
337
|
+
rx = num(fs?.rx);
|
|
338
|
+
wx = num(fs?.wx);
|
|
339
|
+
} catch {
|
|
340
|
+
// Windows fsStats is always null; fall through
|
|
341
|
+
}
|
|
342
|
+
if (rx == null && wx == null && winDisk) {
|
|
343
|
+
const w = await winDisk.read();
|
|
344
|
+
if (w) {
|
|
345
|
+
rx = w.rx;
|
|
346
|
+
wx = w.wx;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (rx == null && wx == null) return { readBps: null, writeBps: null };
|
|
350
|
+
const readBps = rateFromCounters(prevDisk?.rx, rx ?? prevDisk?.rx ?? 0, prevTs, ts);
|
|
351
|
+
const writeBps = rateFromCounters(prevDisk?.wx, wx ?? prevDisk?.wx ?? 0, prevTs, ts);
|
|
352
|
+
prevDisk = { rx: rx ?? 0, wx: wx ?? 0 };
|
|
353
|
+
return { readBps, writeBps };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function sampleNet(lib, ts) {
|
|
357
|
+
try {
|
|
358
|
+
const stats = await lib.networkStats("*");
|
|
359
|
+
const { rx, tx } = sumNetBytes(Array.isArray(stats) ? stats : stats ? [stats] : []);
|
|
360
|
+
const rxBps = rateFromCounters(prevNet?.rx, rx, prevTs, ts);
|
|
361
|
+
const txBps = rateFromCounters(prevNet?.tx, tx, prevTs, ts);
|
|
362
|
+
if (rx != null && tx != null) prevNet = { rx, tx };
|
|
363
|
+
return { txBps, rxBps };
|
|
364
|
+
} catch {
|
|
365
|
+
return { txBps: null, rxBps: null };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function sampleGpu(lib) {
|
|
370
|
+
try {
|
|
371
|
+
const g = await lib.graphics();
|
|
372
|
+
return pickGpu(g?.controllers || []);
|
|
373
|
+
} catch {
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return {
|
|
379
|
+
/**
|
|
380
|
+
* @returns {Promise<Snapshot>}
|
|
381
|
+
*/
|
|
382
|
+
async sample() {
|
|
383
|
+
const lib = await loadSi();
|
|
384
|
+
if (!staticP) staticP = loadStatic();
|
|
385
|
+
const meta = await staticP;
|
|
386
|
+
const ts = Date.now();
|
|
387
|
+
const [cpu, ram, disk, net, gpu] = await Promise.all([
|
|
388
|
+
sampleCpu(lib).catch(() => last?.cpu ?? null),
|
|
389
|
+
sampleRam(lib, meta.ramTotal).catch(() => last?.ram ?? null),
|
|
390
|
+
sampleDisk(lib, ts).catch(() => last?.disk ?? { readBps: null, writeBps: null }),
|
|
391
|
+
sampleNet(lib, ts).catch(() => last?.net ?? { txBps: null, rxBps: null }),
|
|
392
|
+
sampleGpu(lib).catch(() => last?.gpu ?? null),
|
|
393
|
+
]);
|
|
394
|
+
prevTs = ts;
|
|
395
|
+
const snap = mergeLastGood(last, {
|
|
396
|
+
ts,
|
|
397
|
+
...meta,
|
|
398
|
+
cpu,
|
|
399
|
+
ram,
|
|
400
|
+
disk,
|
|
401
|
+
net,
|
|
402
|
+
gpu,
|
|
403
|
+
});
|
|
404
|
+
last = snap;
|
|
405
|
+
return snap;
|
|
406
|
+
},
|
|
407
|
+
close() {
|
|
408
|
+
winDisk?.close();
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
package/src/tty.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const ENTER_ALT = "\u001b[?1049h";
|
|
2
|
+
export const LEAVE_ALT = "\u001b[?1049l";
|
|
3
|
+
export const HIDE_CURSOR = "\u001b[?25l";
|
|
4
|
+
export const SHOW_CURSOR = "\u001b[?25h";
|
|
5
|
+
export const CLEAR_HOME = "\u001b[H\u001b[J";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {(s: string) => void} write
|
|
9
|
+
* @param {NodeJS.ReadStream | { isTTY?: boolean, setRawMode?: Function, resume?: Function } | null} [stdin]
|
|
10
|
+
*/
|
|
11
|
+
export function enterTerminal(write, stdin) {
|
|
12
|
+
write(ENTER_ALT + HIDE_CURSOR);
|
|
13
|
+
if (stdin && stdin.isTTY && typeof stdin.setRawMode === "function") {
|
|
14
|
+
stdin.setRawMode(true);
|
|
15
|
+
if (typeof stdin.resume === "function") stdin.resume();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Restore cursor, leave the alternate screen, and drop raw mode.
|
|
21
|
+
* Safe to call more than once.
|
|
22
|
+
*
|
|
23
|
+
* @param {(s: string) => void} write
|
|
24
|
+
* @param {NodeJS.ReadStream | { isTTY?: boolean, setRawMode?: Function, isRaw?: boolean } | null} [stdin]
|
|
25
|
+
*/
|
|
26
|
+
export function restoreTerminal(write, stdin) {
|
|
27
|
+
try {
|
|
28
|
+
if (stdin && stdin.isTTY && typeof stdin.setRawMode === "function") {
|
|
29
|
+
stdin.setRawMode(false);
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// stdin may already be destroyed
|
|
33
|
+
}
|
|
34
|
+
write(SHOW_CURSOR + LEAVE_ALT);
|
|
35
|
+
}
|