rtjscomp 0.9.0 → 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 +483 -146
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 = () => {
|
|
@@ -528,8 +792,12 @@ const request_handle = async (request, response, https) => {
|
|
|
528
792
|
path.includes('//')
|
|
529
793
|
) throw 404;
|
|
530
794
|
if (path.length > 1 && path.endsWith('/')) {
|
|
531
|
-
|
|
532
|
-
|
|
795
|
+
path = path.slice(0, -1);
|
|
796
|
+
// is file with extension?
|
|
797
|
+
if (path.lastIndexOf('/') < path.lastIndexOf('.')) {
|
|
798
|
+
response.setHeader('Location', path);
|
|
799
|
+
throw 301;
|
|
800
|
+
}
|
|
533
801
|
}
|
|
534
802
|
path = path.slice(1);
|
|
535
803
|
|
|
@@ -537,6 +805,7 @@ const request_handle = async (request, response, https) => {
|
|
|
537
805
|
if (
|
|
538
806
|
path.includes('php') ||
|
|
539
807
|
path.includes('sql') ||
|
|
808
|
+
path.includes('.git/') ||
|
|
540
809
|
path_ghosts.has(path)
|
|
541
810
|
) return;
|
|
542
811
|
|
|
@@ -594,6 +863,7 @@ const request_handle = async (request, response, https) => {
|
|
|
594
863
|
).toLowerCase();
|
|
595
864
|
|
|
596
865
|
let file_gz_enabled = (
|
|
866
|
+
gz_enabled &&
|
|
597
867
|
'accept-encoding' in request_headers &&
|
|
598
868
|
!type_raws.has(file_type) &&
|
|
599
869
|
request_headers['accept-encoding'].split(HTTP_LIST_REG).includes('gzip')
|
|
@@ -635,7 +905,7 @@ const request_handle = async (request, response, https) => {
|
|
|
635
905
|
file_dyn_enabled
|
|
636
906
|
? 'dynam'
|
|
637
907
|
: 'stat'
|
|
638
|
-
}ic file: ${
|
|
908
|
+
}ic file: ${path}`);
|
|
639
909
|
|
|
640
910
|
if (
|
|
641
911
|
path_hiddens.has(path) ||
|
|
@@ -739,6 +1009,8 @@ const request_handle = async (request, response, https) => {
|
|
|
739
1009
|
'response',
|
|
740
1010
|
'require',
|
|
741
1011
|
'custom_import',
|
|
1012
|
+
'service_require',
|
|
1013
|
+
'service_require_try',
|
|
742
1014
|
code
|
|
743
1015
|
);
|
|
744
1016
|
}
|
|
@@ -877,47 +1149,76 @@ const request_handle = async (request, response, https) => {
|
|
|
877
1149
|
)
|
|
878
1150
|
]);
|
|
879
1151
|
|
|
880
|
-
|
|
881
|
-
await Promise.all(Array.from(services_loading));
|
|
882
|
-
}
|
|
1152
|
+
await services_loaded_promise;
|
|
883
1153
|
|
|
1154
|
+
let returned;
|
|
884
1155
|
try {
|
|
885
|
-
await file_function(
|
|
1156
|
+
returned = await file_function(
|
|
886
1157
|
file_function_input,
|
|
887
1158
|
file_function_output,
|
|
888
1159
|
request,
|
|
889
1160
|
response,
|
|
890
1161
|
custom_require,
|
|
891
|
-
custom_import
|
|
1162
|
+
custom_import,
|
|
1163
|
+
service_require,
|
|
1164
|
+
service_require_try,
|
|
892
1165
|
);
|
|
893
|
-
file_function_output.end();
|
|
894
1166
|
}
|
|
895
1167
|
catch (err) {
|
|
896
1168
|
if (err instanceof Error) {
|
|
897
1169
|
log(`[error] ${path}: ${err.message}`);
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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!`);
|
|
901
1197
|
}
|
|
902
1198
|
}
|
|
903
|
-
if (
|
|
1199
|
+
else if (file_gz_enabled) {
|
|
904
1200
|
response.removeHeader('Content-Encoding');
|
|
905
|
-
throw err;
|
|
906
1201
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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;
|
|
913
1213
|
}
|
|
1214
|
+
file_function_output.end();
|
|
914
1215
|
}
|
|
915
1216
|
else { // static file
|
|
916
1217
|
let file_data = null;
|
|
917
1218
|
|
|
918
1219
|
if (
|
|
919
1220
|
file_gz_enabled &&
|
|
920
|
-
file_stat.size >
|
|
1221
|
+
file_stat.size > 80 &&
|
|
921
1222
|
fs.existsSync(path_real + '.gz')
|
|
922
1223
|
) {
|
|
923
1224
|
file_data = fs.createReadStream(path_real + '.gz');
|
|
@@ -954,16 +1255,22 @@ const request_handle = async (request, response, https) => {
|
|
|
954
1255
|
log(`[error] request failed: ${err}; ${request_ip}; ${request.url}`);
|
|
955
1256
|
}
|
|
956
1257
|
|
|
957
|
-
response.
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
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
|
+
}
|
|
962
1269
|
}
|
|
963
1270
|
}
|
|
964
1271
|
|
|
965
|
-
let exiting = false;
|
|
966
1272
|
actions.halt = async () => {
|
|
1273
|
+
exiting = true;
|
|
967
1274
|
if (log_verbose) log('stop all');
|
|
968
1275
|
await Promise.all([
|
|
969
1276
|
actions.http_stop && actions.http_stop(),
|
|
@@ -975,7 +1282,6 @@ actions.halt = async () => {
|
|
|
975
1282
|
}
|
|
976
1283
|
actions.exit = async status => {
|
|
977
1284
|
if (exiting) return;
|
|
978
|
-
exiting = true;
|
|
979
1285
|
if (typeof status !== 'number') status = 0;
|
|
980
1286
|
await actions.halt();
|
|
981
1287
|
if (log_verbose) log('exit');
|
|
@@ -1007,6 +1313,7 @@ log(`rtjscomp v${
|
|
|
1007
1313
|
IS_BUN ? 'bun' : 'node'
|
|
1008
1314
|
} on ${
|
|
1009
1315
|
process.platform
|
|
1316
|
+
.replace('win32', 'windows')
|
|
1010
1317
|
}`);
|
|
1011
1318
|
|
|
1012
1319
|
await file_keep_new(PATH_CONFIG + 'init.js', async data => {
|
|
@@ -1107,7 +1414,7 @@ await file_keep_new(PATH_CONFIG + 'init.js', async data => {
|
|
|
1107
1414
|
|
|
1108
1415
|
await fsp.writeFile(
|
|
1109
1416
|
'rtjscomp.json',
|
|
1110
|
-
JSON.stringify(json, null,
|
|
1417
|
+
JSON.stringify(json, null, '\t') + '\n',
|
|
1111
1418
|
'utf8'
|
|
1112
1419
|
);
|
|
1113
1420
|
log('[deprecated] config files found, rtjscomp.json written, please delete config files');
|
|
@@ -1156,7 +1463,7 @@ actions.http_kill = async () => {
|
|
|
1156
1463
|
if (http_status_target) return;
|
|
1157
1464
|
log('kill http');
|
|
1158
1465
|
await Promise.all(
|
|
1159
|
-
|
|
1466
|
+
[...http_connections.values()]
|
|
1160
1467
|
.map(connection => connection.destroy())
|
|
1161
1468
|
);
|
|
1162
1469
|
if (log_verbose) log('killed http');
|
|
@@ -1207,7 +1514,8 @@ try {
|
|
|
1207
1514
|
if (https_status_target) return;
|
|
1208
1515
|
log('kill https');
|
|
1209
1516
|
await Promise.all(
|
|
1210
|
-
|
|
1517
|
+
[...https_connections.values()]
|
|
1518
|
+
.map(connection => connection.destroy())
|
|
1211
1519
|
);
|
|
1212
1520
|
if (log_verbose) log('killed https');
|
|
1213
1521
|
https_connections.clear();
|
|
@@ -1225,6 +1533,8 @@ await file_keep_new('rtjscomp.json', data => {
|
|
|
1225
1533
|
throw 'must contain {}';
|
|
1226
1534
|
}
|
|
1227
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);
|
|
1228
1538
|
const path_aliases_new = get_prop_map(data, 'path_aliases');
|
|
1229
1539
|
const path_ghosts_new = get_prop_list(data, 'path_ghosts');
|
|
1230
1540
|
const path_hiddens_new = get_prop_list(data, 'path_hiddens');
|
|
@@ -1242,22 +1552,37 @@ await file_keep_new('rtjscomp.json', data => {
|
|
|
1242
1552
|
throw 'unknown: ' + keys_left.join(', ');
|
|
1243
1553
|
}
|
|
1244
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
|
+
}
|
|
1245
1564
|
|
|
1565
|
+
gz_enabled = gzip_level_new > 0;
|
|
1566
|
+
GZIP_OPTIONS.level = gzip_level_new;
|
|
1567
|
+
log_verbose = log_verbose_new;
|
|
1246
1568
|
if (path_ghosts_new) {
|
|
1247
1569
|
path_ghosts.clear();
|
|
1248
1570
|
for (const key of path_ghosts_new) {
|
|
1571
|
+
config_path_check(key);
|
|
1249
1572
|
path_ghosts.add(key);
|
|
1250
1573
|
}
|
|
1251
1574
|
}
|
|
1252
1575
|
if (path_hiddens_new) {
|
|
1253
1576
|
path_hiddens.clear();
|
|
1254
1577
|
for (const key of path_hiddens_new) {
|
|
1578
|
+
config_path_check(key);
|
|
1255
1579
|
path_hiddens.add(key);
|
|
1256
1580
|
}
|
|
1257
1581
|
}
|
|
1258
1582
|
if (path_statics_new) {
|
|
1259
1583
|
path_statics.clear();
|
|
1260
1584
|
for (const key of path_statics_new) {
|
|
1585
|
+
config_path_check(key);
|
|
1261
1586
|
path_statics.add(key);
|
|
1262
1587
|
}
|
|
1263
1588
|
}
|
|
@@ -1266,6 +1591,8 @@ await file_keep_new('rtjscomp.json', data => {
|
|
|
1266
1591
|
path_aliases_reverse.clear();
|
|
1267
1592
|
path_aliases_templates.clear();
|
|
1268
1593
|
for (const [key, value] of path_aliases_new) {
|
|
1594
|
+
config_path_check(key, true);
|
|
1595
|
+
config_path_check(value);
|
|
1269
1596
|
if (key.includes('*')) {
|
|
1270
1597
|
const path_split = key.split('/');
|
|
1271
1598
|
const first = path_split.shift();
|
|
@@ -1282,7 +1609,13 @@ await file_keep_new('rtjscomp.json', data => {
|
|
|
1282
1609
|
}
|
|
1283
1610
|
else {
|
|
1284
1611
|
path_aliases.set(key, value);
|
|
1285
|
-
path_aliases_reverse.
|
|
1612
|
+
const existing = path_aliases_reverse.get(value);
|
|
1613
|
+
if (
|
|
1614
|
+
existing == null ||
|
|
1615
|
+
existing.length - 1 > value.length
|
|
1616
|
+
) {
|
|
1617
|
+
path_aliases_reverse.set(value, '/' + key);
|
|
1618
|
+
}
|
|
1286
1619
|
}
|
|
1287
1620
|
}
|
|
1288
1621
|
}
|
|
@@ -1304,8 +1637,12 @@ await file_keep_new('rtjscomp.json', data => {
|
|
|
1304
1637
|
}
|
|
1305
1638
|
}
|
|
1306
1639
|
|
|
1640
|
+
for (const path of services_new) config_path_check(path);
|
|
1307
1641
|
const promises = [
|
|
1308
|
-
services_list_react(
|
|
1642
|
+
services_list_react(
|
|
1643
|
+
services_new
|
|
1644
|
+
.filter(path => path.charCodeAt(0) !== 35)
|
|
1645
|
+
),
|
|
1309
1646
|
];
|
|
1310
1647
|
|
|
1311
1648
|
if (port_http_new !== port_http) {
|