@daemonitor/plugins 1.1.5 → 1.1.8

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.
@@ -54,11 +54,24 @@ function parseNum(s) {
54
54
  const m = /(-?\d+(?:\.\d+)?)/.exec(s || "");
55
55
  return m ? Number(m[1]) : undefined;
56
56
  }
57
+ // net-snmp renders octet strings that contain a NUL/non-printable byte (e.g. HP's
58
+ // NUL-terminated "Black Cartridge 81A HP CF281A\0") as space-separated hex.
59
+ // Decode that back to text, and strip control chars from any value.
60
+ function decodeSnmp(s) {
61
+ const t = s.trim();
62
+ if (/^([0-9A-Fa-f]{2})(\s+[0-9A-Fa-f]{2})+\s*$/.test(t)) {
63
+ const bytes = t.split(/\s+/).map((h) => parseInt(h, 16));
64
+ return Buffer.from(bytes).toString("latin1").replace(/[\x00-\x1f\x7f]+/g, "").trim();
65
+ }
66
+ return t.replace(/[\x00-\x1f\x7f]+/g, "").trim();
67
+ }
57
68
  async function snmpGet(t, oid, timeout = 8000) {
58
69
  try {
59
70
  const { stdout } = await pexec("snmpget", ["-v", t.version || "2c", "-c", t.community || "public", "-Ovq", "-t", "2", "-r", "1", t.ip, oid], { timeout });
60
- const v = stdout.trim().replace(/^"|"$/g, "");
61
- return v && !/No Such|no response|Timeout/i.test(v) ? v : null;
71
+ const raw = stdout.trim().replace(/^"|"$/g, "");
72
+ if (!raw || /No Such|no response|Timeout/i.test(raw))
73
+ return null;
74
+ return decodeSnmp(raw);
62
75
  }
63
76
  catch {
64
77
  return null;
@@ -67,7 +80,7 @@ async function snmpGet(t, oid, timeout = 8000) {
67
80
  async function snmpWalk(t, oid, timeout = 10000) {
68
81
  try {
69
82
  const { stdout } = await pexec("snmpwalk", ["-v", t.version || "2c", "-c", t.community || "public", "-Oqv", "-t", "2", "-r", "1", t.ip, oid], { timeout });
70
- return stdout.split("\n").map((l) => l.trim().replace(/^"|"$/g, "")).filter(Boolean);
83
+ return stdout.split("\n").map((l) => decodeSnmp(l.trim().replace(/^"|"$/g, ""))).filter(Boolean);
71
84
  }
72
85
  catch {
73
86
  return [];
@@ -75,32 +88,38 @@ async function snmpWalk(t, oid, timeout = 10000) {
75
88
  }
76
89
  async function collectPrinter(t) {
77
90
  const warnPct = t.tonerWarnPct ?? 10;
78
- const [model, serial, pagesRaw, statusRaw, descs, maxes, levels] = await Promise.all([
91
+ // Walk LEVELS (clean numeric values, one per row) to get the supply indices,
92
+ // then fetch each supply's description/max by that index — a single get whose
93
+ // (possibly multi-line hex) value decodeSnmp handles, so a long description
94
+ // can't be split into phantom supplies and misalign the levels.
95
+ const [model, serial, pagesRaw, statusRaw, levelRows] = await Promise.all([
79
96
  snmpGet(t, OID.model),
80
97
  snmpGet(t, OID.serial),
81
98
  snmpGet(t, OID.pages),
82
99
  snmpGet(t, OID.status),
83
- snmpWalk(t, OID.supplyDesc),
84
- snmpWalk(t, OID.supplyMax),
85
- snmpWalk(t, OID.supplyLevel),
100
+ snmpWalkIndexed(t, OID.supplyLevel),
86
101
  ]);
87
102
  // No response at all → the printer is unreachable / SNMP off.
88
- if (model == null && serial == null && pagesRaw == null && !descs.length) {
103
+ if (model == null && serial == null && pagesRaw == null && !levelRows.length) {
89
104
  return { name: t.name, ip: t.ip, kind: "printer", reachable: false };
90
105
  }
91
- // Pair descriptions with levels. prtMarkerSuppliesLevel semantics: -2 =
92
- // unknown, -3 = "some remaining" (a level the device won't quantify); else a
93
- // count out of MaxCapacity (which can itself be -2 = unknown).
94
- const supplies = descs.map((desc, i) => {
95
- const max = Number(maxes[i]);
96
- const lvl = Number(levels[i]);
106
+ // prtMarkerSuppliesLevel semantics: -2 = unknown, -3 = "some remaining" (a
107
+ // level the device won't quantify); else a count out of MaxCapacity (which can
108
+ // itself be -2 = unknown).
109
+ const supplies = await Promise.all(levelRows.map(async (row) => {
110
+ const [descRaw, maxRaw] = await Promise.all([
111
+ snmpGet(t, `${OID.supplyDesc}.${row.idx}`),
112
+ snmpGet(t, `${OID.supplyMax}.${row.idx}`),
113
+ ]);
114
+ const lvl = Number(row.val);
115
+ const max = Number(maxRaw);
97
116
  let pct = null;
98
117
  if (lvl === -3)
99
- pct = 100; // "ok / some remaining"
118
+ pct = 100;
100
119
  else if (lvl >= 0 && max > 0)
101
120
  pct = Math.round((lvl / max) * 100);
102
- return { name: desc, level: lvl, max, pct };
103
- });
121
+ return { name: descRaw || "supply", level: lvl, max, pct };
122
+ }));
104
123
  const low = supplies.filter((s) => s.pct != null && s.pct <= warnPct);
105
124
  return {
106
125
  name: t.name,
@@ -164,7 +183,9 @@ async function snmpWalkIndexed(t, oid, timeout = 10000) {
164
183
  const m = /^(\S+)\s*=\s*[^:]+:\s*(.*)$/.exec(l.trim());
165
184
  if (!m)
166
185
  return null;
167
- const idx = m[1].startsWith(oid) ? m[1].slice(oid.length).replace(/^\./, "") : m[1];
186
+ // snmpwalk -On emits a leading dot (".1.3.6…") the bare OID lacks.
187
+ const clean = m[1].replace(/^\./, "");
188
+ const idx = clean.startsWith(oid) ? clean.slice(oid.length).replace(/^\./, "") : clean;
168
189
  return { idx, val: m[2].trim().replace(/^"|"$/g, "") };
169
190
  }).filter(Boolean);
170
191
  }
@@ -199,11 +220,19 @@ async function collectQnap(t) {
199
220
  const [st, tp, sm, ds] = await Promise.all([
200
221
  snmpGet(t, QNAP.hdStatus + i), snmpGet(t, QNAP.hdTemp + i), snmpGet(t, QNAP.hdSmart + i), snmpGet(t, QNAP.hdDescr + i),
201
222
  ]);
202
- if (st == null && tp == null)
223
+ const status = st != null ? Number(st) : undefined;
224
+ const tempC = parseC(tp);
225
+ // Empty bay: no readable temp and a non-ready (negative/absent, e.g. -5)
226
+ // status. Skip it — an empty slot isn't a failed disk.
227
+ if (tempC == null && (status == null || status < 0))
203
228
  continue;
204
- disks.push({ slot: i, descr: ds || undefined, tempC: parseC(tp), status: st != null ? Number(st) : undefined, smart: sm || undefined });
229
+ disks.push({ slot: i, descr: ds || undefined, tempC, status, smart: sm && sm !== "--" ? sm : undefined });
205
230
  }
206
- const badDisks = disks.filter((d) => (d.status != null && d.status !== 0) || (d.smart && !/good|normal/i.test(d.smart)) || (d.tempC != null && d.tempC >= tempWarn));
231
+ // A present disk is bad only on a real error status, an explicitly-bad SMART
232
+ // verdict, or overheating — not a blank "--" or an empty bay.
233
+ const badDisks = disks.filter((d) => (d.status != null && d.status !== 0) ||
234
+ (typeof d.smart === "string" && /warn|abnormal|fail|error|bad|caution/i.test(d.smart)) ||
235
+ (d.tempC != null && d.tempC >= tempWarn));
207
236
  // Volumes — the CACHEDEVn_DATA mounts from hrStorage.
208
237
  const volumes = [];
209
238
  for (const row of storDescrs.filter((r) => /CACHEDEV\d+_DATA$/.test(r.val))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@daemonitor/plugins",
3
- "version": "1.1.5",
3
+ "version": "1.1.8",
4
4
  "description": "daemonitor plugins",
5
5
  "types": "types/index.d.ts",
6
6
  "main": "dist/index.js",