spooder 3.2.6 → 3.2.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.
- package/README.md +27 -7
- package/package.json +1 -1
- package/src/api.ts +10 -3
- package/src/cli.ts +40 -19
- package/src/config.d.ts +2 -1
- package/src/config.ts +9 -8
- package/src/dispatch.ts +10 -6
- package/src/utils.d.ts +0 -2
- package/src/utils.ts +0 -5
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ Both the runner and the API are configured in the same way by providing a `spood
|
|
|
37
37
|
```json
|
|
38
38
|
{
|
|
39
39
|
"spooder": {
|
|
40
|
-
"
|
|
40
|
+
"auto_restart": 5000,
|
|
41
41
|
"run": "bun run index.ts",
|
|
42
42
|
"update": [
|
|
43
43
|
"git pull",
|
|
@@ -81,12 +81,12 @@ While `spooder` uses a `bun run` command by default, it is possible to use any c
|
|
|
81
81
|
|
|
82
82
|
## Auto Restart
|
|
83
83
|
|
|
84
|
-
In the event that the server exits (regardless of exit code), `spooder` can automatically restart it after a short delay. To enable this feature specify the restart delay in milliseconds as `
|
|
84
|
+
In the event that the server exits (regardless of exit code), `spooder` can automatically restart it after a short delay. To enable this feature specify the restart delay in milliseconds as `auto_restart` in the configuration.
|
|
85
85
|
|
|
86
86
|
```json
|
|
87
87
|
{
|
|
88
88
|
"spooder": {
|
|
89
|
-
"
|
|
89
|
+
"auto_restart": 5000
|
|
90
90
|
}
|
|
91
91
|
}
|
|
92
92
|
```
|
|
@@ -192,12 +192,12 @@ It is recommended that you harden your server code against unexpected exceptions
|
|
|
192
192
|
|
|
193
193
|
In the event that the server does encounter an unexpected exception which causes it to exit with a non-zero exit code, `spooder` will automatically raise an issue on GitHub using the canary feature, if configured.
|
|
194
194
|
|
|
195
|
-
Since this issue has been caught externally, `spooder` has no context of the exception which was raised. Instead, the canary report will contain the output from `stderr`.
|
|
195
|
+
Since this issue has been caught externally, `spooder` has no context of the exception which was raised. Instead, the canary report will contain the output from both `stdout` and `stderr`.
|
|
196
196
|
|
|
197
197
|
```json
|
|
198
198
|
{
|
|
199
|
-
"
|
|
200
|
-
"
|
|
199
|
+
"proc_exit_code": 1,
|
|
200
|
+
"console_output": [
|
|
201
201
|
"[2.48ms] \".env.local\"",
|
|
202
202
|
"Test output",
|
|
203
203
|
"Test output",
|
|
@@ -216,10 +216,26 @@ Since this issue has been caught externally, `spooder` has no context of the exc
|
|
|
216
216
|
}
|
|
217
217
|
```
|
|
218
218
|
|
|
219
|
+
The `proc_exit_code` property contains the exit code that the server exited with.
|
|
220
|
+
|
|
221
|
+
The `console_output` will contain the last `64` lines of output from `stdout` and `stderr` combined. This can be configured by setting the `spooder.canary.crash_console_history` property.
|
|
222
|
+
|
|
223
|
+
```json
|
|
224
|
+
{
|
|
225
|
+
"spooder": {
|
|
226
|
+
"canary": {
|
|
227
|
+
"crash_console_history": 128
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
219
233
|
This information is subject to sanitization, as described in the `Sanitization` section, however you should be aware that stack traces may contain sensitive information.
|
|
220
234
|
|
|
221
235
|
Additionally, Bun includes a relevant code snippet from the source file where the exception was raised. This is intended to help you identify the source of the problem.
|
|
222
236
|
|
|
237
|
+
Setting `spooder.canary.crash_console_history` to `0` will omit the `console_output` property from the report entirely, which may make it harder to diagnose the problem but will ensure that no sensitive information is leaked.
|
|
238
|
+
|
|
223
239
|
## Sanitization
|
|
224
240
|
|
|
225
241
|
All reports sent via the canary feature are sanitized to prevent sensitive information from being leaked. This includes:
|
|
@@ -303,13 +319,17 @@ In addition to the information provided by the developer, `spooder` also include
|
|
|
303
319
|
"uv": "1.44.2",
|
|
304
320
|
"napi": "8",
|
|
305
321
|
"modules": "108"
|
|
322
|
+
},
|
|
323
|
+
"bun": {
|
|
324
|
+
"version": "0.6.4",
|
|
325
|
+
"rev": "f02561530fda1ee9396f51c8bc99b38716e38296"
|
|
306
326
|
}
|
|
307
327
|
}
|
|
308
328
|
```
|
|
309
329
|
|
|
310
330
|
# API
|
|
311
331
|
|
|
312
|
-
`spooder` exposes a
|
|
332
|
+
`spooder` exposes a building-block style API for developing servers. The API is designed to be minimal to leave control in the hands of the developer and not add overhead for features you may not need.
|
|
313
333
|
|
|
314
334
|
```ts
|
|
315
335
|
import { ... } from 'spooder';
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -14,12 +14,19 @@ export class ErrorWithMetadata extends Error {
|
|
|
14
14
|
async resolve_metadata(): Promise<object> {
|
|
15
15
|
const metadata = Object.assign({}, this.metadata);
|
|
16
16
|
for (const [key, value] of Object.entries(metadata)) {
|
|
17
|
+
let resolved_value = value;
|
|
18
|
+
|
|
17
19
|
if (value instanceof Promise)
|
|
18
|
-
|
|
20
|
+
resolved_value = await value;
|
|
19
21
|
else if (typeof value === 'function')
|
|
20
|
-
|
|
22
|
+
resolved_value = value();
|
|
21
23
|
else if (value instanceof ReadableStream)
|
|
22
|
-
|
|
24
|
+
resolved_value = await Bun.readableStreamToText(value);
|
|
25
|
+
|
|
26
|
+
if (typeof resolved_value === 'string' && resolved_value.includes('\n'))
|
|
27
|
+
resolved_value = resolved_value.split(/\r?\n/);
|
|
28
|
+
|
|
29
|
+
metadata[key] = resolved_value;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
return metadata;
|
package/src/cli.ts
CHANGED
|
@@ -36,35 +36,56 @@ async function start_server() {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
const crash_console_history = config.canary.crash_console_history;
|
|
40
|
+
const include_crash_history = crash_console_history > 0;
|
|
41
|
+
|
|
42
|
+
const std_mode = include_crash_history ? 'pipe' : 'inherit';
|
|
39
43
|
const proc = Bun.spawn(parse_command_line(config.run), {
|
|
40
44
|
cwd: process.cwd(),
|
|
41
|
-
stdout:
|
|
42
|
-
stderr:
|
|
45
|
+
stdout: std_mode,
|
|
46
|
+
stderr: std_mode
|
|
43
47
|
});
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
const stream_history = new Array<string>();
|
|
50
|
+
if (include_crash_history) {
|
|
51
|
+
const text_decoder = new TextDecoder();
|
|
46
52
|
|
|
47
|
-
|
|
48
|
-
|
|
53
|
+
function capture_stream(stream: ReadableStream, output: NodeJS.WritableStream) {
|
|
54
|
+
const reader = stream.getReader();
|
|
49
55
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
reader.read().then(function read_chunk(chunk: ReadableStreamDefaultReadResult<Uint8Array>) {
|
|
57
|
+
if (chunk.done)
|
|
58
|
+
return;
|
|
59
|
+
|
|
60
|
+
const chunk_str = text_decoder.decode(chunk.value);
|
|
61
|
+
stream_history.push(...chunk_str.split(/\r?\n/));
|
|
62
|
+
|
|
63
|
+
if (stream_history.length > crash_console_history)
|
|
64
|
+
stream_history.splice(0, stream_history.length - crash_console_history);
|
|
65
|
+
|
|
66
|
+
output.write(chunk.value);
|
|
67
|
+
reader.read().then(read_chunk);
|
|
59
68
|
});
|
|
60
|
-
} else {
|
|
61
|
-
dispatch_report('crash: service exited unexpectedly', [{
|
|
62
|
-
proc_exit_code
|
|
63
|
-
}]);
|
|
64
69
|
}
|
|
70
|
+
|
|
71
|
+
capture_stream(proc.stdout as ReadableStream, process.stdout);
|
|
72
|
+
capture_stream(proc.stderr as ReadableStream, process.stderr);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
await proc.exited;
|
|
76
|
+
|
|
77
|
+
const proc_exit_code = proc.exitCode;
|
|
78
|
+
log('server exited with code %s', proc_exit_code);
|
|
79
|
+
|
|
80
|
+
if (proc_exit_code !== 0) {
|
|
81
|
+
const console_output = include_crash_history ? strip_color_codes(stream_history.join('\n')) : undefined;
|
|
82
|
+
dispatch_report('crash: server exited unexpectedly', [{
|
|
83
|
+
proc_exit_code, console_output
|
|
84
|
+
}]);
|
|
65
85
|
}
|
|
86
|
+
|
|
66
87
|
|
|
67
|
-
const auto_restart_ms = config.
|
|
88
|
+
const auto_restart_ms = config.auto_restart;
|
|
68
89
|
if (auto_restart_ms > -1) {
|
|
69
90
|
log('restarting server in %dms', auto_restart_ms);
|
|
70
91
|
setTimeout(start_server, auto_restart_ms);
|
package/src/config.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
declare const internal_config: {
|
|
2
2
|
run: string;
|
|
3
|
-
|
|
3
|
+
auto_restart: number;
|
|
4
4
|
update: never[];
|
|
5
5
|
canary: {
|
|
6
6
|
account: string;
|
|
7
7
|
repository: string;
|
|
8
8
|
labels: never[];
|
|
9
|
+
crash_console_history: number;
|
|
9
10
|
throttle: number;
|
|
10
11
|
sanitize: boolean;
|
|
11
12
|
};
|
package/src/config.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { log } from './utils';
|
|
3
3
|
|
|
4
4
|
const internal_config = {
|
|
5
5
|
run: 'bun run index.ts',
|
|
6
|
-
|
|
6
|
+
auto_restart: -1,
|
|
7
7
|
update: [],
|
|
8
8
|
canary: {
|
|
9
9
|
account: '',
|
|
10
10
|
repository: '',
|
|
11
11
|
labels: [],
|
|
12
|
+
crash_console_history: 64,
|
|
12
13
|
throttle: 86400,
|
|
13
14
|
sanitize: true
|
|
14
15
|
}
|
|
@@ -27,7 +28,7 @@ function validate_config_option(source: ConfigObject, target: ConfigObject, root
|
|
|
27
28
|
const actual_type = typeof value;
|
|
28
29
|
|
|
29
30
|
if (actual_type !== expected_type) {
|
|
30
|
-
|
|
31
|
+
log('ignoring invalid configuration value `%s` (expected %s, got %s)', key_name, expected_type, actual_type);
|
|
31
32
|
continue;
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -37,14 +38,14 @@ function validate_config_option(source: ConfigObject, target: ConfigObject, root
|
|
|
37
38
|
|
|
38
39
|
if (is_default_array) {
|
|
39
40
|
if (!is_actual_array) {
|
|
40
|
-
|
|
41
|
+
log('ignoring invalid configuration value `%s` (expected array)', key_name);
|
|
41
42
|
continue;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
source[key as keyof Config] = value as Config[keyof Config];
|
|
45
46
|
} else {
|
|
46
47
|
if (is_actual_array) {
|
|
47
|
-
|
|
48
|
+
log('ignoring invalid configuration value `%s` (expected object)', key_name);
|
|
48
49
|
continue;
|
|
49
50
|
}
|
|
50
51
|
|
|
@@ -54,7 +55,7 @@ function validate_config_option(source: ConfigObject, target: ConfigObject, root
|
|
|
54
55
|
source[key as keyof Config] = value as Config[keyof Config];
|
|
55
56
|
}
|
|
56
57
|
} else {
|
|
57
|
-
|
|
58
|
+
log('ignoring unknown configuration key `%s`', key_name);
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
61
|
}
|
|
@@ -65,13 +66,13 @@ export async function get_config(): Promise<Config> {
|
|
|
65
66
|
const json = await config_file.json();
|
|
66
67
|
|
|
67
68
|
if (json.spooder === null || typeof json.spooder !== 'object') {
|
|
68
|
-
|
|
69
|
+
log('failed to parse spooder configuration in package.json, using defaults');
|
|
69
70
|
return internal_config;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
validate_config_option(internal_config, json.spooder, 'spooder');
|
|
73
74
|
} catch (e) {
|
|
74
|
-
|
|
75
|
+
log('failed to read package.json, using configuration defaults');
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
return internal_config;
|
package/src/dispatch.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { App } from '@octokit/app';
|
|
2
2
|
import { get_config } from './config';
|
|
3
|
-
import {
|
|
3
|
+
import { log } from './utils';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import os from 'node:os';
|
|
@@ -93,9 +93,9 @@ async function check_cache_table(key: string, repository: string, expiry: number
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
} catch (e) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
log('Failed to read canary cache file ' + cache_file_path);
|
|
97
|
+
log('Error: ' + (e as Error).message);
|
|
98
|
+
log('You should resolve this issue to prevent spamming GitHub with canary reports.');
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
if (cache_table.has(key_hash)) {
|
|
@@ -142,6 +142,10 @@ function generate_diagnostics(): object {
|
|
|
142
142
|
'platform': os.platform(),
|
|
143
143
|
'uptime': os.uptime(),
|
|
144
144
|
'versions': process.versions,
|
|
145
|
+
'bun': {
|
|
146
|
+
'version': Bun.version,
|
|
147
|
+
'rev': Bun.revision,
|
|
148
|
+
}
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
|
|
@@ -158,7 +162,7 @@ export async function dispatch_report(report_title: string, report_body: Array<u
|
|
|
158
162
|
|
|
159
163
|
const is_cached = await check_cache_table(report_title, canary_repostiory, config.canary.throttle);
|
|
160
164
|
if (is_cached) {
|
|
161
|
-
|
|
165
|
+
log('Throttled canary report: ' + report_title);
|
|
162
166
|
return;
|
|
163
167
|
}
|
|
164
168
|
|
|
@@ -218,6 +222,6 @@ export async function dispatch_report(report_title: string, report_body: Array<u
|
|
|
218
222
|
}
|
|
219
223
|
}
|
|
220
224
|
} catch (e) {
|
|
221
|
-
|
|
225
|
+
log('Failed to dispatch canary report: ' + (e as Error)?.message ?? 'unspecified error');
|
|
222
226
|
}
|
|
223
227
|
}
|
package/src/utils.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/** Logs a message to stdout with the prefix `[spooder] ` */
|
|
2
2
|
export declare function log(message: string, ...args: unknown[]): void;
|
|
3
|
-
/** Logs a message to stderr with the prefix `[spooder] ` */
|
|
4
|
-
export declare function warn(message: string, ...args: unknown[]): void;
|
|
5
3
|
/** Strips ANSI color codes from a string */
|
|
6
4
|
export declare function strip_color_codes(str: string): string;
|
|
7
5
|
/** Converts a command line string into an array of arguments */
|
package/src/utils.ts
CHANGED
|
@@ -3,11 +3,6 @@ export function log(message: string, ...args: unknown[]): void {
|
|
|
3
3
|
console.log('[spooder] ' + message, ...args);
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
-
/** Logs a message to stderr with the prefix `[spooder] ` */
|
|
7
|
-
export function warn(message: string, ...args: unknown[]): void {
|
|
8
|
-
console.error('[spooder] ' + message, ...args);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
6
|
/** Strips ANSI color codes from a string */
|
|
12
7
|
export function strip_color_codes(str: string): string {
|
|
13
8
|
return str.replace(/\x1b\[[0-9;]*m/g, '');
|