@glassmkr/crucible 0.6.2 → 0.6.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.
@@ -3,5 +3,6 @@ export interface NtpData {
3
3
  offset_seconds: number;
4
4
  source: string;
5
5
  daemon_running: boolean;
6
+ daemon_name: string;
6
7
  }
7
8
  export declare function collectNtp(): Promise<NtpData>;
@@ -1,12 +1,30 @@
1
1
  import { run } from "../lib/exec.js";
2
+ // Check whether a systemd unit is active. Returns false for missing units, not-found, etc.
3
+ async function isUnitActive(unit) {
4
+ const out = await run("systemctl", ["is-active", unit], 3000);
5
+ return out?.trim() === "active";
6
+ }
7
+ // Detect which time-sync daemon unit is currently active on the host, if any.
8
+ // Returns "" when none are. We check the common names in order of preference.
9
+ async function detectActiveDaemon() {
10
+ const candidates = ["chrony", "chronyd", "systemd-timesyncd", "ntp", "ntpsec", "ntpd"];
11
+ for (const unit of candidates) {
12
+ if (await isUnitActive(unit))
13
+ return unit;
14
+ }
15
+ return "";
16
+ }
2
17
  export async function collectNtp() {
3
- // Try timedatectl first (systemd-timesyncd)
18
+ // The authoritative "is the daemon running" check is systemctl is-active,
19
+ // not any derived flag from timedatectl. This catches daemon crashes and
20
+ // manual stops where the kernel clock is still synced.
21
+ const daemonName = await detectActiveDaemon();
22
+ const daemonRunning = daemonName !== "";
23
+ // Try timedatectl first (works for systemd-timesyncd and records the kernel
24
+ // NTPSynchronized flag regardless of which daemon set it).
4
25
  const tdctl = await run("timedatectl", ["show", "--property=NTPSynchronized", "--value"]);
5
- // Only trust timedatectl if it returns a clear "yes" or "no"
6
26
  if (tdctl !== null && (tdctl.trim() === "yes" || tdctl.trim() === "no")) {
7
27
  const synced = tdctl.trim() === "yes";
8
- const statusOut = await run("timedatectl", ["show", "--property=NTP", "--value"]);
9
- const ntpEnabled = statusOut?.trim() === "yes";
10
28
  let offset = 0;
11
29
  try {
12
30
  const tsStatus = await run("timedatectl", ["timesync-status"]);
@@ -25,19 +43,16 @@ export async function collectNtp() {
25
43
  }
26
44
  }
27
45
  catch { /* offset stays 0 */ }
28
- return {
29
- synced,
30
- offset_seconds: offset,
31
- source: "systemd-timesyncd",
32
- daemon_running: ntpEnabled || synced,
33
- };
46
+ // Prefer an explicitly detected daemon name; fall back to systemd-timesyncd
47
+ // since timedatectl is most commonly the timesyncd frontend.
48
+ const source = daemonName || "systemd-timesyncd";
49
+ return { synced, offset_seconds: offset, source, daemon_running: daemonRunning, daemon_name: daemonName };
34
50
  }
35
- // Try chrony - validate output contains expected "Leap status" field
51
+ // Chrony tracking. Validate Leap status so we do not misread error text.
36
52
  const chronyOut = await run("chronyc", ["tracking"]);
37
53
  if (chronyOut) {
38
54
  const leapMatch = chronyOut.match(/Leap status\s*:\s*(.+)/);
39
55
  if (leapMatch) {
40
- // Output looks like real chrony tracking data
41
56
  const synced = leapMatch[1].trim() === "Normal";
42
57
  let offset = 0;
43
58
  const offsetMatch = chronyOut.match(/Last offset\s*:\s*([+-]?\d+\.?\d*)\s*seconds/);
@@ -46,19 +61,17 @@ export async function collectNtp() {
46
61
  return {
47
62
  synced,
48
63
  offset_seconds: Math.abs(offset),
49
- source: "chrony",
50
- daemon_running: true,
64
+ source: daemonName || "chrony",
65
+ daemon_running: daemonRunning,
66
+ daemon_name: daemonName,
51
67
  };
52
68
  }
53
- // chronyc returned output but not tracking data (error message, daemon not running)
54
- // Fall through to ntpq
55
69
  }
56
- // Try ntpq - validate output contains the header line with "remote"
70
+ // ntpq peer table. Header check avoids false positives on error messages.
57
71
  const ntpqOut = await run("ntpq", ["-pn"]);
58
72
  if (ntpqOut) {
59
73
  const hasHeader = ntpqOut.split("\n").some((line) => line.includes("remote"));
60
74
  if (hasHeader) {
61
- // Output looks like real ntpq peer table
62
75
  const synced = ntpqOut.split("\n").some((line) => line.startsWith("*"));
63
76
  let offset = 0;
64
77
  const selectedLine = ntpqOut.split("\n").find((line) => line.startsWith("*"));
@@ -73,19 +86,20 @@ export async function collectNtp() {
73
86
  return {
74
87
  synced,
75
88
  offset_seconds: offset,
76
- source: "ntpd",
77
- daemon_running: true,
89
+ source: daemonName || "ntpd",
90
+ daemon_running: daemonRunning,
91
+ daemon_name: daemonName,
78
92
  };
79
93
  }
80
- // ntpq returned output but not peer table (error message, daemon not running)
81
- // Fall through to "none"
82
94
  }
83
- // No time sync daemon found (or all probes returned invalid output)
95
+ // No usable probe output. If systemd still reports a daemon as active, trust that;
96
+ // otherwise report fully down.
84
97
  return {
85
98
  synced: false,
86
99
  offset_seconds: 0,
87
- source: "none",
88
- daemon_running: false,
100
+ source: daemonName || "none",
101
+ daemon_running: daemonRunning,
102
+ daemon_name: daemonName,
89
103
  };
90
104
  }
91
105
  //# sourceMappingURL=ntp.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"ntp.js","sourceRoot":"","sources":["../../src/collect/ntp.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ntp.js","sourceRoot":"","sources":["../../src/collect/ntp.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glassmkr/crucible",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Lightweight bare metal server monitoring. IPMI, SMART, OS, network. Opinionated alerts.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -5,16 +5,37 @@ export interface NtpData {
5
5
  offset_seconds: number;
6
6
  source: string;
7
7
  daemon_running: boolean;
8
+ daemon_name: string;
9
+ }
10
+
11
+ // Check whether a systemd unit is active. Returns false for missing units, not-found, etc.
12
+ async function isUnitActive(unit: string): Promise<boolean> {
13
+ const out = await run("systemctl", ["is-active", unit], 3000);
14
+ return out?.trim() === "active";
15
+ }
16
+
17
+ // Detect which time-sync daemon unit is currently active on the host, if any.
18
+ // Returns "" when none are. We check the common names in order of preference.
19
+ async function detectActiveDaemon(): Promise<string> {
20
+ const candidates = ["chrony", "chronyd", "systemd-timesyncd", "ntp", "ntpsec", "ntpd"];
21
+ for (const unit of candidates) {
22
+ if (await isUnitActive(unit)) return unit;
23
+ }
24
+ return "";
8
25
  }
9
26
 
10
27
  export async function collectNtp(): Promise<NtpData> {
11
- // Try timedatectl first (systemd-timesyncd)
28
+ // The authoritative "is the daemon running" check is systemctl is-active,
29
+ // not any derived flag from timedatectl. This catches daemon crashes and
30
+ // manual stops where the kernel clock is still synced.
31
+ const daemonName = await detectActiveDaemon();
32
+ const daemonRunning = daemonName !== "";
33
+
34
+ // Try timedatectl first (works for systemd-timesyncd and records the kernel
35
+ // NTPSynchronized flag regardless of which daemon set it).
12
36
  const tdctl = await run("timedatectl", ["show", "--property=NTPSynchronized", "--value"]);
13
- // Only trust timedatectl if it returns a clear "yes" or "no"
14
37
  if (tdctl !== null && (tdctl.trim() === "yes" || tdctl.trim() === "no")) {
15
38
  const synced = tdctl.trim() === "yes";
16
- const statusOut = await run("timedatectl", ["show", "--property=NTP", "--value"]);
17
- const ntpEnabled = statusOut?.trim() === "yes";
18
39
 
19
40
  let offset = 0;
20
41
  try {
@@ -31,20 +52,17 @@ export async function collectNtp(): Promise<NtpData> {
31
52
  }
32
53
  } catch { /* offset stays 0 */ }
33
54
 
34
- return {
35
- synced,
36
- offset_seconds: offset,
37
- source: "systemd-timesyncd",
38
- daemon_running: ntpEnabled || synced,
39
- };
55
+ // Prefer an explicitly detected daemon name; fall back to systemd-timesyncd
56
+ // since timedatectl is most commonly the timesyncd frontend.
57
+ const source = daemonName || "systemd-timesyncd";
58
+ return { synced, offset_seconds: offset, source, daemon_running: daemonRunning, daemon_name: daemonName };
40
59
  }
41
60
 
42
- // Try chrony - validate output contains expected "Leap status" field
61
+ // Chrony tracking. Validate Leap status so we do not misread error text.
43
62
  const chronyOut = await run("chronyc", ["tracking"]);
44
63
  if (chronyOut) {
45
64
  const leapMatch = chronyOut.match(/Leap status\s*:\s*(.+)/);
46
65
  if (leapMatch) {
47
- // Output looks like real chrony tracking data
48
66
  const synced = leapMatch[1].trim() === "Normal";
49
67
  let offset = 0;
50
68
  const offsetMatch = chronyOut.match(/Last offset\s*:\s*([+-]?\d+\.?\d*)\s*seconds/);
@@ -52,20 +70,18 @@ export async function collectNtp(): Promise<NtpData> {
52
70
  return {
53
71
  synced,
54
72
  offset_seconds: Math.abs(offset),
55
- source: "chrony",
56
- daemon_running: true,
73
+ source: daemonName || "chrony",
74
+ daemon_running: daemonRunning,
75
+ daemon_name: daemonName,
57
76
  };
58
77
  }
59
- // chronyc returned output but not tracking data (error message, daemon not running)
60
- // Fall through to ntpq
61
78
  }
62
79
 
63
- // Try ntpq - validate output contains the header line with "remote"
80
+ // ntpq peer table. Header check avoids false positives on error messages.
64
81
  const ntpqOut = await run("ntpq", ["-pn"]);
65
82
  if (ntpqOut) {
66
83
  const hasHeader = ntpqOut.split("\n").some((line) => line.includes("remote"));
67
84
  if (hasHeader) {
68
- // Output looks like real ntpq peer table
69
85
  const synced = ntpqOut.split("\n").some((line) => line.startsWith("*"));
70
86
  let offset = 0;
71
87
  const selectedLine = ntpqOut.split("\n").find((line) => line.startsWith("*"));
@@ -79,19 +95,20 @@ export async function collectNtp(): Promise<NtpData> {
79
95
  return {
80
96
  synced,
81
97
  offset_seconds: offset,
82
- source: "ntpd",
83
- daemon_running: true,
98
+ source: daemonName || "ntpd",
99
+ daemon_running: daemonRunning,
100
+ daemon_name: daemonName,
84
101
  };
85
102
  }
86
- // ntpq returned output but not peer table (error message, daemon not running)
87
- // Fall through to "none"
88
103
  }
89
104
 
90
- // No time sync daemon found (or all probes returned invalid output)
105
+ // No usable probe output. If systemd still reports a daemon as active, trust that;
106
+ // otherwise report fully down.
91
107
  return {
92
108
  synced: false,
93
109
  offset_seconds: 0,
94
- source: "none",
95
- daemon_running: false,
110
+ source: daemonName || "none",
111
+ daemon_running: daemonRunning,
112
+ daemon_name: daemonName,
96
113
  };
97
114
  }