@fordi-org/sdn 0.0.4 → 0.0.6

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/lib/Journal.js ADDED
@@ -0,0 +1,55 @@
1
+ import { inspect } from "node:util";
2
+ import { WriteStream } from "node:tty";
3
+ import { shellQuote } from "./shellQuote.js";
4
+ import { spawn } from "node:child_process";
5
+
6
+ const normalize = (args) => {
7
+ if (args.length === 1 && typeof args[0] === 'string') {
8
+ args = args[0];
9
+ } else {
10
+ args = args.map((arg) => inspect(arg, { color: true })).join('\xa0');
11
+ }
12
+ return args;
13
+ }
14
+
15
+ const logOut = process.env.INVOCATION_ID
16
+ ? async (type, tag, args) => {
17
+ args = normalize(args);
18
+ let priority = {
19
+ "debug": "debug",
20
+ "warn": "warning",
21
+ "error": "err",
22
+ "info": "info",
23
+ "log": "log",
24
+ }[type] ?? "info";
25
+ const proc = spawn('systemd-cat', ['-t', tag, '-p', priority], { stdio: ['pipe', 'ignore', 'ignore'] });
26
+ proc.stdin.write(args);
27
+ proc.unref();
28
+ }
29
+ : async (type, tag, args) => new Promise((resolve, reject) => {
30
+ args = normalize(args);
31
+ const priority = {
32
+ "debug": "DBG",
33
+ "error": "ERR",
34
+ "warn": "WRN",
35
+ "log": "LOG",
36
+ "info": "LOG",
37
+ }[type] ?? "LOG";
38
+ const stream = {
39
+ "debug": process.stdout,
40
+ "error": process.stderr,
41
+ "warn": process.stderr,
42
+ "log": process.stdout,
43
+ "info": process.stdout,
44
+ }[type] ?? process.stdout;
45
+ return stream.write(`[${tag ? `${tag} ` : ''}${priority}]:\xa0${args}\n`, (err) => {
46
+ if (err) {
47
+ reject(err);
48
+ } else {
49
+ resolve();
50
+ }
51
+ });
52
+ });
53
+
54
+ // TODO: wrap console:
55
+ // console.tag(name: string): Logger implements Console
package/lib/config.js CHANGED
@@ -30,8 +30,7 @@ function getConfig(root) {
30
30
  config.main = PACKAGE.main;
31
31
  config.package = Object.freeze(PACKAGE);
32
32
 
33
- Object.freeze(config);
34
-
33
+
35
34
  class ConfigError extends Error {
36
35
  constructor(message, needed) {
37
36
  super(needed ? `${message}; Please add the following to ${config.from}:\n ${
@@ -39,7 +38,12 @@ function getConfig(root) {
39
38
  }\n` : `${message}; please check ${config.from}`);
40
39
  }
41
40
  }
42
-
41
+
42
+ config.Error = ConfigError;
43
+
44
+ Object.freeze(config);
45
+ Object.freeze(PACKAGE);
46
+
43
47
  return { config, PACKAGE, ConfigError };
44
48
  }
45
49
 
@@ -0,0 +1,30 @@
1
+ export const discordClient = (url, delay = 5000) => {
2
+ let waiting = [];
3
+ let promise = null;
4
+ return (message) => {
5
+ if (!url) {
6
+ return;
7
+ }
8
+ waiting.push(message);
9
+ if (promise) {
10
+ return promise;
11
+ }
12
+ promise = new Promise((resolve) => {
13
+ setTimeout(() => {
14
+ fetch(url, {
15
+ method: 'post',
16
+ headers: {
17
+ 'content-type': 'application/json'
18
+ },
19
+ body: JSON.stringify({ content: waiting.join('\n-----\n') }),
20
+ }).then(r => {
21
+ resolve();
22
+ promise = null;
23
+ waiting = [];
24
+ return r.text();
25
+ });
26
+ }, delay);
27
+ });
28
+ return promise;
29
+ };
30
+ };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@fordi-org/sdn",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Make an npm project run as a systemd service",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "dependencies": {
8
- "nodemon": "^3.1.11"
8
+ "nodemon": "^3.1.14"
9
9
  },
10
10
  "bin": "index.js",
11
11
  "config": {
package/service/index.js CHANGED
@@ -2,11 +2,13 @@
2
2
  import { resolve } from "node:path";
3
3
  import nodemon from "nodemon";
4
4
  import { getConfig } from "../lib/config.js";
5
+ import { discordClient } from "../lib/discordClient.js";
5
6
 
6
7
  const { config: project } = getConfig(process.argv[2] ?? process.cwd());
7
-
8
8
  process.title = project.name;
9
9
 
10
+ const hook = discordClient(project.discord?.status, project.discord?.delay ?? 5000);
11
+
10
12
  nodemon({
11
13
  script: resolve(project.root, project.main),
12
14
  args: process.argv.slice(3),
@@ -14,15 +16,21 @@ nodemon({
14
16
 
15
17
  nodemon.on("start", () => {
16
18
  console.log(`Service ${project.name} is running.`);
19
+ hook(`${project.name} is running.`);
20
+
17
21
  }).on("quit", () => {
18
22
  console.log(`Service ${project.name} has quit.`);
23
+ hook(`${project.name} has quit.`);
19
24
  process.exit();
20
25
  }).on("restart", () => {
21
26
  console.log(`Service ${project.name} will restart.`);
27
+ hook(`${project.name} will restart.`);
22
28
  }).on("exit", () => {
23
29
  console.log(`Service ${project.name} exited cleanly.`);
30
+ hook(`${project.name} exited cleanly.`);
24
31
  }).on("crash", () => {
25
32
  console.log(`Service ${project.name} crashed.`);
33
+ hook(`${project.name} crashed.`);
26
34
  }).on("config:update", () => {
27
35
  console.log(`Service ${project.name} nodemon config has changed.`);
28
36
  });
package/service/logs.js CHANGED
@@ -2,9 +2,10 @@
2
2
  import { resolve } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
 
5
- import { config, getConfig } from "../lib/config.js";
5
+ import { getConfig } from "../lib/config.js";
6
6
  import { shellQuote } from "../lib/shellQuote.js";
7
7
  import { jsonCmd } from "../lib/jsonCmd.js";
8
+ import { stdin } from "node:process";
8
9
 
9
10
  const { config: project } = getConfig(process.cwd());
10
11
 
@@ -24,39 +25,150 @@ export const formatJournal = (line) => {
24
25
  }
25
26
 
26
27
  export function journalctl() {
27
- const argv = ['journalctl', '--user-unit', project.name, '-o', 'json', '-e', '-f'];
28
+ const argv = ['journalctl', '--user-unit', project.name];
29
+ for (const unit of project.otherUnits ?? []) {
30
+ argv.push(`--user-unit`, unit);
31
+ }
32
+ argv.push('-o', 'json', '-e', '-f');
28
33
  console.info(`> ${shellQuote(argv)}`);
29
34
  return jsonCmd(argv[0], argv.slice(1));
30
35
  }
31
36
 
32
37
  const useColor = process.argv.slice(2).includes('--force-color') || process.stdout.isTTY;
33
38
 
39
+ let leftOffset = 0;
40
+ let tailOffset = 0;
41
+
42
+ const handleKeyPress = (key) => {
43
+ if (key === '\x1B' || key === 'x') {
44
+ process.exit();
45
+ }
46
+ switch (key) {
47
+ case 'a':
48
+ case '\x1b[D':
49
+ leftOffset = Math.max(0, leftOffset - Math.round(process.stdout.columns * 0.20));
50
+ redraw();
51
+ break;
52
+ case 'l':
53
+ case '\x1b[C':
54
+ leftOffset += Math.round(process.stdout.columns * 0.20);
55
+ redraw();
56
+ break;
57
+ case 'y':
58
+ case '\x1b[A':
59
+ tailOffset += Math.round(process.stdout.columns * 0.20);
60
+ redraw();
61
+ break;
62
+ case 'b':
63
+ case '\x1b[B':
64
+ tailOffset = Math.max(0, tailOffset - Math.round(process.stdout.columns * 0.20));
65
+ redraw();
66
+ default:
67
+ }
68
+ };
69
+
70
+ if (process.stdin.isTTY) {
71
+ process.stdin.setRawMode(true);
72
+ process.stdin.resume();
73
+ process.stdin.setEncoding('utf8');
74
+ process.stdin.on('data', (key) => {
75
+ if (key === '\x03') {
76
+ process.exit();
77
+ }
78
+ handleKeyPress(key);
79
+ });
80
+ }
81
+
82
+ function sliceAnsi(text, start, end) {
83
+ let i = 0, j = 0;
84
+ text = [...text];
85
+ for (; i < start; i++, j++) {
86
+ if (text[j] === '\x1b') {
87
+ const m = text.slice(j).join('').match(ansiEscRx);
88
+ j += m[0].length;
89
+ }
90
+ }
91
+ const aStart = j;
92
+ let aEnd = text.length - 1;
93
+ if (end) {
94
+ for (; i <= end; i++, j++) {
95
+ if (text[j] === '\x1b') {
96
+ const m = text.slice(j).join('').match(ansiEscRx);
97
+ j += m[0].length;
98
+ }
99
+ }
100
+ aEnd = j;
101
+ }
102
+ let append = '';
103
+ if (aEnd < text.length - 1) {
104
+ aEnd = aEnd - 1;
105
+ append = '⇶'
106
+ }
107
+
108
+ return [...text.slice(aStart, aEnd), append].join('');
109
+ }
110
+ function drawMessage({ sym, stamp, tag, priority, message }) {
111
+ if (!useColor) {
112
+ // Strip colors if not on a terminal
113
+ message = message.replace(ansiEscRx, '');
114
+ }
115
+ if (process.stdout.columns < (stamp.length + 3) * 4) {
116
+ stamp = stamp.replace(/^\d{4}-\d{2}-\d{2} /, '').replace(/:\d{2}([ap])/, '$1');
117
+ }
118
+ const stampLen = (stamp.length + 4);
119
+ const cols = process.stdout.columns;
120
+ const prefix = (tag || priority !== 'LOG') ? `[${tag ? `${tag} ` : ''}${priority}]: ` : '';
121
+ // Reset and restore the current ANSI color state
122
+ const output = `${RESET}${stamp}${sym}${SET(state)}${prefix}${sliceAnsi(message, leftOffset, leftOffset + cols - stampLen)}${RESET}`;
123
+
124
+ // Capture the last esc[*m instance in the message, and store it to be restored before the next message.
125
+ // Unnessesary if not on a terminal.
126
+ if (useColor) {
127
+ state = [...(message.matchAll(ansiEscRx) ?? [])].at(-1)?.[1] ?? '0';
128
+ }
129
+ console.log(output);
130
+ }
131
+
132
+ function redraw() {
133
+ process.stdout.write('\x1b[2J\x1b[1;1H');
134
+ for (const payload of history.slice(-process.stdout.rows - tailOffset, tailOffset ? -tailOffset : undefined)) {
135
+ drawMessage(payload);
136
+ }
137
+ }
138
+
139
+ process.stdout.on('resize', () => redraw());
140
+
34
141
  // Only generate colors if on a terminal
35
142
  const SET = (...states) => useColor ? `\x1b[${states.join(';')}m` : '';
36
143
  const RESET = SET(0);
144
+ const history = [];
145
+ let needsRedraw = false;
146
+ let state = '0';
37
147
  if (fileURLToPath(import.meta.url) === resolve(process.argv[1])) {
38
- let state = '0';
148
+ redraw();
39
149
  for await (const line of journalctl(process.argv.slice(2))) {
150
+ if (needsRedraw) {
151
+
152
+ }
153
+ const tag = line.SYSLOG_IDENTIFIER === 'node' ? undefined : line.SYSLOG_IDENTIFIER;
154
+ const priority = {
155
+ "7": "DBG",
156
+ "3": "ERR",
157
+ "4": "WRN",
158
+ "6": "LOG",
159
+ }[line.PRIORITY];
40
160
  const time = parseInt(line.__REALTIME_TIMESTAMP.slice(0, -3));
41
161
  const sym = line._TRANSPORT === 'stdout' ? ' > ' : '‼> ';
42
162
  // Unwrap the line if it's an array of numbers
43
163
  let message = Array.isArray(line.MESSAGE) && (typeof line.MESSAGE[0] === 'number')
44
164
  ? decoder.decode(new Uint8Array(line.MESSAGE))
45
165
  : line.MESSAGE;
46
-
47
- if (!useColor) {
48
- // Strip colors if not on a terminal
49
- message = message.replace(ansiEscRx, '');
50
- }
51
-
52
- // Reset and restore the current ANSI color state
53
- const output = `${RESET}${formatDateTime(new Date(time))}${sym}${SET(state)}${message}${RESET}`;
54
-
55
- // Capture the last esc[*m instance in the message, and store it to be restored before the next message.
56
- // Unnessesary if not on a terminal.
57
- if (useColor) {
58
- state = [...(message.matchAll(ansiEscRx) ?? [])].at(-1)?.[1] ?? '0';
166
+ let stamp = formatDateTime(new Date(time));
167
+ const payload = { sym, tag, priority, stamp, length: message.replace(ansiEscRx, '').length, message };
168
+ history.push(payload);
169
+ while (history.length > 2000) {
170
+ history.shift();
59
171
  }
60
- console.log(output);
172
+ drawMessage(payload);
61
173
  }
62
174
  }