@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 +55 -0
- package/lib/config.js +7 -3
- package/lib/discordClient.js +30 -0
- package/package.json +2 -2
- package/service/index.js +9 -1
- package/service/logs.js +129 -17
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
|
-
|
|
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.
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
172
|
+
drawMessage(payload);
|
|
61
173
|
}
|
|
62
174
|
}
|