@nosana/node 1.1.56-rc → 1.1.57-rc

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.56-rc",
3
+ "version": "1.1.57-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"
@@ -4,6 +4,7 @@ export default class NodeManager {
4
4
  private exiting;
5
5
  private apiStarted;
6
6
  private signalled;
7
+ private configLocation;
7
8
  /**
8
9
  * Seeded from the environment so the loop survives the process being
9
10
  * replaced by an update.
@@ -12,6 +12,7 @@ import { LogMonitoringRegistry } from './monitoring/LogMonitoringRegistry.js';
12
12
  import { checkWSLStatus } from './utils/wslCheck.js';
13
13
  import { IN_JOB_LOOP_ENV } from '../exitCodes.js';
14
14
  import { reportError } from './monitoring/reportError.js';
15
+ import { reportPodmanDiagnostics } from './monitoring/reportPodmanDiagnostics.js';
15
16
  export default class NodeManager {
16
17
  constructor(options) {
17
18
  this.exiting = false;
@@ -22,6 +23,7 @@ export default class NodeManager {
22
23
  * replaced by an update.
23
24
  */
24
25
  this.inJobLoop = process.env[IN_JOB_LOOP_ENV] === 'true';
26
+ this.configLocation = options.config;
25
27
  this.node = createLoggingProxy(new BasicNode(options));
26
28
  /**
27
29
  * the node class makes the api but we pass the api to the NodeManager class
@@ -58,6 +60,11 @@ export default class NodeManager {
58
60
  * this prints the logs to the console.
59
61
  */
60
62
  consoleLogging();
63
+ /**
64
+ * collect the diagnostics the podman container leaves in the shared
65
+ * directory, which has no other route off the node
66
+ */
67
+ reportPodmanDiagnostics(this.configLocation);
61
68
  /**
62
69
  * start
63
70
  *
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Forward the events the podman container leaves in the directory the two
3
+ * share. That container has no route to the host manager of its own, so its
4
+ * event log is the only sign of whether the CDI watcher ever fired.
5
+ *
6
+ * Nothing here may throw: this is diagnostics, and the node's uncaught handler
7
+ * would turn a failure to read a log file into the node stopping. Watching can
8
+ * fail outright (a directory that is not there, a host out of inotify watches)
9
+ * and a watch already established still reports errors of its own if the
10
+ * directory it covers is removed.
11
+ */
12
+ export declare function reportPodmanDiagnostics(configLocation: string): void;
@@ -0,0 +1,111 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { reportError } from './reportError.js';
5
+ const CDI_EVENTS = 'cdi-events.log';
6
+ // Enough of the first event to tell one log from another.
7
+ const HEAD_BYTES = 64;
8
+ // A log this far ahead of us is skipped rather than read into memory at once.
9
+ const MAX_CATCH_UP = 1024 * 1024;
10
+ /**
11
+ * Forward the events the podman container leaves in the directory the two
12
+ * share. That container has no route to the host manager of its own, so its
13
+ * event log is the only sign of whether the CDI watcher ever fired.
14
+ *
15
+ * Nothing here may throw: this is diagnostics, and the node's uncaught handler
16
+ * would turn a failure to read a log file into the node stopping. Watching can
17
+ * fail outright (a directory that is not there, a host out of inotify watches)
18
+ * and a watch already established still reports errors of its own if the
19
+ * directory it covers is removed.
20
+ */
21
+ export function reportPodmanDiagnostics(configLocation) {
22
+ if (!configLocation)
23
+ return;
24
+ const log = path.join(configLocation.replace(/^~/, os.homedir()), CDI_EVENTS);
25
+ let head = '';
26
+ let drained = 0;
27
+ let partial = '';
28
+ // Only the bytes appended since the last read: the log is never rotated, so
29
+ // re-reading it whole would cost more with every event.
30
+ //
31
+ // Replacement is detected by re-reading the start of the file, which is the
32
+ // only signal that survives it: the log is replaced when the podman container
33
+ // is recreated, and neither size nor inode moves — these events are formulaic
34
+ // enough that the new first line is often exactly as long as the old, and a
35
+ // path recreated at once takes the inode back. The descriptor is opened per
36
+ // read for the same reason: a held one keeps reporting the size of the
37
+ // replaced file and never sees another event.
38
+ const drain = () => {
39
+ let fd;
40
+ try {
41
+ fd = fs.openSync(log, 'r');
42
+ }
43
+ catch {
44
+ return; // not written yet, or gone with the container that wrote it
45
+ }
46
+ try {
47
+ const { size } = fs.fstatSync(fd);
48
+ const start = Buffer.alloc(Math.min(size, HEAD_BYTES));
49
+ fs.readSync(fd, start, 0, start.length, 0);
50
+ // Only the prefix the two have in common: while the log is still shorter
51
+ // than HEAD_BYTES an append lengthens what is read here, which is growth
52
+ // rather than a different file.
53
+ const first = start.toString('utf8');
54
+ const shared = Math.min(first.length, head.length);
55
+ if (first.slice(0, shared) !== head.slice(0, shared) || size < drained) {
56
+ drained = 0;
57
+ partial = '';
58
+ }
59
+ head = first;
60
+ if (size === drained)
61
+ return;
62
+ // Reading a log that has run far ahead would hold all of it in memory at
63
+ // once; the older events are given up instead.
64
+ if (size - drained > MAX_CATCH_UP) {
65
+ drained = size;
66
+ partial = '';
67
+ return;
68
+ }
69
+ const appended = Buffer.alloc(size - drained);
70
+ fs.readSync(fd, appended, 0, appended.length, drained);
71
+ drained = size;
72
+ // A write can be seen part way through a line; hold the tail until the
73
+ // newline that ends it arrives.
74
+ const lines = (partial + appended.toString('utf8')).split('\n');
75
+ partial = lines.pop() ?? '';
76
+ lines
77
+ .filter(Boolean)
78
+ .forEach((event) => void reportError({
79
+ error_type: 'cdiEvent',
80
+ error_name: 'Event',
81
+ error_message: event,
82
+ error_stack: '',
83
+ }));
84
+ }
85
+ finally {
86
+ fs.closeSync(fd);
87
+ }
88
+ };
89
+ const drainQuietly = () => {
90
+ try {
91
+ drain();
92
+ }
93
+ catch {
94
+ // Reported nothing this time; the next event tries again.
95
+ }
96
+ };
97
+ drainQuietly();
98
+ try {
99
+ // The directory, not the file: the file does not exist until the first
100
+ // fault, and a watch on a path that is replaced follows neither file.
101
+ const watcher = fs.watch(path.dirname(log), (_, file) => {
102
+ if (!file || file === CDI_EVENTS)
103
+ drainQuietly();
104
+ });
105
+ watcher.on('error', () => watcher.close());
106
+ watcher.unref();
107
+ }
108
+ catch {
109
+ // No watch, so no events are forwarded; the node runs regardless.
110
+ }
111
+ }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.56-rc",
3
+ "version": "1.1.57-rc",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@nosana/node",
9
- "version": "1.1.56-rc",
9
+ "version": "1.1.57-rc",
10
10
  "license": "ISC",
11
11
  "dependencies": {
12
12
  "@coral-xyz/anchor": "^0.28.1-beta.1",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nosana/node",
3
- "version": "1.1.56-rc",
3
+ "version": "1.1.57-rc",
4
4
  "description": "",
5
5
  "bin": {
6
6
  "nosana-node": "./dist/src/index.js"