rtjscomp 0.9.1 → 0.9.2
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 +3 -0
- package/changes_before_git.txt +58 -0
- package/package.json +1 -1
- package/rtjscomp.js +469 -142
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ $ npm start
|
|
|
33
33
|
|
|
34
34
|
it is only needed if you want to change default settings. it has the following properties:
|
|
35
35
|
|
|
36
|
+
- `gzip_level`: compression level, 0-9, default is 9
|
|
37
|
+
- `log_verbose`: boolean, set true for more verbose logging or run with -v
|
|
36
38
|
- `path_aliases`: object where the key is the url path and the value is the file path
|
|
37
39
|
- `path_ghosts`: array of paths that are simply ignored, no response is sent
|
|
38
40
|
- `path_hiddens`: array of paths that give 404
|
|
@@ -106,6 +108,7 @@ in every served dynamic file (like .html), you can insert `<?` and `?>` tags to
|
|
|
106
108
|
request-independent services can be created using .service.js files referenced in services.txt.
|
|
107
109
|
in both file types (dynamic served and services), you can use all node/bun methods including `require`, but also those:
|
|
108
110
|
|
|
111
|
+
- `log(msg)`: logs the message to the console
|
|
109
112
|
- `service_require(service path)`: returns the matching service object
|
|
110
113
|
- `service_require_try(service path)`: returns the matching service object or null if not found or if disabled
|
|
111
114
|
- `rtjscomp`: has these properties/methods:
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
add compression of dynamic data
|
|
2
|
+
add https
|
|
3
|
+
add actions
|
|
4
|
+
0.5
|
|
5
|
+
add shutdown action
|
|
6
|
+
prefer gzip over lzma
|
|
7
|
+
move public files into separate dir
|
|
8
|
+
0.51
|
|
9
|
+
add list of raw files
|
|
10
|
+
0.52
|
|
11
|
+
ignore empty areas (<??> or ?><?)
|
|
12
|
+
0.53
|
|
13
|
+
move configuration data to separate files
|
|
14
|
+
0.54
|
|
15
|
+
add path aliases
|
|
16
|
+
block accessing directories
|
|
17
|
+
0.55
|
|
18
|
+
add http-header Content-Location
|
|
19
|
+
0.56
|
|
20
|
+
add list of file types not to compress
|
|
21
|
+
keep raw files in ram
|
|
22
|
+
remove lzma
|
|
23
|
+
add compression of raw files
|
|
24
|
+
load already compressed files
|
|
25
|
+
just compress when at least 100 bytes of data
|
|
26
|
+
0.57
|
|
27
|
+
cleanup code
|
|
28
|
+
execute and send file in parallel
|
|
29
|
+
add services
|
|
30
|
+
0.58
|
|
31
|
+
add analytic logger
|
|
32
|
+
use compressed file even if file type is not intended as being compressed
|
|
33
|
+
0.59
|
|
34
|
+
remove static files from cache when precompressed files got changed
|
|
35
|
+
0.591
|
|
36
|
+
set gzip level to 9
|
|
37
|
+
save automatically compressed files
|
|
38
|
+
0.592
|
|
39
|
+
block requests with invalid hostname
|
|
40
|
+
0.593
|
|
41
|
+
add user data storage
|
|
42
|
+
0.594
|
|
43
|
+
remove custom file init
|
|
44
|
+
asynchronize request handling
|
|
45
|
+
temporarily disable chaching of static files
|
|
46
|
+
0.595
|
|
47
|
+
send partial files
|
|
48
|
+
load and interpret file in parallel
|
|
49
|
+
optimize source codes of sent files
|
|
50
|
+
0.596
|
|
51
|
+
stop services before replacing them
|
|
52
|
+
service_require return service object
|
|
53
|
+
0.597
|
|
54
|
+
stop services on exit
|
|
55
|
+
0.598
|
|
56
|
+
read request body
|
|
57
|
+
0.599
|
|
58
|
+
???
|
package/package.json
CHANGED
package/rtjscomp.js
CHANGED
|
@@ -24,19 +24,30 @@ const AGENT_CHECK_BOT = /bot|googlebot|crawler|spider|robot|crawling|favicon/i;
|
|
|
24
24
|
const AGENT_CHECK_MOBIL = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i;
|
|
25
25
|
const GZIP_OPTIONS = {level: 9};
|
|
26
26
|
const HTTP_LIST_REG = /,\s*/;
|
|
27
|
-
const IMPORT_REG =
|
|
27
|
+
const IMPORT_REG = /\bimport\(/g;
|
|
28
28
|
const IS_BUN = typeof Bun !== 'undefined';
|
|
29
|
+
const LINENUMBER_REG = /:([0-9]+)[\):]/;
|
|
29
30
|
const PATH_CONFIG = 'config/';
|
|
30
31
|
const PATH_DATA = 'data/';
|
|
31
32
|
const PATH_PUBLIC = 'public/';
|
|
32
33
|
const RESOLVE_OPTIONS = {paths: [require('path').resolve()]};
|
|
34
|
+
const SERVICE_REQUIRE_REG = /\bservice_require\(([^)]*)\)/g;
|
|
35
|
+
const SERVICE_STATUS_PENDING = 0; // just added to list
|
|
36
|
+
const SERVICE_STATUS_WAITING = 1; // waiting for deps
|
|
37
|
+
const SERVICE_STATUS_STARTING = 2; // running startup
|
|
38
|
+
const SERVICE_STATUS_ACTIVE = 3; // ready for use
|
|
39
|
+
const SERVICE_STATUS_STOPPING = 4; // running stop fn
|
|
40
|
+
const SERVICE_STATUS_FAILED = 5; // waiting for fix
|
|
33
41
|
const VERSION = require('./package.json').version;
|
|
34
42
|
const WATCH_OPTIONS = {persistent: true, interval: 1000};
|
|
35
43
|
|
|
36
44
|
// config
|
|
37
|
-
|
|
45
|
+
const log_verbose_flag = process.argv.includes('-v');
|
|
46
|
+
let log_verbose = log_verbose_flag;
|
|
38
47
|
let port_http = 0;
|
|
39
48
|
let port_https = 0;
|
|
49
|
+
let gz_enabled = true;
|
|
50
|
+
let exiting = false;
|
|
40
51
|
/// any path -> file
|
|
41
52
|
const path_aliases = new Map([
|
|
42
53
|
['', 'index.html'],
|
|
@@ -108,7 +119,7 @@ if (!Object.fromEntries) {
|
|
|
108
119
|
const object = {};
|
|
109
120
|
for (const entry of entries) object[entry[0]] = entry[1];
|
|
110
121
|
return object;
|
|
111
|
-
}
|
|
122
|
+
}
|
|
112
123
|
}
|
|
113
124
|
|
|
114
125
|
// workaround for bun: https://github.com/oven-sh/bun/issues/18919
|
|
@@ -160,7 +171,11 @@ global.number_check_uint = number => (
|
|
|
160
171
|
rtjscomp.data_load = async name => {
|
|
161
172
|
if (log_verbose) log('load file: ' + PATH_DATA + name);
|
|
162
173
|
const data = await fsp.readFile(PATH_DATA + name, 'utf8').catch(() => null);
|
|
163
|
-
return
|
|
174
|
+
return (
|
|
175
|
+
name.endsWith('.json')
|
|
176
|
+
? JSON.parse(data || null)
|
|
177
|
+
: data
|
|
178
|
+
);
|
|
164
179
|
}
|
|
165
180
|
rtjscomp.data_load_watch = (name, callback) => (
|
|
166
181
|
file_keep_new(PATH_DATA + name, data => (
|
|
@@ -175,11 +190,33 @@ rtjscomp.data_save = (name, data) => (
|
|
|
175
190
|
log_verbose && log('save file: ' + PATH_DATA + name),
|
|
176
191
|
fsp.writeFile(
|
|
177
192
|
PATH_DATA + name,
|
|
178
|
-
|
|
193
|
+
(
|
|
194
|
+
name.endsWith('.json')
|
|
195
|
+
? JSON.stringify(data)
|
|
196
|
+
: data
|
|
197
|
+
),
|
|
179
198
|
'utf8'
|
|
180
199
|
)
|
|
181
200
|
)
|
|
182
201
|
|
|
202
|
+
/**
|
|
203
|
+
hack to guess the line number of an error
|
|
204
|
+
*/
|
|
205
|
+
const linenumber_try = err => {
|
|
206
|
+
try {
|
|
207
|
+
return `:${
|
|
208
|
+
err.stack
|
|
209
|
+
.split('\n', 2)[1]
|
|
210
|
+
.split(',').pop()
|
|
211
|
+
.match(LINENUMBER_REG)[1]
|
|
212
|
+
- 2
|
|
213
|
+
}`;
|
|
214
|
+
}
|
|
215
|
+
catch (_) {
|
|
216
|
+
return '';
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
183
220
|
const custom_require_paths = new Set;
|
|
184
221
|
const custom_require_cache = new Map;
|
|
185
222
|
const custom_import_cache = new Map;
|
|
@@ -218,153 +255,348 @@ actions.module_cache_clear = () => {
|
|
|
218
255
|
}
|
|
219
256
|
const AsyncFunction = custom_import.constructor;
|
|
220
257
|
|
|
221
|
-
const
|
|
222
|
-
|
|
258
|
+
const services = new Map;
|
|
259
|
+
let services_loaded_promise = null;
|
|
260
|
+
let services_loaded_promise_resolve = null;
|
|
261
|
+
/**
|
|
262
|
+
stop/start services according to list
|
|
263
|
+
*/
|
|
223
264
|
const services_list_react = async list => {
|
|
265
|
+
// stop all services not in list
|
|
224
266
|
await Promise.all(
|
|
225
|
-
|
|
226
|
-
.filter(
|
|
227
|
-
|
|
267
|
+
[...services.values()]
|
|
268
|
+
.filter(service_object => (
|
|
269
|
+
service_object.status < SERVICE_STATUS_STOPPING &&
|
|
270
|
+
!list.includes(service_object.path)
|
|
271
|
+
))
|
|
272
|
+
// so they will not be restarted
|
|
273
|
+
.map(service_object => (
|
|
274
|
+
service_object.dependencies = null,
|
|
275
|
+
service_object
|
|
276
|
+
))
|
|
277
|
+
.map(service_stop)
|
|
228
278
|
);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
279
|
+
// start all services in list
|
|
280
|
+
const start_queue = [];
|
|
281
|
+
for (const path of list) {
|
|
282
|
+
let service_object = services.get(path);
|
|
283
|
+
if (service_object == null) {
|
|
284
|
+
services.set(path, service_object = {
|
|
285
|
+
content: null,
|
|
286
|
+
dependencies: null,
|
|
287
|
+
dependencies_paths: null,
|
|
288
|
+
file_function: null,
|
|
289
|
+
handler_stop: null,
|
|
290
|
+
path,
|
|
291
|
+
promise_deps: null,
|
|
292
|
+
promise_deps_resolve: null,
|
|
293
|
+
promise_stopped: null,
|
|
294
|
+
promise_stopped_resolve: null,
|
|
295
|
+
status: SERVICE_STATUS_PENDING,
|
|
296
|
+
watcher: null,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
else if (service_object.status < SERVICE_STATUS_STOPPING) continue;
|
|
300
|
+
start_queue.push(service_object);
|
|
235
301
|
}
|
|
302
|
+
await Promise.all(
|
|
303
|
+
start_queue.map(service_start)
|
|
304
|
+
);
|
|
236
305
|
}
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
306
|
+
const services_shutdown = () => (
|
|
307
|
+
Promise.all(
|
|
308
|
+
[...services.values()]
|
|
309
|
+
// so they will not be restarted
|
|
310
|
+
.map(service_object => (
|
|
311
|
+
service_object.dependencies = null,
|
|
312
|
+
service_object
|
|
313
|
+
))
|
|
314
|
+
.map(service_stop)
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
/**
|
|
318
|
+
(re)start service
|
|
319
|
+
*/
|
|
320
|
+
const service_start = async service_object => {
|
|
321
|
+
if (!services_loaded_promise) {
|
|
322
|
+
services_loaded_promise = new Promise(resolve => {
|
|
323
|
+
services_loaded_promise_resolve = resolve;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
const {path} = service_object;
|
|
327
|
+
if (log_verbose) log(path + ': prepare for (re)start');
|
|
328
|
+
let start_interval = 0;
|
|
329
|
+
try {
|
|
330
|
+
// if service is running, stop it
|
|
331
|
+
if (
|
|
332
|
+
service_object.status === SERVICE_STATUS_WAITING ||
|
|
333
|
+
service_object.status === SERVICE_STATUS_STARTING
|
|
334
|
+
) {
|
|
335
|
+
if (log_verbose) log(path + ': abort previous start');
|
|
336
|
+
service_object.status = SERVICE_STATUS_PENDING;
|
|
337
|
+
if (service_object.promise_deps_resolve) {
|
|
338
|
+
service_object.promise_deps_resolve();
|
|
252
339
|
}
|
|
253
|
-
|
|
254
|
-
|
|
340
|
+
}
|
|
341
|
+
else if (service_object.status === SERVICE_STATUS_ACTIVE) {
|
|
342
|
+
service_object.status = SERVICE_STATUS_PENDING;
|
|
343
|
+
// restart all depending services
|
|
344
|
+
for (const other of services.values())
|
|
345
|
+
if (
|
|
346
|
+
(
|
|
347
|
+
other.status === SERVICE_STATUS_STARTING ||
|
|
348
|
+
other.status === SERVICE_STATUS_ACTIVE
|
|
349
|
+
) &&
|
|
350
|
+
other.dependencies &&
|
|
351
|
+
other.dependencies.includes(service_object)
|
|
352
|
+
) {
|
|
353
|
+
other.dependencies = null;
|
|
354
|
+
service_start(other);
|
|
255
355
|
}
|
|
256
|
-
|
|
257
|
-
services_loading.add(start_promise);
|
|
258
|
-
await start_promise;
|
|
259
|
-
services_loading.delete(start_promise);
|
|
356
|
+
service_stop_inner(service_object);
|
|
260
357
|
}
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
358
|
+
await service_object.promise_stopped;
|
|
359
|
+
// service is not running, now start it
|
|
360
|
+
if (service_object.status > SERVICE_STATUS_ACTIVE) {
|
|
361
|
+
service_object.status = SERVICE_STATUS_PENDING;
|
|
362
|
+
}
|
|
363
|
+
service_object.promise_stopped = new Promise(resolve => {
|
|
364
|
+
service_object.promise_stopped_resolve = resolve;
|
|
365
|
+
});
|
|
366
|
+
if (!service_object.file_function) {
|
|
367
|
+
const path_real = PATH_PUBLIC + path + '.service.js';
|
|
368
|
+
const file_content = await fsp.readFile(path_real, 'utf8');
|
|
369
|
+
if (service_object.status !== SERVICE_STATUS_PENDING) return;
|
|
370
|
+
if (!service_object.watcher) {
|
|
371
|
+
let timeout = 0;
|
|
372
|
+
service_object.watcher = fs_watch(path_real, WATCH_OPTIONS, () => (
|
|
373
|
+
clearTimeout(timeout),
|
|
374
|
+
timeout = setTimeout(() => (
|
|
375
|
+
log_verbose && log('file updated: ' + path),
|
|
376
|
+
service_object.file_function = null,
|
|
377
|
+
service_start(service_object)
|
|
378
|
+
), 50)
|
|
379
|
+
));
|
|
380
|
+
}
|
|
269
381
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
382
|
+
if (file_content.includes('globals.')) {
|
|
383
|
+
log(`[deprecated] ${path}: uses globals object`);
|
|
384
|
+
}
|
|
273
385
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
386
|
+
service_object.dependencies_paths = [];
|
|
387
|
+
for (let [, dep] of file_content.matchAll(SERVICE_REQUIRE_REG)) {
|
|
388
|
+
const first_char = dep.charCodeAt(0);
|
|
389
|
+
const last_char = dep.charCodeAt(dep.length - 1);
|
|
390
|
+
if (
|
|
391
|
+
dep.length <3 ||
|
|
392
|
+
(first_char !== 34 || last_char !== 34) && // "
|
|
393
|
+
(first_char !== 39 || last_char !== 39) // '
|
|
394
|
+
) {
|
|
395
|
+
throw new Error('service_require() needs inline string');
|
|
396
|
+
}
|
|
397
|
+
if (
|
|
398
|
+
!service_object.dependencies_paths.includes(
|
|
399
|
+
dep = dep.slice(1, -1)
|
|
400
|
+
)
|
|
401
|
+
) {
|
|
402
|
+
service_object.dependencies_paths.push(dep);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
277
405
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
return;
|
|
406
|
+
service_object.file_function = new AsyncFunction(
|
|
407
|
+
'require',
|
|
408
|
+
'custom_import',
|
|
409
|
+
'service_require',
|
|
410
|
+
'service_require_try',
|
|
411
|
+
`const log=a=>rtjscomp.log(${
|
|
412
|
+
JSON.stringify(path + ': ')
|
|
413
|
+
}+a);${
|
|
414
|
+
file_content
|
|
415
|
+
.replace(IMPORT_REG, 'custom_import(')
|
|
416
|
+
}`
|
|
417
|
+
);
|
|
291
418
|
}
|
|
419
|
+
|
|
420
|
+
service_object.dependencies = [];
|
|
421
|
+
let waiting_needed = false;
|
|
422
|
+
for (const dep_path of service_object.dependencies_paths) {
|
|
423
|
+
const dep = services.get(dep_path);
|
|
424
|
+
if (dep == null) {
|
|
425
|
+
throw new Error('unknown required service: ' + dep_path);
|
|
426
|
+
}
|
|
427
|
+
service_object.dependencies.push(dep);
|
|
428
|
+
waiting_needed = (
|
|
429
|
+
waiting_needed ||
|
|
430
|
+
dep.status !== SERVICE_STATUS_ACTIVE
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
if (waiting_needed) {
|
|
434
|
+
if (log_verbose) log(path + ': wait for dependencies');
|
|
435
|
+
service_object.status = SERVICE_STATUS_WAITING;
|
|
436
|
+
service_object.promise_deps = new Promise(resolve => {
|
|
437
|
+
service_object.promise_deps_resolve = resolve;
|
|
438
|
+
});
|
|
439
|
+
await service_object.promise_deps;
|
|
440
|
+
if (service_object.status !== SERVICE_STATUS_WAITING) return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
log('start service: ' + path);
|
|
444
|
+
service_object.status = SERVICE_STATUS_STARTING;
|
|
445
|
+
start_interval = setInterval(() => {
|
|
446
|
+
log(`[warning] ${path}: still starting`);
|
|
447
|
+
}, 5e3);
|
|
448
|
+
const content_object = service_object.content = {};
|
|
449
|
+
const result = await service_object.file_function.call(
|
|
450
|
+
content_object,
|
|
451
|
+
custom_require,
|
|
452
|
+
custom_import,
|
|
453
|
+
service_require,
|
|
454
|
+
service_require_try,
|
|
455
|
+
);
|
|
456
|
+
if (service_object.status !== SERVICE_STATUS_STARTING) return;
|
|
292
457
|
if (typeof result === 'function') {
|
|
293
458
|
service_object.handler_stop = result;
|
|
294
459
|
}
|
|
460
|
+
const handler_start = content_object.start;
|
|
461
|
+
if (handler_start) {
|
|
462
|
+
log(`[deprecated] ${path}: has start method`);
|
|
463
|
+
delete content_object.start;
|
|
464
|
+
await handler_start();
|
|
465
|
+
if (service_object.status !== SERVICE_STATUS_STARTING) return;
|
|
466
|
+
}
|
|
467
|
+
if (content_object.stop) {
|
|
468
|
+
log(`[deprecated] ${path}: has stop method`);
|
|
469
|
+
service_object.handler_stop = content_object.stop;
|
|
470
|
+
delete content_object.stop;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (log_verbose) log('started service: ' + path);
|
|
474
|
+
service_object.status = SERVICE_STATUS_ACTIVE;
|
|
295
475
|
}
|
|
296
476
|
catch (err) {
|
|
477
|
+
if (!err instanceof Error) {
|
|
478
|
+
err = new Error(err + '?! wtf');
|
|
479
|
+
}
|
|
480
|
+
log(`[error] ${
|
|
481
|
+
path
|
|
482
|
+
}${
|
|
483
|
+
linenumber_try(err)
|
|
484
|
+
}: ${
|
|
485
|
+
err.message
|
|
486
|
+
}`);
|
|
487
|
+
service_object.status = SERVICE_STATUS_FAILED;
|
|
488
|
+
service_object.dependencies = null;
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
297
492
|
clearInterval(start_interval);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
493
|
+
service_object.promise_deps =
|
|
494
|
+
service_object.promise_deps_resolve = null;
|
|
495
|
+
if (service_object.status !== SERVICE_STATUS_ACTIVE) {
|
|
496
|
+
service_object.promise_stopped_resolve();
|
|
497
|
+
services_loaded_promise_try();
|
|
498
|
+
}
|
|
301
499
|
}
|
|
302
500
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
catch (err) {
|
|
311
|
-
clearInterval(start_interval);
|
|
312
|
-
log(`[error] ${path} start: ${err.message}`);
|
|
313
|
-
return service_stop(service_object, false);
|
|
501
|
+
// restart waiting services
|
|
502
|
+
other: for (const other of services.values())
|
|
503
|
+
if (other.status === SERVICE_STATUS_WAITING) {
|
|
504
|
+
for (const dep of other.dependencies)
|
|
505
|
+
if (dep.status !== SERVICE_STATUS_ACTIVE) {
|
|
506
|
+
continue other;
|
|
314
507
|
}
|
|
508
|
+
other.promise_deps_resolve();
|
|
509
|
+
}
|
|
510
|
+
services_loaded_promise_try();
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
check if any loading services are left
|
|
514
|
+
*/
|
|
515
|
+
const services_loaded_promise_try = () => {
|
|
516
|
+
if (!services_loaded_promise) return;
|
|
517
|
+
for (const service_object of services.values())
|
|
518
|
+
if (
|
|
519
|
+
service_object.status < SERVICE_STATUS_ACTIVE
|
|
520
|
+
) return;
|
|
521
|
+
services_loaded_promise_resolve();
|
|
522
|
+
services_loaded_promise =
|
|
523
|
+
services_loaded_promise_resolve = null;
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
stop and forget service
|
|
527
|
+
*/
|
|
528
|
+
const service_stop = async service_object => {
|
|
529
|
+
log('stop service: ' + service_object.path);
|
|
530
|
+
service_object.status = SERVICE_STATUS_STOPPING;
|
|
531
|
+
service_object.file_function = null;
|
|
532
|
+
if (service_object.watcher) {
|
|
533
|
+
service_object.watcher.close();
|
|
315
534
|
}
|
|
316
|
-
clearInterval(start_interval);
|
|
317
535
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
536
|
+
for (const other of services.values())
|
|
537
|
+
if (
|
|
538
|
+
other.dependencies &&
|
|
539
|
+
other.dependencies.includes(service_object)
|
|
540
|
+
) {
|
|
541
|
+
other.dependencies = null;
|
|
542
|
+
if (other.status !== SERVICE_STATUS_STOPPING) {
|
|
543
|
+
service_start(other);
|
|
544
|
+
}
|
|
322
545
|
}
|
|
323
546
|
|
|
324
|
-
|
|
325
|
-
|
|
547
|
+
if (service_object.promise_deps_resolve) {
|
|
548
|
+
service_object.promise_deps_resolve();
|
|
549
|
+
}
|
|
550
|
+
else {
|
|
551
|
+
await service_stop_inner(service_object);
|
|
552
|
+
}
|
|
553
|
+
services.delete(service_object.path);
|
|
554
|
+
if (log_verbose) log('stopped service: ' + service_object.path);
|
|
326
555
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
forget && service_object.watcher &&
|
|
336
|
-
service_object.watcher.close(),
|
|
337
|
-
service_stop_handler(service_object)
|
|
338
|
-
)
|
|
339
|
-
const service_stop_handler = async service_object => {
|
|
340
|
-
services_active.delete(service_object.path);
|
|
341
|
-
log('stop service: ' + service_object.path);
|
|
342
|
-
const handler_stop = service_object.handler_stop;
|
|
556
|
+
/**
|
|
557
|
+
stop service so it can be forgot or restarted
|
|
558
|
+
*/
|
|
559
|
+
const service_stop_inner = async service_object => {
|
|
560
|
+
const {
|
|
561
|
+
handler_stop,
|
|
562
|
+
path,
|
|
563
|
+
} = service_object;
|
|
343
564
|
if (handler_stop) {
|
|
565
|
+
service_object.handler_stop = null;
|
|
344
566
|
const stop_interval = setInterval(() => {
|
|
345
|
-
log(`[warning] ${
|
|
567
|
+
log(`[warning] ${path}: still stopping`);
|
|
346
568
|
}, 1e3);
|
|
347
569
|
try {
|
|
348
|
-
service_object.handler_stop = null;
|
|
349
570
|
await handler_stop();
|
|
350
571
|
}
|
|
351
572
|
catch (err) {
|
|
352
|
-
log(`[error] ${
|
|
573
|
+
log(`[error] ${
|
|
574
|
+
path
|
|
575
|
+
}${
|
|
576
|
+
linenumber_try(err)
|
|
577
|
+
} stop handler: ${
|
|
578
|
+
err.message
|
|
579
|
+
}`);
|
|
580
|
+
service_object.status = SERVICE_STATUS_FAILED;
|
|
353
581
|
}
|
|
354
582
|
clearInterval(stop_interval);
|
|
355
583
|
}
|
|
356
|
-
|
|
584
|
+
service_object.promise_stopped_resolve();
|
|
357
585
|
}
|
|
358
|
-
|
|
359
|
-
const
|
|
360
|
-
if (
|
|
586
|
+
const service_require = path => {
|
|
587
|
+
const service_object = services.get(path);
|
|
588
|
+
if (
|
|
589
|
+
service_object != null &&
|
|
590
|
+
service_object.status === SERVICE_STATUS_ACTIVE
|
|
591
|
+
) return service_object.content;
|
|
361
592
|
throw new Error('service required: ' + path);
|
|
362
593
|
}
|
|
363
|
-
|
|
364
|
-
const
|
|
594
|
+
const service_require_try = path => {
|
|
595
|
+
const service_object = services.get(path);
|
|
365
596
|
return (
|
|
366
|
-
|
|
367
|
-
|
|
597
|
+
service_object != null &&
|
|
598
|
+
service_object.status === SERVICE_STATUS_ACTIVE
|
|
599
|
+
? service_object.content
|
|
368
600
|
: null
|
|
369
601
|
);
|
|
370
602
|
}
|
|
@@ -390,7 +622,7 @@ const file_keep_new = async (path, callback) => {
|
|
|
390
622
|
let timeout = 0;
|
|
391
623
|
return fs_watch(path, WATCH_OPTIONS, () => (
|
|
392
624
|
clearTimeout(timeout),
|
|
393
|
-
timeout = setTimeout(() => (
|
|
625
|
+
timeout = setTimeout(() => exiting || (
|
|
394
626
|
log_verbose && log('file updated: ' + path),
|
|
395
627
|
fsp.readFile(path, 'utf8')
|
|
396
628
|
.catch(() => null)
|
|
@@ -399,6 +631,18 @@ const file_keep_new = async (path, callback) => {
|
|
|
399
631
|
));
|
|
400
632
|
}
|
|
401
633
|
|
|
634
|
+
const get_prop_bool = (obj, prop, fallback) => {
|
|
635
|
+
if (
|
|
636
|
+
obj === null ||
|
|
637
|
+
!(prop in obj)
|
|
638
|
+
) return fallback;
|
|
639
|
+
const value = obj[prop];
|
|
640
|
+
if (typeof value !== 'boolean') {
|
|
641
|
+
throw prop + ' must be boolean';
|
|
642
|
+
}
|
|
643
|
+
delete obj[prop];
|
|
644
|
+
return value;
|
|
645
|
+
}
|
|
402
646
|
const get_prop_uint = (obj, prop, fallback) => {
|
|
403
647
|
if (
|
|
404
648
|
obj === null ||
|
|
@@ -467,6 +711,26 @@ const parse_old_map = data => (
|
|
|
467
711
|
.map(entry => entry.split(':'))
|
|
468
712
|
)
|
|
469
713
|
)
|
|
714
|
+
const config_path_check = (path, allow_empty = false) => {
|
|
715
|
+
if (!allow_empty && !path) {
|
|
716
|
+
throw 'path is empty';
|
|
717
|
+
}
|
|
718
|
+
if (path.charCodeAt(0) === 47) {
|
|
719
|
+
throw 'path must not start with /';
|
|
720
|
+
}
|
|
721
|
+
if (path.charCodeAt(path.length - 1) === 47) {
|
|
722
|
+
throw 'path must not end with /';
|
|
723
|
+
}
|
|
724
|
+
if (path.includes('..')) {
|
|
725
|
+
throw 'path must not contain ..';
|
|
726
|
+
}
|
|
727
|
+
if (path.includes('~')) {
|
|
728
|
+
throw 'path must not contain ~';
|
|
729
|
+
}
|
|
730
|
+
if (path.includes('//')) {
|
|
731
|
+
throw 'path must not contain //';
|
|
732
|
+
}
|
|
733
|
+
}
|
|
470
734
|
|
|
471
735
|
let log_history = rtjscomp.log_history = [];
|
|
472
736
|
actions.log_clear = () => {
|
|
@@ -541,6 +805,7 @@ const request_handle = async (request, response, https) => {
|
|
|
541
805
|
if (
|
|
542
806
|
path.includes('php') ||
|
|
543
807
|
path.includes('sql') ||
|
|
808
|
+
path.includes('.git/') ||
|
|
544
809
|
path_ghosts.has(path)
|
|
545
810
|
) return;
|
|
546
811
|
|
|
@@ -598,6 +863,7 @@ const request_handle = async (request, response, https) => {
|
|
|
598
863
|
).toLowerCase();
|
|
599
864
|
|
|
600
865
|
let file_gz_enabled = (
|
|
866
|
+
gz_enabled &&
|
|
601
867
|
'accept-encoding' in request_headers &&
|
|
602
868
|
!type_raws.has(file_type) &&
|
|
603
869
|
request_headers['accept-encoding'].split(HTTP_LIST_REG).includes('gzip')
|
|
@@ -639,7 +905,7 @@ const request_handle = async (request, response, https) => {
|
|
|
639
905
|
file_dyn_enabled
|
|
640
906
|
? 'dynam'
|
|
641
907
|
: 'stat'
|
|
642
|
-
}ic file: ${
|
|
908
|
+
}ic file: ${path}`);
|
|
643
909
|
|
|
644
910
|
if (
|
|
645
911
|
path_hiddens.has(path) ||
|
|
@@ -743,6 +1009,8 @@ const request_handle = async (request, response, https) => {
|
|
|
743
1009
|
'response',
|
|
744
1010
|
'require',
|
|
745
1011
|
'custom_import',
|
|
1012
|
+
'service_require',
|
|
1013
|
+
'service_require_try',
|
|
746
1014
|
code
|
|
747
1015
|
);
|
|
748
1016
|
}
|
|
@@ -881,47 +1149,76 @@ const request_handle = async (request, response, https) => {
|
|
|
881
1149
|
)
|
|
882
1150
|
]);
|
|
883
1151
|
|
|
884
|
-
|
|
885
|
-
await Promise.all(Array.from(services_loading));
|
|
886
|
-
}
|
|
1152
|
+
await services_loaded_promise;
|
|
887
1153
|
|
|
1154
|
+
let returned;
|
|
888
1155
|
try {
|
|
889
|
-
await file_function(
|
|
1156
|
+
returned = await file_function(
|
|
890
1157
|
file_function_input,
|
|
891
1158
|
file_function_output,
|
|
892
1159
|
request,
|
|
893
1160
|
response,
|
|
894
1161
|
custom_require,
|
|
895
|
-
custom_import
|
|
1162
|
+
custom_import,
|
|
1163
|
+
service_require,
|
|
1164
|
+
service_require_try,
|
|
896
1165
|
);
|
|
897
|
-
file_function_output.end();
|
|
898
1166
|
}
|
|
899
1167
|
catch (err) {
|
|
900
1168
|
if (err instanceof Error) {
|
|
901
1169
|
log(`[error] ${path}: ${err.message}`);
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1170
|
+
returned = (
|
|
1171
|
+
err.message.startsWith('service required: ')
|
|
1172
|
+
? 503
|
|
1173
|
+
: 500
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
else if (typeof err === 'number') {
|
|
1177
|
+
log(`[deprecated] ${path}: status code thrown, use return instead`);
|
|
1178
|
+
returned = err;
|
|
1179
|
+
}
|
|
1180
|
+
else {
|
|
1181
|
+
log(`[error] ${path}: invalid throw type: ${typeof err}`);
|
|
1182
|
+
returned = 500;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
if (returned != null) {
|
|
1186
|
+
if (response.headersSent) {
|
|
1187
|
+
if (
|
|
1188
|
+
response.writableEnded != null
|
|
1189
|
+
? !response.writableEnded
|
|
1190
|
+
: !response.finished
|
|
1191
|
+
) {
|
|
1192
|
+
file_function_output.end(`${
|
|
1193
|
+
file_type === 'html'
|
|
1194
|
+
? '<hr>'
|
|
1195
|
+
: '\n\n---\n'
|
|
1196
|
+
}ERROR!`);
|
|
905
1197
|
}
|
|
906
1198
|
}
|
|
907
|
-
if (
|
|
1199
|
+
else if (file_gz_enabled) {
|
|
908
1200
|
response.removeHeader('Content-Encoding');
|
|
909
|
-
throw err;
|
|
910
1201
|
}
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1202
|
+
if (typeof returned !== 'number') {
|
|
1203
|
+
log(`[error] ${path}: invalid return type: ${typeof returned}`);
|
|
1204
|
+
throw 500;
|
|
1205
|
+
}
|
|
1206
|
+
if (response.headersSent) {
|
|
1207
|
+
if (returned < 500) {
|
|
1208
|
+
log(`[error] ${path}: status code after content`);
|
|
1209
|
+
throw 500;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
throw returned;
|
|
917
1213
|
}
|
|
1214
|
+
file_function_output.end();
|
|
918
1215
|
}
|
|
919
1216
|
else { // static file
|
|
920
1217
|
let file_data = null;
|
|
921
1218
|
|
|
922
1219
|
if (
|
|
923
1220
|
file_gz_enabled &&
|
|
924
|
-
file_stat.size >
|
|
1221
|
+
file_stat.size > 80 &&
|
|
925
1222
|
fs.existsSync(path_real + '.gz')
|
|
926
1223
|
) {
|
|
927
1224
|
file_data = fs.createReadStream(path_real + '.gz');
|
|
@@ -958,16 +1255,22 @@ const request_handle = async (request, response, https) => {
|
|
|
958
1255
|
log(`[error] request failed: ${err}; ${request_ip}; ${request.url}`);
|
|
959
1256
|
}
|
|
960
1257
|
|
|
961
|
-
response.
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
1258
|
+
if (!response.headersSent) {
|
|
1259
|
+
response.writeHead(err, {
|
|
1260
|
+
'Content-Type': 'text/html',
|
|
1261
|
+
'Cache-Control': 'no-cache, no-store',
|
|
1262
|
+
});
|
|
1263
|
+
response.end(`<!DOCTYPE html><html><body><h1>HTTP ${
|
|
1264
|
+
err
|
|
1265
|
+
}: ${
|
|
1266
|
+
http.STATUS_CODES[err] || 'Error'
|
|
1267
|
+
}</h1></body></html>`);
|
|
1268
|
+
}
|
|
966
1269
|
}
|
|
967
1270
|
}
|
|
968
1271
|
|
|
969
|
-
let exiting = false;
|
|
970
1272
|
actions.halt = async () => {
|
|
1273
|
+
exiting = true;
|
|
971
1274
|
if (log_verbose) log('stop all');
|
|
972
1275
|
await Promise.all([
|
|
973
1276
|
actions.http_stop && actions.http_stop(),
|
|
@@ -979,7 +1282,6 @@ actions.halt = async () => {
|
|
|
979
1282
|
}
|
|
980
1283
|
actions.exit = async status => {
|
|
981
1284
|
if (exiting) return;
|
|
982
|
-
exiting = true;
|
|
983
1285
|
if (typeof status !== 'number') status = 0;
|
|
984
1286
|
await actions.halt();
|
|
985
1287
|
if (log_verbose) log('exit');
|
|
@@ -1011,6 +1313,7 @@ log(`rtjscomp v${
|
|
|
1011
1313
|
IS_BUN ? 'bun' : 'node'
|
|
1012
1314
|
} on ${
|
|
1013
1315
|
process.platform
|
|
1316
|
+
.replace('win32', 'windows')
|
|
1014
1317
|
}`);
|
|
1015
1318
|
|
|
1016
1319
|
await file_keep_new(PATH_CONFIG + 'init.js', async data => {
|
|
@@ -1160,7 +1463,7 @@ actions.http_kill = async () => {
|
|
|
1160
1463
|
if (http_status_target) return;
|
|
1161
1464
|
log('kill http');
|
|
1162
1465
|
await Promise.all(
|
|
1163
|
-
|
|
1466
|
+
[...http_connections.values()]
|
|
1164
1467
|
.map(connection => connection.destroy())
|
|
1165
1468
|
);
|
|
1166
1469
|
if (log_verbose) log('killed http');
|
|
@@ -1211,7 +1514,8 @@ try {
|
|
|
1211
1514
|
if (https_status_target) return;
|
|
1212
1515
|
log('kill https');
|
|
1213
1516
|
await Promise.all(
|
|
1214
|
-
|
|
1517
|
+
[...https_connections.values()]
|
|
1518
|
+
.map(connection => connection.destroy())
|
|
1215
1519
|
);
|
|
1216
1520
|
if (log_verbose) log('killed https');
|
|
1217
1521
|
https_connections.clear();
|
|
@@ -1229,6 +1533,8 @@ await file_keep_new('rtjscomp.json', data => {
|
|
|
1229
1533
|
throw 'must contain {}';
|
|
1230
1534
|
}
|
|
1231
1535
|
|
|
1536
|
+
const gzip_level_new = get_prop_uint(data, 'gzip_level', 9);
|
|
1537
|
+
const log_verbose_new = get_prop_bool(data, 'log_verbose', log_verbose_flag);
|
|
1232
1538
|
const path_aliases_new = get_prop_map(data, 'path_aliases');
|
|
1233
1539
|
const path_ghosts_new = get_prop_list(data, 'path_ghosts');
|
|
1234
1540
|
const path_hiddens_new = get_prop_list(data, 'path_hiddens');
|
|
@@ -1246,22 +1552,37 @@ await file_keep_new('rtjscomp.json', data => {
|
|
|
1246
1552
|
throw 'unknown: ' + keys_left.join(', ');
|
|
1247
1553
|
}
|
|
1248
1554
|
}
|
|
1555
|
+
if (gzip_level_new > 9) {
|
|
1556
|
+
throw 'gzip_level > 9';
|
|
1557
|
+
}
|
|
1558
|
+
if (
|
|
1559
|
+
port_http_new > 65535 ||
|
|
1560
|
+
port_https_new > 65535
|
|
1561
|
+
) {
|
|
1562
|
+
throw 'port > 65535';
|
|
1563
|
+
}
|
|
1249
1564
|
|
|
1565
|
+
gz_enabled = gzip_level_new > 0;
|
|
1566
|
+
GZIP_OPTIONS.level = gzip_level_new;
|
|
1567
|
+
log_verbose = log_verbose_new;
|
|
1250
1568
|
if (path_ghosts_new) {
|
|
1251
1569
|
path_ghosts.clear();
|
|
1252
1570
|
for (const key of path_ghosts_new) {
|
|
1571
|
+
config_path_check(key);
|
|
1253
1572
|
path_ghosts.add(key);
|
|
1254
1573
|
}
|
|
1255
1574
|
}
|
|
1256
1575
|
if (path_hiddens_new) {
|
|
1257
1576
|
path_hiddens.clear();
|
|
1258
1577
|
for (const key of path_hiddens_new) {
|
|
1578
|
+
config_path_check(key);
|
|
1259
1579
|
path_hiddens.add(key);
|
|
1260
1580
|
}
|
|
1261
1581
|
}
|
|
1262
1582
|
if (path_statics_new) {
|
|
1263
1583
|
path_statics.clear();
|
|
1264
1584
|
for (const key of path_statics_new) {
|
|
1585
|
+
config_path_check(key);
|
|
1265
1586
|
path_statics.add(key);
|
|
1266
1587
|
}
|
|
1267
1588
|
}
|
|
@@ -1270,6 +1591,8 @@ await file_keep_new('rtjscomp.json', data => {
|
|
|
1270
1591
|
path_aliases_reverse.clear();
|
|
1271
1592
|
path_aliases_templates.clear();
|
|
1272
1593
|
for (const [key, value] of path_aliases_new) {
|
|
1594
|
+
config_path_check(key, true);
|
|
1595
|
+
config_path_check(value);
|
|
1273
1596
|
if (key.includes('*')) {
|
|
1274
1597
|
const path_split = key.split('/');
|
|
1275
1598
|
const first = path_split.shift();
|
|
@@ -1314,8 +1637,12 @@ await file_keep_new('rtjscomp.json', data => {
|
|
|
1314
1637
|
}
|
|
1315
1638
|
}
|
|
1316
1639
|
|
|
1640
|
+
for (const path of services_new) config_path_check(path);
|
|
1317
1641
|
const promises = [
|
|
1318
|
-
services_list_react(
|
|
1642
|
+
services_list_react(
|
|
1643
|
+
services_new
|
|
1644
|
+
.filter(path => path.charCodeAt(0) !== 35)
|
|
1645
|
+
),
|
|
1319
1646
|
];
|
|
1320
1647
|
|
|
1321
1648
|
if (port_http_new !== port_http) {
|