spooder 3.0.7 → 3.1.0
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 +291 -21
- package/package.json +4 -1
- package/src/api.d.ts +2 -1
- package/src/api.ts +38 -2
- package/src/cli.ts +54 -80
- package/src/config.d.ts +15 -0
- package/src/config.ts +78 -0
- package/src/dispatch.d.ts +1 -0
- package/src/dispatch.ts +223 -0
- package/src/utils.d.ts +8 -0
- package/src/utils.ts +55 -0
package/README.md
CHANGED
|
@@ -2,13 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
# Spooder ·  [](LICENSE)  
|
|
4
4
|
|
|
5
|
-
`spooder` is a purpose-built
|
|
5
|
+
`spooder` is a purpose-built server solution written using the [Bun](https://bun.sh/) runtime.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### What does it do?
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
`spooder` consists of a command-line tool which provides automatic updating/restarting and canary functionality, and a building-block API for creating servers.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
### Should I use it?
|
|
12
|
+
|
|
13
|
+
Probably not. You are free to use `spooder` if you fully understand the risks and limitations of doing so, however here is a list of things you should consider before using it:
|
|
14
|
+
|
|
15
|
+
⚠️ This is not a Node.js package. It is built using the [Bun](https://bun.sh/) runtime, which is still experimental as of writing.
|
|
16
|
+
|
|
17
|
+
⚠️ It is designed to be highly opinionated and is not intended to be a general-purpose server, so configuration is limited.
|
|
18
|
+
|
|
19
|
+
⚠️ It is not a full-featured web server and only provides the functionality as required for the projects it has been built for.
|
|
20
|
+
|
|
21
|
+
⚠️ It has not been battle-tested and may contain bugs or security issues. The authors of this project are not responsible for any problems caused by using this software.
|
|
22
|
+
|
|
23
|
+
# Installation
|
|
12
24
|
|
|
13
25
|
```bash
|
|
14
26
|
# Installing globally for CLI runner usage.
|
|
@@ -18,7 +30,28 @@ bun add spooder --global
|
|
|
18
30
|
bun add spooder
|
|
19
31
|
```
|
|
20
32
|
|
|
21
|
-
|
|
33
|
+
# Configuration
|
|
34
|
+
|
|
35
|
+
Both the runner and the API are configured in the same way by providing a `spooder` object in your `package.json` file.
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"spooder": {
|
|
40
|
+
"autoRestart": 5000,
|
|
41
|
+
"run": "bun run index.ts",
|
|
42
|
+
"update": [
|
|
43
|
+
"git pull",
|
|
44
|
+
"bun install"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
If there are any issues with the provided configuration, a warning will be printed to the console but will not halt execution. `spooder` will always fall back to default values where invalid configuration is provided.
|
|
51
|
+
|
|
52
|
+
Configuration warnings **do not** raise `caution` events with the `spooder` canary functionality.
|
|
53
|
+
|
|
54
|
+
# Runner
|
|
22
55
|
|
|
23
56
|
`spooder` includes a global command-line tool for running servers. It is recommended that you run this in a `screen` session.
|
|
24
57
|
|
|
@@ -28,9 +61,9 @@ cd /var/www/my_server/
|
|
|
28
61
|
spooder
|
|
29
62
|
```
|
|
30
63
|
|
|
31
|
-
While the intended use of this runner is for web servers, it can be used to run anything. It provides two primary features: automatic updating and restarting.
|
|
64
|
+
While the intended use of this runner is for web servers, it can be used to run anything. It provides two primary features: automatic updating and automatic restarting.
|
|
32
65
|
|
|
33
|
-
|
|
66
|
+
## Entry Point
|
|
34
67
|
|
|
35
68
|
`spooder` will attempt to launch the server from the current working directory using the command `bun run index.ts` as a default.
|
|
36
69
|
|
|
@@ -46,13 +79,9 @@ To customize this, provide an alternative command via the `run` configuration.
|
|
|
46
79
|
|
|
47
80
|
While `spooder` uses a `bun run` command by default, it is possible to use any command string.
|
|
48
81
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
### Auto Restart
|
|
82
|
+
## Auto Restart
|
|
52
83
|
|
|
53
|
-
In the event that the server exits (regardless of exit code), `spooder`
|
|
54
|
-
|
|
55
|
-
This feature is enabled by default with a delay of `5000` milliseconds. The delay can be changed by providing a value for `autoRestart` in the configuration.
|
|
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 `autoRestart` in the configuration.
|
|
56
85
|
|
|
57
86
|
```json
|
|
58
87
|
{
|
|
@@ -64,20 +93,28 @@ This feature is enabled by default with a delay of `5000` milliseconds. The dela
|
|
|
64
93
|
|
|
65
94
|
If set to `0`, the server will be restarted immediately without delay. If set to `-1`, the server will not be restarted at all.
|
|
66
95
|
|
|
67
|
-
|
|
96
|
+
## Auto Update
|
|
68
97
|
|
|
69
|
-
When starting your server, `spooder` can automatically update the source code in the working directory. To enable this feature,
|
|
98
|
+
When starting your server, `spooder` can automatically update the source code in the working directory. To enable this feature, the necessary update commands can be provided in the configuration as an array of strings.
|
|
70
99
|
|
|
71
100
|
```json
|
|
72
101
|
{
|
|
73
102
|
"spooder": {
|
|
74
|
-
"update":
|
|
103
|
+
"update": [
|
|
104
|
+
"git pull",
|
|
105
|
+
"bun install"
|
|
106
|
+
]
|
|
75
107
|
}
|
|
76
108
|
}
|
|
77
109
|
```
|
|
78
|
-
It is worth nothing that if the `update` command fails to execute, the server will still be started. This is preferred over entering a restart loop or failing to start the server at all.
|
|
79
110
|
|
|
80
|
-
|
|
111
|
+
Commands will be executed in sequence, and the server will not be started until after the commands have resolved.
|
|
112
|
+
|
|
113
|
+
Each command should be a separate item in the array. Chaining commands in a single string using the `&&` or `||` operators will not work.
|
|
114
|
+
|
|
115
|
+
If a command in the sequence fails, the remaining commands will not be executed, however the server will still be started. This is preferred over entering a restart loop or failing to start the server at all.
|
|
116
|
+
|
|
117
|
+
As well as being executed when the server is first started, the `update` commands are also run when `spooder` automatically restarts the server after it exits.
|
|
81
118
|
|
|
82
119
|
You can utilize this to automatically update your server in response to a webhook or other event by simply exiting the process.
|
|
83
120
|
|
|
@@ -88,13 +125,246 @@ events.on('receive-webhook', () => {
|
|
|
88
125
|
});
|
|
89
126
|
```
|
|
90
127
|
|
|
91
|
-
##
|
|
128
|
+
## Canary
|
|
129
|
+
|
|
130
|
+
`canary` is a feature in `spooder` which allows server problems to be raised as issues in your repository on GitHub.
|
|
131
|
+
|
|
132
|
+
To enable this feature, there are a couple of steps you need to take.
|
|
133
|
+
|
|
134
|
+
### 1. Create a GitHub App
|
|
135
|
+
|
|
136
|
+
Create a new GitHub App either on your personal account or on an organization. The app will need the following permissions:
|
|
137
|
+
|
|
138
|
+
- **Issues** - Read & Write
|
|
139
|
+
- **Metadata** - Read-only
|
|
140
|
+
|
|
141
|
+
Once created, install the GitHub App to your account. The app will need to be given access to the repositories you want to use the canary feature with.
|
|
142
|
+
|
|
143
|
+
In addition to the **App ID** that is assigned automatically, you will also need to generate a **Private Key** for the app. This can be done by clicking the **Generate a private key** button on the app page.
|
|
144
|
+
|
|
145
|
+
> Note: The private keys provided by GitHub are in PKCS#1 format, but only PKCS#8 is supported. You can convert the key file with the following command.
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in private-key.pem -out private-key-pkcs8.key
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Each server that intends to use the canary feature will need to have the private key installed somewhere the server process can access it.
|
|
152
|
+
|
|
153
|
+
### 2. Add package.json configuration
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
"spooder": {
|
|
157
|
+
"canary": {
|
|
158
|
+
"account": "<GITHUB_ACCOUNT_NAME>",
|
|
159
|
+
"repository": "<GITHUB_REPOSITORY>",
|
|
160
|
+
"labels": ["some-label"]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Replace `<GITHUB_ACCOUNT_NAME>` with the account name you have installed the GitHub App to, and `<GITHUB_REPOSITORY>` with the repository name you want to use for issues.
|
|
166
|
+
|
|
167
|
+
The repository name must in the format `owner/repo` (e.g. `facebook/react`).
|
|
168
|
+
|
|
169
|
+
The `labels` property can be used to provide a list of labels to automatically add to the issue. This property is optional and can be omitted.
|
|
170
|
+
|
|
171
|
+
### 3. Setup environment variables
|
|
172
|
+
|
|
173
|
+
The following two environment variables must be defined on the server.
|
|
174
|
+
|
|
175
|
+
```
|
|
176
|
+
SPOODER_CANARY_APP_ID=1234
|
|
177
|
+
SPOODER_CANARY_KEY=/home/bond/.ssh/id_007_pcks8.key
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`SPOODER_CANARY_APP_ID` is the **App ID** as shown on the GitHub App page.
|
|
181
|
+
`SPOODER_CANARY_KEY` is the path to the private key file in PKCS#8 format.
|
|
182
|
+
|
|
183
|
+
### 4. Use canary
|
|
184
|
+
|
|
185
|
+
Once configured, `spooder` will automatically raise an issue when the server exits with a non-zero exit code.
|
|
186
|
+
|
|
187
|
+
In addition, you can manually raise issues using the `spooder` API by calling `caution()` or `panic()`. More information about these functions can be found in the `API` section.
|
|
188
|
+
|
|
189
|
+
## Crash
|
|
190
|
+
|
|
191
|
+
It is recommended that you harden your server code against unexpected exceptions and use `panic()` and `caution()` to raise issues with selected diagnostic information.
|
|
192
|
+
|
|
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
|
+
|
|
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`.
|
|
196
|
+
|
|
197
|
+
```json
|
|
198
|
+
{
|
|
199
|
+
"exitCode": 1,
|
|
200
|
+
"stderr": [
|
|
201
|
+
"[2.48ms] \".env.local\"",
|
|
202
|
+
"Test output",
|
|
203
|
+
"Test output",
|
|
204
|
+
"4 | console.warn('Test output');",
|
|
205
|
+
"5 | ",
|
|
206
|
+
"6 | // Create custom error class.",
|
|
207
|
+
"7 | class TestError extends Error {",
|
|
208
|
+
"8 | constructor(message: string) {",
|
|
209
|
+
"9 | super(message);",
|
|
210
|
+
" ^",
|
|
211
|
+
"TestError: Home is [IPv4 address]",
|
|
212
|
+
" at new TestError (/mnt/i/spooder/test.ts:9:2)",
|
|
213
|
+
" at /mnt/i/spooder/test.ts:13:6",
|
|
214
|
+
""
|
|
215
|
+
]
|
|
216
|
+
}
|
|
217
|
+
```
|
|
92
218
|
|
|
93
|
-
|
|
219
|
+
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
|
+
|
|
221
|
+
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
|
+
|
|
223
|
+
## Sanitization
|
|
224
|
+
|
|
225
|
+
All reports sent via the canary feature are sanitized to prevent sensitive information from being leaked. This includes:
|
|
226
|
+
|
|
227
|
+
- Environment variables from `.env.local`
|
|
228
|
+
- IPv4 / IPv6 addresses.
|
|
229
|
+
- E-mail addresses.
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# .env.local
|
|
233
|
+
DB_PASSWORD=secret
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
await panic({
|
|
238
|
+
a: 'foo',
|
|
239
|
+
b: process.env.DB_PASSWORD,
|
|
240
|
+
c: 'Hello person@place.net',
|
|
241
|
+
d: 'Client: 192.168.1.1'
|
|
242
|
+
});
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
```json
|
|
246
|
+
[
|
|
247
|
+
{
|
|
248
|
+
"a": "foo",
|
|
249
|
+
"b": "[redacted]",
|
|
250
|
+
"c": "Hello [e-mail address]",
|
|
251
|
+
"d": "Client: [IPv4 address]"
|
|
252
|
+
}
|
|
253
|
+
]
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
The sanitization behavior can be disabled by setting `spooder.canary.sanitize` to `false` in the configuration. This is not recommended as it may leak sensitive information.
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"spooder": {
|
|
261
|
+
"canary": {
|
|
262
|
+
"sanitize": false
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
While this sanitization adds a layer of protection against information leaking, it does not catch everything. You should pay special attention to messages and objects provided to the canary to not unintentionally leak sensitive information.
|
|
269
|
+
|
|
270
|
+
## System Information
|
|
271
|
+
|
|
272
|
+
In addition to the information provided by the developer, `spooder` also includes some system information in the canary reports.
|
|
273
|
+
|
|
274
|
+
```json
|
|
275
|
+
{
|
|
276
|
+
"loadavg": [
|
|
277
|
+
0,
|
|
278
|
+
0,
|
|
279
|
+
0
|
|
280
|
+
],
|
|
281
|
+
"memory": {
|
|
282
|
+
"free": 7620907008,
|
|
283
|
+
"total": 8261840896
|
|
284
|
+
},
|
|
285
|
+
"platform": "linux",
|
|
286
|
+
"uptime": 7123,
|
|
287
|
+
"versions": {
|
|
288
|
+
"node": "18.15.0",
|
|
289
|
+
"bun": "0.6.5",
|
|
290
|
+
"webkit": "60d11703a533fd694cd1d6ddda04813eecb5d69f",
|
|
291
|
+
"boringssl": "b275c5ce1c88bc06f5a967026d3c0ce1df2be815",
|
|
292
|
+
"libarchive": "dc321febde83dd0f31158e1be61a7aedda65e7a2",
|
|
293
|
+
"mimalloc": "3c7079967a269027e438a2aac83197076d9fe09d",
|
|
294
|
+
"picohttpparser": "066d2b1e9ab820703db0837a7255d92d30f0c9f5",
|
|
295
|
+
"uwebsockets": "70b1b9fc1341e8b791b42c5447f90505c2abe156",
|
|
296
|
+
"zig": "0.11.0-dev.2571+31738de28",
|
|
297
|
+
"zlib": "885674026394870b7e7a05b7bf1ec5eb7bd8a9c0",
|
|
298
|
+
"tinycc": "2d3ad9e0d32194ad7fd867b66ebe218dcc8cb5cd",
|
|
299
|
+
"lolhtml": "2eed349dcdfa4ff5c19fe7c6e501cfd687601033",
|
|
300
|
+
"ares": "0e7a5dee0fbb04080750cf6eabbe89d8bae87faa",
|
|
301
|
+
"usockets": "fafc241e8664243fc0c51d69684d5d02b9805134",
|
|
302
|
+
"v8": "10.8.168.20-node.8",
|
|
303
|
+
"uv": "1.44.2",
|
|
304
|
+
"napi": "8",
|
|
305
|
+
"modules": "108"
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
# API
|
|
311
|
+
|
|
312
|
+
`spooder` exposes a build-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
|
+
|
|
314
|
+
```ts
|
|
315
|
+
import { ... } from 'spooder';
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
#### `caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>`
|
|
319
|
+
Raise a warning issue on GitHub. This is useful for non-fatal errors which you want to be notified about.
|
|
320
|
+
|
|
321
|
+
```ts
|
|
322
|
+
try {
|
|
323
|
+
// connect to database
|
|
324
|
+
} catch (e) {
|
|
325
|
+
await caution('Failed to connect to database', e);
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
Providing a custom error message is optional and can be omitted. Additionally you can also provide additional error objects which will be serialized to JSON and included in the report.
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
caution(e); // provide just the error
|
|
333
|
+
caution(e, { foo: 42 }); // additional data
|
|
334
|
+
caution('Custom error', e, { foo: 42 }); // all
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
To prevent spam, issues raised with `caution()` are rate-limited based on a configurable threshold in seconds. By default, the threshold is set to 24 hours per unique issue.
|
|
338
|
+
|
|
339
|
+
```json
|
|
340
|
+
{
|
|
341
|
+
"spooder": {
|
|
342
|
+
"canary": {
|
|
343
|
+
"throttle": 86400
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Issues are considered unique by the `err_message` parameter, so it is recommended that you do not include any dynamic information in this parameter that would prevent the issue from being unique.
|
|
350
|
+
|
|
351
|
+
If you need to provide unique information, you can use the `err` parameter to provide an object which will be serialized to JSON and included in the issue body.
|
|
94
352
|
|
|
95
353
|
```ts
|
|
96
|
-
|
|
354
|
+
const some_important_value = Math.random();
|
|
355
|
+
|
|
356
|
+
// Bad: Do not use dynamic information in err_message.
|
|
357
|
+
await caution('Error with number ' + some_important_value);
|
|
358
|
+
|
|
359
|
+
// Good: Use err parameter to provide dynamic information.
|
|
360
|
+
await caution('Error with number', { some_important_value });
|
|
97
361
|
```
|
|
362
|
+
It is not required that you `await` the `caution()`, and in situations where parallel processing is required, it is recommended that you do not.
|
|
363
|
+
|
|
364
|
+
#### `panic(err_message_or_obj: string | object, ...err: object[]): Promise<void>`
|
|
365
|
+
This behaves the same as `caution()` with the difference that once `panic()` has raised the issue, it will exit the process with a non-zero exit code.
|
|
366
|
+
|
|
367
|
+
This should only be called in worst-case scenarios where the server cannot continue to run. Since the process will exit, it is recommended that you `await` the `panic()` call.
|
|
98
368
|
|
|
99
369
|
## License
|
|
100
370
|
The code in this repository is licensed under the ISC license. See the [LICENSE](LICENSE) file for more information.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spooder",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.0
|
|
4
|
+
"version": "3.1.0",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
7
|
"bun": "./src/api.ts",
|
|
@@ -15,5 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"bin": {
|
|
17
17
|
"spooder": "./src/cli.ts"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@octokit/app": "^13.1.5"
|
|
18
21
|
}
|
|
19
22
|
}
|
package/src/api.d.ts
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export declare function
|
|
1
|
+
export declare function panic(err_message_or_obj: string | object, ...err: object[]): Promise<void>;
|
|
2
|
+
export declare function caution(err_message_or_obj: string | object, ...err: object[]): Promise<void>;
|
package/src/api.ts
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { dispatch_report } from './dispatch';
|
|
2
|
+
|
|
3
|
+
async function handle_error(prefix: string, err_message_or_obj: string | object, ...err: unknown[]): Promise<void> {
|
|
4
|
+
let error_message = 'unknown error';
|
|
5
|
+
|
|
6
|
+
if (typeof err_message_or_obj === 'string') {
|
|
7
|
+
error_message = err_message_or_obj;
|
|
8
|
+
err.unshift(error_message);
|
|
9
|
+
} else {
|
|
10
|
+
if (err_message_or_obj instanceof Error)
|
|
11
|
+
error_message = err_message_or_obj.message;
|
|
12
|
+
|
|
13
|
+
err.push(err_message_or_obj);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Serialize error objects.
|
|
17
|
+
err = err.map(e => {
|
|
18
|
+
if (e instanceof Error) {
|
|
19
|
+
return {
|
|
20
|
+
name: e.name,
|
|
21
|
+
message: e.message,
|
|
22
|
+
stack: e.stack?.split('\n') ?? []
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return e;
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
await dispatch_report(prefix + error_message, err);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function panic(err_message_or_obj: string | object, ...err: object[]): Promise<void> {
|
|
33
|
+
await handle_error('panic: ', err_message_or_obj, ...err);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function caution(err_message_or_obj: string | object, ...err: object[]): Promise<void> {
|
|
38
|
+
await handle_error('caution: ', err_message_or_obj, ...err);
|
|
3
39
|
}
|
package/src/cli.ts
CHANGED
|
@@ -1,99 +1,73 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import
|
|
2
|
+
import { get_config } from './config';
|
|
3
|
+
import { parse_command_line, log, strip_color_codes } from './utils';
|
|
4
|
+
import { dispatch_report } from './dispatch';
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
async function load_config(): Promise<Config> {
|
|
7
|
-
try {
|
|
8
|
-
const config_file = Bun.file(path.join(process.cwd(), 'package.json'));
|
|
9
|
-
const json = await config_file.json();
|
|
10
|
-
|
|
11
|
-
return json?.spooder ?? {};
|
|
12
|
-
} catch (e) {
|
|
13
|
-
return {};
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function parse_command(command: string): string[] {
|
|
18
|
-
const args = [];
|
|
19
|
-
let current_arg = '';
|
|
20
|
-
let in_quotes = false;
|
|
21
|
-
let in_escape = false;
|
|
22
|
-
|
|
23
|
-
for (let i = 0; i < command.length; i++) {
|
|
24
|
-
const char = command[i];
|
|
25
|
-
|
|
26
|
-
if (in_escape) {
|
|
27
|
-
current_arg += char;
|
|
28
|
-
in_escape = false;
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (char === '\\') {
|
|
33
|
-
in_escape = true;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (char === '"') {
|
|
38
|
-
in_quotes = !in_quotes;
|
|
39
|
-
continue;
|
|
40
|
-
}
|
|
6
|
+
async function start_server() {
|
|
7
|
+
log('start_server');
|
|
41
8
|
|
|
42
|
-
|
|
43
|
-
args.push(current_arg);
|
|
44
|
-
current_arg = '';
|
|
45
|
-
continue;
|
|
46
|
-
}
|
|
9
|
+
const config = await get_config();
|
|
47
10
|
|
|
48
|
-
|
|
49
|
-
|
|
11
|
+
const update_commands = config.update;
|
|
12
|
+
const n_update_commands = update_commands.length;
|
|
50
13
|
|
|
51
|
-
if (
|
|
52
|
-
|
|
14
|
+
if (n_update_commands > 0) {
|
|
15
|
+
log('running %d update commands', n_update_commands);
|
|
53
16
|
|
|
54
|
-
|
|
55
|
-
|
|
17
|
+
for (let i = 0; i < n_update_commands; i++) {
|
|
18
|
+
const config_update_command = update_commands[i];
|
|
56
19
|
|
|
57
|
-
|
|
58
|
-
console.log('[spooder] ' + message, ...args);
|
|
59
|
-
}
|
|
20
|
+
log('[%d] %s', i, config_update_command);
|
|
60
21
|
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
22
|
+
const update_proc = Bun.spawn(parse_command_line(config_update_command), {
|
|
23
|
+
cwd: process.cwd(),
|
|
24
|
+
stdout: 'inherit',
|
|
25
|
+
stderr: 'inherit'
|
|
26
|
+
});
|
|
65
27
|
|
|
66
|
-
|
|
67
|
-
log('start_server');
|
|
28
|
+
await update_proc.exited;
|
|
68
29
|
|
|
69
|
-
|
|
70
|
-
log('running update command: %s', config_update_command);
|
|
71
|
-
const update = Bun.spawn(parse_command(config_update_command), {
|
|
72
|
-
cwd: process.cwd(),
|
|
73
|
-
stdout: 'inherit',
|
|
74
|
-
stderr: 'inherit'
|
|
75
|
-
});
|
|
30
|
+
log('[%d] exited with code %d', i, update_proc.exitCode);
|
|
76
31
|
|
|
77
|
-
|
|
78
|
-
|
|
32
|
+
if (update_proc.exitCode !== 0) {
|
|
33
|
+
log('aborting update due to non-zero exit code from [%d]', i);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
79
37
|
}
|
|
80
38
|
|
|
81
|
-
|
|
39
|
+
Bun.spawn(parse_command_line(config.run), {
|
|
82
40
|
cwd: process.cwd(),
|
|
83
41
|
stdout: 'inherit',
|
|
84
|
-
stderr: '
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
42
|
+
stderr: 'pipe',
|
|
43
|
+
|
|
44
|
+
onExit: (proc, exitCode, signal) => {
|
|
45
|
+
log('server exited with code %d', exitCode);
|
|
46
|
+
|
|
47
|
+
if (exitCode !== null && exitCode > 0) {
|
|
48
|
+
if (proc.stderr !== undefined) {
|
|
49
|
+
const res = new Response(proc.stderr as ReadableStream);
|
|
50
|
+
|
|
51
|
+
res.text().then(async stderr => {
|
|
52
|
+
await dispatch_report('crash: server exited unexpectedly', [{
|
|
53
|
+
exitCode,
|
|
54
|
+
stderr: strip_color_codes(stderr).split(/\r?\n/)
|
|
55
|
+
}]);
|
|
56
|
+
});
|
|
57
|
+
} else {
|
|
58
|
+
dispatch_report('crash: service exited unexpectedly', [{
|
|
59
|
+
exitCode
|
|
60
|
+
}]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const auto_restart_ms = config.autoRestart;
|
|
65
|
+
if (auto_restart_ms > -1) {
|
|
66
|
+
log('restarting server in %dms', auto_restart_ms);
|
|
67
|
+
setTimeout(start_server, auto_restart_ms);
|
|
68
|
+
}
|
|
95
69
|
}
|
|
96
70
|
});
|
|
97
71
|
}
|
|
98
72
|
|
|
99
|
-
start_server();
|
|
73
|
+
await start_server();
|
package/src/config.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare const internal_config: {
|
|
2
|
+
run: string;
|
|
3
|
+
autoRestart: number;
|
|
4
|
+
update: never[];
|
|
5
|
+
canary: {
|
|
6
|
+
account: string;
|
|
7
|
+
repository: string;
|
|
8
|
+
labels: never[];
|
|
9
|
+
throttle: number;
|
|
10
|
+
sanitize: boolean;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
type Config = typeof internal_config;
|
|
14
|
+
export declare function get_config(): Promise<Config>;
|
|
15
|
+
export {};
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { warn } from './utils';
|
|
3
|
+
|
|
4
|
+
const internal_config = {
|
|
5
|
+
run: 'bun run index.ts',
|
|
6
|
+
autoRestart: -1,
|
|
7
|
+
update: [],
|
|
8
|
+
canary: {
|
|
9
|
+
account: '',
|
|
10
|
+
repository: '',
|
|
11
|
+
labels: [],
|
|
12
|
+
throttle: 86400,
|
|
13
|
+
sanitize: true
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type Config = typeof internal_config;
|
|
18
|
+
type ConfigObject = Record<string, unknown>;
|
|
19
|
+
|
|
20
|
+
function validate_config_option(source: ConfigObject, target: ConfigObject, root_name: string) {
|
|
21
|
+
for (const [key, value] of Object.entries(target)) {
|
|
22
|
+
const key_name = `${root_name}.${key}`;
|
|
23
|
+
if (key in source) {
|
|
24
|
+
const default_value = source[key as keyof Config];
|
|
25
|
+
const expected_type = typeof default_value;
|
|
26
|
+
|
|
27
|
+
const actual_type = typeof value;
|
|
28
|
+
|
|
29
|
+
if (actual_type !== expected_type) {
|
|
30
|
+
warn('ignoring invalid configuration value `%s` (expected %s, got %s)', key_name, expected_type, actual_type);
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (actual_type === 'object') {
|
|
35
|
+
const is_default_array = Array.isArray(default_value);
|
|
36
|
+
const is_actual_array = Array.isArray(value);
|
|
37
|
+
|
|
38
|
+
if (is_default_array) {
|
|
39
|
+
if (!is_actual_array) {
|
|
40
|
+
warn('ignoring invalid configuration value `%s` (expected array)', key_name);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
source[key as keyof Config] = value as Config[keyof Config];
|
|
45
|
+
} else {
|
|
46
|
+
if (is_actual_array) {
|
|
47
|
+
warn('ignoring invalid configuration value `%s` (expected object)', key_name);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
validate_config_option(default_value as ConfigObject, value as ConfigObject, key_name);
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
source[key as keyof Config] = value as Config[keyof Config];
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
warn('ignoring unknown configuration key `%s`', key_name);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function get_config(): Promise<Config> {
|
|
63
|
+
try {
|
|
64
|
+
const config_file = Bun.file(path.join(process.cwd(), 'package.json'));
|
|
65
|
+
const json = await config_file.json();
|
|
66
|
+
|
|
67
|
+
if (json.spooder === null || typeof json.spooder !== 'object') {
|
|
68
|
+
warn('failed to parse spooder configuration in package.json, using defaults');
|
|
69
|
+
return internal_config;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
validate_config_option(internal_config, json.spooder, 'spooder');
|
|
73
|
+
} catch (e) {
|
|
74
|
+
warn('failed to read package.json, using configuration defaults');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return internal_config;
|
|
78
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function dispatch_report(report_title: string, report_body: Array<unknown>): Promise<void>;
|
package/src/dispatch.ts
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { App } from '@octokit/app';
|
|
2
|
+
import { get_config } from './config';
|
|
3
|
+
import { warn, log } from './utils';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
|
|
8
|
+
async function load_local_env(): Promise<Map<string, string>> {
|
|
9
|
+
const env = new Map<string, string>();
|
|
10
|
+
|
|
11
|
+
const env_file = Bun.file(path.join(process.cwd(), '.env.local'));
|
|
12
|
+
|
|
13
|
+
if (env_file.size > 0) {
|
|
14
|
+
const env_text = await env_file.text();
|
|
15
|
+
const env_lines = env_text.split(/\r?\n/);
|
|
16
|
+
|
|
17
|
+
for (const line of env_lines) {
|
|
18
|
+
// Empty lines / comments
|
|
19
|
+
if (line.length === 0 || line.startsWith('#'))
|
|
20
|
+
continue;
|
|
21
|
+
|
|
22
|
+
const separator_index = line.indexOf('=');
|
|
23
|
+
if (separator_index === -1)
|
|
24
|
+
continue;
|
|
25
|
+
|
|
26
|
+
const key = line.slice(0, separator_index).trim();
|
|
27
|
+
let value = line.slice(separator_index + 1).trim();
|
|
28
|
+
|
|
29
|
+
// Strip quotes.
|
|
30
|
+
if (value.startsWith('"') && value.endsWith('"'))
|
|
31
|
+
value = value.slice(1, -1);
|
|
32
|
+
else if (value.startsWith("'") && value.endsWith("'"))
|
|
33
|
+
value = value.slice(1, -1);
|
|
34
|
+
|
|
35
|
+
env.set(key, value);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return env;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function save_cache_table(table: Map<bigint, number>, cache_file_path: string): Promise<void> {
|
|
43
|
+
const data = Buffer.alloc(4 + (table.size * 12));
|
|
44
|
+
|
|
45
|
+
let offset = 4;
|
|
46
|
+
data.writeUInt32LE(table.size, 0);
|
|
47
|
+
|
|
48
|
+
for (const [key, value] of table.entries()) {
|
|
49
|
+
data.writeBigUint64LE(key, offset);
|
|
50
|
+
offset += 8;
|
|
51
|
+
|
|
52
|
+
data.writeUInt32LE(value, offset);
|
|
53
|
+
offset += 4;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
await new Promise(resolve => fs.mkdir(path.dirname(cache_file_path), { recursive: true }, resolve));
|
|
57
|
+
await Bun.write(cache_file_path, data);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function check_cache_table(key: string, repository: string, expiry: number): Promise<boolean> {
|
|
61
|
+
if (expiry === 0)
|
|
62
|
+
return false;
|
|
63
|
+
|
|
64
|
+
const [owner, repo] = repository.split('/');
|
|
65
|
+
const cache_file_path = path.join(os.tmpdir(), 'spooder_canary', owner, repo, 'cache.bin');
|
|
66
|
+
|
|
67
|
+
const cache_table = new Map<bigint, number>();
|
|
68
|
+
const key_hash = BigInt(Bun.hash.wyhash(key));
|
|
69
|
+
|
|
70
|
+
const time_now = Math.floor(Date.now() / 1000);
|
|
71
|
+
const expiry_threshold = time_now - expiry;
|
|
72
|
+
|
|
73
|
+
let changed = false;
|
|
74
|
+
try {
|
|
75
|
+
const cache_file = Bun.file(cache_file_path);
|
|
76
|
+
|
|
77
|
+
if (cache_file.size > 0) {
|
|
78
|
+
const data = Buffer.from(await cache_file.arrayBuffer());
|
|
79
|
+
const entry_count = data.readUInt32LE(0);
|
|
80
|
+
|
|
81
|
+
let offset = 4;
|
|
82
|
+
for (let i = 0; i < entry_count; i++) {
|
|
83
|
+
const hash = data.readBigUInt64LE(offset);
|
|
84
|
+
offset += 8;
|
|
85
|
+
|
|
86
|
+
const expiry = data.readUInt32LE(offset);
|
|
87
|
+
offset += 4;
|
|
88
|
+
|
|
89
|
+
if (expiry >= expiry_threshold)
|
|
90
|
+
cache_table.set(hash, expiry);
|
|
91
|
+
else
|
|
92
|
+
changed = true;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
warn('Failed to read canary cache file ' + cache_file_path);
|
|
97
|
+
warn('Error: ' + (e as Error).message);
|
|
98
|
+
warn('You should resolve this issue to prevent spamming GitHub with canary reports.');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (cache_table.has(key_hash)) {
|
|
102
|
+
if (changed)
|
|
103
|
+
await save_cache_table(cache_table, cache_file_path);
|
|
104
|
+
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
cache_table.set(key_hash, time_now);
|
|
109
|
+
await save_cache_table(cache_table, cache_file_path);
|
|
110
|
+
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function sanitize_string(input: string, local_env?: Map<string, string>): string {
|
|
115
|
+
// Strip all potential e-mail addresses.
|
|
116
|
+
input = input.replaceAll(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9_-]+)/g, '[e-mail address]');
|
|
117
|
+
|
|
118
|
+
// Strip IPv4 addresses.
|
|
119
|
+
input = input.replaceAll(/([0-9]{1,3}\.){3}[0-9]{1,3}/g, '[IPv4 address]');
|
|
120
|
+
|
|
121
|
+
// Strip IPv6 addresses.
|
|
122
|
+
input = input.replaceAll(/([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}/g, '[IPv6 address]');
|
|
123
|
+
|
|
124
|
+
// Strip local environment variables.
|
|
125
|
+
if (local_env !== undefined) {
|
|
126
|
+
// Do not expose the name of the key redacted, as this may inadvertently expose the key/value
|
|
127
|
+
// if the value coincidentally appears in some other context.
|
|
128
|
+
for (const value of local_env.values())
|
|
129
|
+
input = input.replaceAll(value, '[redacted]');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return input;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function generate_diagnostics(): object {
|
|
136
|
+
return {
|
|
137
|
+
'loadavg': os.loadavg(),
|
|
138
|
+
'memory': {
|
|
139
|
+
'free': os.freemem(),
|
|
140
|
+
'total': os.totalmem(),
|
|
141
|
+
},
|
|
142
|
+
'platform': os.platform(),
|
|
143
|
+
'uptime': os.uptime(),
|
|
144
|
+
'versions': process.versions,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function dispatch_report(report_title: string, report_body: Array<unknown>): Promise<void> {
|
|
149
|
+
try {
|
|
150
|
+
const config = await get_config();
|
|
151
|
+
|
|
152
|
+
const canary_account = config.canary.account.toLowerCase();
|
|
153
|
+
const canary_repostiory = config.canary.repository.toLowerCase();
|
|
154
|
+
const canary_labels = config.canary.labels;
|
|
155
|
+
|
|
156
|
+
if (canary_account.length === 0|| canary_repostiory.length === 0)
|
|
157
|
+
return;
|
|
158
|
+
|
|
159
|
+
const is_cached = await check_cache_table(report_title, canary_repostiory, config.canary.throttle);
|
|
160
|
+
if (is_cached) {
|
|
161
|
+
warn('Throttled canary report: ' + report_title);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const canary_app_id = process.env.SPOODER_CANARY_APP_ID as string;
|
|
166
|
+
const canary_app_key = process.env.SPOODER_CANARY_KEY as string;
|
|
167
|
+
|
|
168
|
+
if (canary_app_id === undefined)
|
|
169
|
+
throw new Error('dispatch_report() called without SPOODER_CANARY_APP_ID environment variable set');
|
|
170
|
+
|
|
171
|
+
if (canary_app_key === undefined)
|
|
172
|
+
throw new Error('dispatch_report() called without SPOODER_CANARY_KEY environment variable set');
|
|
173
|
+
|
|
174
|
+
const key_file = Bun.file(canary_app_key);
|
|
175
|
+
if (key_file.size === 0)
|
|
176
|
+
throw new Error('dispatch_report() failed to read canary private key file');
|
|
177
|
+
|
|
178
|
+
const app_id = parseInt(canary_app_id, 10);
|
|
179
|
+
if (isNaN(app_id))
|
|
180
|
+
throw new Error('dispatch_report() failed to parse SPOODER_CANARY_APP_ID environment variable as integer');
|
|
181
|
+
|
|
182
|
+
const app = new App({
|
|
183
|
+
appId: app_id,
|
|
184
|
+
privateKey: await key_file.text(),
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
await app.octokit.request('GET /app');
|
|
188
|
+
|
|
189
|
+
const post_object = {
|
|
190
|
+
title: report_title,
|
|
191
|
+
body: '',
|
|
192
|
+
labels: canary_labels
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
report_body.push(generate_diagnostics());
|
|
196
|
+
|
|
197
|
+
if (config.canary.sanitize) {
|
|
198
|
+
const local_env = await load_local_env();
|
|
199
|
+
post_object.body = sanitize_string(JSON.stringify(report_body, null, 4), local_env);
|
|
200
|
+
post_object.title = sanitize_string(report_title, local_env);
|
|
201
|
+
} else {
|
|
202
|
+
post_object.body = JSON.stringify(report_body, null, 4);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
post_object.body = '```json\n' + post_object.body + '\n```\n\nℹ️ *This issue has been created automatically in response to a server panic, caution or crash.*';
|
|
206
|
+
|
|
207
|
+
for await (const { installation } of app.eachInstallation.iterator()) {
|
|
208
|
+
const login = (installation?.account as { login: string })?.login;
|
|
209
|
+
if (login?.toLowerCase() !== canary_account)
|
|
210
|
+
continue;
|
|
211
|
+
|
|
212
|
+
for await (const { octokit, repository } of app.eachRepository.iterator({ installationId: installation.id })) {
|
|
213
|
+
if (repository.full_name.toLowerCase() !== canary_repostiory)
|
|
214
|
+
continue;
|
|
215
|
+
|
|
216
|
+
await octokit.request('POST /repos/' + canary_repostiory + '/issues', post_object);
|
|
217
|
+
log('Dispatched canary report to %s: %s', canary_repostiory, report_title);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (e) {
|
|
221
|
+
warn('Failed to dispatch canary report: ' + (e as Error)?.message ?? 'unspecified error');
|
|
222
|
+
}
|
|
223
|
+
}
|
package/src/utils.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Logs a message to stdout with the prefix `[spooder] ` */
|
|
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
|
+
/** Strips ANSI color codes from a string */
|
|
6
|
+
export declare function strip_color_codes(str: string): string;
|
|
7
|
+
/** Converts a command line string into an array of arguments */
|
|
8
|
+
export declare function parse_command_line(command: string): string[];
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/** Logs a message to stdout with the prefix `[spooder] ` */
|
|
2
|
+
export function log(message: string, ...args: unknown[]): void {
|
|
3
|
+
console.log('[spooder] ' + message, ...args);
|
|
4
|
+
}
|
|
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
|
+
/** Strips ANSI color codes from a string */
|
|
12
|
+
export function strip_color_codes(str: string): string {
|
|
13
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Converts a command line string into an array of arguments */
|
|
17
|
+
export function parse_command_line(command: string): string[] {
|
|
18
|
+
const args = [];
|
|
19
|
+
let current_arg = '';
|
|
20
|
+
let in_quotes = false;
|
|
21
|
+
let in_escape = false;
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < command.length; i++) {
|
|
24
|
+
const char = command[i];
|
|
25
|
+
|
|
26
|
+
if (in_escape) {
|
|
27
|
+
current_arg += char;
|
|
28
|
+
in_escape = false;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (char === '\\') {
|
|
33
|
+
in_escape = true;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (char === '"') {
|
|
38
|
+
in_quotes = !in_quotes;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (char === ' ' && !in_quotes) {
|
|
43
|
+
args.push(current_arg);
|
|
44
|
+
current_arg = '';
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
current_arg += char;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (current_arg.length > 0)
|
|
52
|
+
args.push(current_arg);
|
|
53
|
+
|
|
54
|
+
return args;
|
|
55
|
+
}
|