@lvce-editor/server 0.108.12 → 0.108.13
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/ThirdPartyNotices.txt +2 -2
- package/package.json +3 -3
- package/src/server.js +43 -29
- package/test/httpResponse.integration.test.js +100 -0
package/ThirdPartyNotices.txt
CHANGED
|
@@ -2,7 +2,7 @@ This project incorporates components from the projects listed below, that may ha
|
|
|
2
2
|
differing from this project:
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
1) License Notice for static/
|
|
5
|
+
1) License Notice for static/241d72e/icons (from https://github.com/microsoft/vscode-codicons)
|
|
6
6
|
---------------------------------------
|
|
7
7
|
|
|
8
8
|
Attribution 4.0 International
|
|
@@ -402,7 +402,7 @@ public licenses.
|
|
|
402
402
|
Creative Commons may be contacted at creativecommons.org.
|
|
403
403
|
|
|
404
404
|
|
|
405
|
-
2) License Notice for static/
|
|
405
|
+
2) License Notice for static/241d72e/fonts/FiraCode-VariableFont.ttf (from https://github.com/tonsky/FiraCode)
|
|
406
406
|
---------------------------------------
|
|
407
407
|
|
|
408
408
|
Copyright (c) 2014, The Fira Code Project Authors (https://github.com/tonsky/FiraCode)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lvce-editor/server",
|
|
3
|
-
"version": "0.108.
|
|
3
|
+
"version": "0.108.13",
|
|
4
4
|
"description": "Run LVCE Editor as a server.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": "bin/server.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"@lvce-editor/jsonc-parser": "^1.5.0",
|
|
24
|
-
"@lvce-editor/shared-process": "0.108.
|
|
25
|
-
"@lvce-editor/static-server": "0.108.
|
|
24
|
+
"@lvce-editor/shared-process": "0.108.13",
|
|
25
|
+
"@lvce-editor/static-server": "0.108.13"
|
|
26
26
|
}
|
|
27
27
|
}
|
package/src/server.js
CHANGED
|
@@ -63,6 +63,7 @@ const ErrorCodes = {
|
|
|
63
63
|
ERR_STREAM_PREMATURE_CLOSE: 'ERR_STREAM_PREMATURE_CLOSE',
|
|
64
64
|
EISDIR: 'EISDIR',
|
|
65
65
|
ECONNRESET: 'ECONNRESET',
|
|
66
|
+
EPIPE: 'EPIPE',
|
|
66
67
|
ENOENT: 'ENOENT',
|
|
67
68
|
EADDRINUSE: 'EADDRINUSE',
|
|
68
69
|
}
|
|
@@ -76,7 +77,7 @@ const isStatic = (url) => {
|
|
|
76
77
|
if (url === '/index.html' || url.startsWith('/index.html?')) {
|
|
77
78
|
return !hasLinkedExtensions
|
|
78
79
|
}
|
|
79
|
-
if (url.startsWith('/
|
|
80
|
+
if (url.startsWith('/241d72e')) {
|
|
80
81
|
return true
|
|
81
82
|
}
|
|
82
83
|
if (url.startsWith('/favicon.ico')) {
|
|
@@ -98,9 +99,9 @@ const handleRequest = (req, res) => {
|
|
|
98
99
|
return
|
|
99
100
|
}
|
|
100
101
|
if (isStatic(req.url)) {
|
|
101
|
-
return
|
|
102
|
+
return handleResponseViaProcess(req, res, getOrCreateStaticServerPathProcess, 'StaticServer.getResponse')
|
|
102
103
|
}
|
|
103
|
-
return
|
|
104
|
+
return handleResponseViaProcess(req, res, getOrCreateSharedProcess, 'HandleRequest.handleRequest')
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
const state = {
|
|
@@ -189,6 +190,7 @@ const launchProcess = async (processPath, execArgv) => {
|
|
|
189
190
|
...process.env,
|
|
190
191
|
},
|
|
191
192
|
execArgv: [],
|
|
193
|
+
serialization: 'advanced',
|
|
192
194
|
})
|
|
193
195
|
childProcess.on('exit', handleExit)
|
|
194
196
|
childProcess.on('disconnect', handleSharedProcessDisconnect)
|
|
@@ -204,7 +206,9 @@ const launchProcess = async (processPath, execArgv) => {
|
|
|
204
206
|
const launchSharedProcess = async () => {
|
|
205
207
|
const sharedProcessUrl = new URL('src/sharedProcessMain.js', import.meta.resolve('@lvce-editor/shared-process')).toString()
|
|
206
208
|
const sharedProcessPath = fileURLToPath(sharedProcessUrl)
|
|
207
|
-
|
|
209
|
+
const ipc = await launchProcess(sharedProcessPath, ['--enable-source-maps', '--ipc-type=node-forked-process', ...argvSliced])
|
|
210
|
+
ipc.on('message', handleMessage)
|
|
211
|
+
return ipc
|
|
208
212
|
}
|
|
209
213
|
|
|
210
214
|
/**
|
|
@@ -256,25 +260,21 @@ const getHandleMessage = (request) => {
|
|
|
256
260
|
}
|
|
257
261
|
}
|
|
258
262
|
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
// ignore
|
|
262
|
-
return
|
|
263
|
-
}
|
|
264
|
-
console.info('[info]: request upgrade error', error)
|
|
263
|
+
const isExpectedConnectionError = (error) => {
|
|
264
|
+
return error && (error.code === ErrorCodes.ECONNRESET || error.code === ErrorCodes.EPIPE)
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
267
|
+
const handleRequestError = (error) => {
|
|
268
|
+
if (!isExpectedConnectionError(error)) {
|
|
269
|
+
console.info('[info]: request error', error)
|
|
270
|
+
}
|
|
270
271
|
}
|
|
271
272
|
|
|
272
273
|
const handleSocketError = (error) => {
|
|
273
|
-
if (error
|
|
274
|
-
|
|
274
|
+
if (!isExpectedConnectionError(error)) {
|
|
275
|
+
// @ts-ignore
|
|
276
|
+
console.info('[info] request socket error', error)
|
|
275
277
|
}
|
|
276
|
-
// @ts-ignore
|
|
277
|
-
console.info('[info] request socket error', error)
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
const sendHandleSharedProcess = async (request, socket, method, ...params) => {
|
|
@@ -282,7 +282,7 @@ const sendHandleSharedProcess = async (request, socket, method, ...params) => {
|
|
|
282
282
|
if (!socket) {
|
|
283
283
|
return
|
|
284
284
|
}
|
|
285
|
-
socket.on('error',
|
|
285
|
+
socket.on('error', handleSocketError)
|
|
286
286
|
const sharedProcess = await getOrCreateSharedProcess()
|
|
287
287
|
sharedProcess.send(
|
|
288
288
|
{
|
|
@@ -331,27 +331,29 @@ const handleMessage = (message) => {
|
|
|
331
331
|
|
|
332
332
|
const hasErrorListener = new WeakSet()
|
|
333
333
|
|
|
334
|
-
const
|
|
335
|
-
request.on('error', handleRequestError)
|
|
336
|
-
if (res.socket && !hasErrorListener.has(res.socket)) {
|
|
337
|
-
res.socket.on('error', handleSocketError)
|
|
338
|
-
hasErrorListener.add(res.socket)
|
|
339
|
-
}
|
|
340
|
-
const staticServerProcess = await getOrCreateStaticServerPathProcess()
|
|
334
|
+
const invoke = async (ipc, method, ...params) => {
|
|
341
335
|
const { id, promise } = registerCallback()
|
|
342
|
-
|
|
343
|
-
staticServerProcess.send({
|
|
336
|
+
ipc.send({
|
|
344
337
|
jsonrpc: '2.0',
|
|
345
338
|
id,
|
|
346
339
|
method,
|
|
347
|
-
params
|
|
340
|
+
params,
|
|
348
341
|
})
|
|
349
342
|
const response = await promise
|
|
350
|
-
|
|
343
|
+
if (response.error) {
|
|
344
|
+
throw new Error(response.error.message)
|
|
345
|
+
}
|
|
346
|
+
return response.result
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const sendResponse = (res, result) => {
|
|
351
350
|
const { status, headers, body, hasBody } = result
|
|
352
351
|
if (!status) {
|
|
353
352
|
throw new Error('invalid status')
|
|
354
353
|
}
|
|
354
|
+
if (res.destroyed) {
|
|
355
|
+
return
|
|
356
|
+
}
|
|
355
357
|
res.statusCode = status
|
|
356
358
|
setHeaders(res, headers)
|
|
357
359
|
if (hasBody) {
|
|
@@ -361,6 +363,18 @@ const handleResponseViaStaticServer = async (request, res, method, ...params) =>
|
|
|
361
363
|
}
|
|
362
364
|
}
|
|
363
365
|
|
|
366
|
+
const handleResponseViaProcess = async (request, res, getProcess, method, ...params) => {
|
|
367
|
+
request.on('error', handleRequestError)
|
|
368
|
+
res.on('error', handleSocketError)
|
|
369
|
+
if (res.socket && !hasErrorListener.has(res.socket)) {
|
|
370
|
+
res.socket.on('error', handleSocketError)
|
|
371
|
+
hasErrorListener.add(res.socket)
|
|
372
|
+
}
|
|
373
|
+
const ipc = await getProcess()
|
|
374
|
+
const result = await invoke(ipc, method, getHandleMessage(request), ...params)
|
|
375
|
+
sendResponse(res, result)
|
|
376
|
+
}
|
|
377
|
+
|
|
364
378
|
/**
|
|
365
379
|
*
|
|
366
380
|
* @param {import('http').IncomingMessage} request
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { once } from 'node:events'
|
|
4
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import path from 'node:path'
|
|
7
|
+
import test from 'node:test'
|
|
8
|
+
|
|
9
|
+
const serverPath = path.join(import.meta.dirname, '..', 'src', 'server.js')
|
|
10
|
+
|
|
11
|
+
const waitForListeningPort = (child) => {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
let output = ''
|
|
14
|
+
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for server\n${output}`)), 30_000)
|
|
15
|
+
const onData = (chunk) => {
|
|
16
|
+
output += chunk.toString('utf8')
|
|
17
|
+
const match = output.match(/listening on http:\/\/localhost:(\d+)/)
|
|
18
|
+
if (match) {
|
|
19
|
+
clearTimeout(timeout)
|
|
20
|
+
child.stdout.off('data', onData)
|
|
21
|
+
resolve(Number.parseInt(match[1], 10))
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
child.stdout.on('data', onData)
|
|
25
|
+
child.stderr.on('data', (chunk) => {
|
|
26
|
+
output += chunk.toString('utf8')
|
|
27
|
+
})
|
|
28
|
+
child.once('error', reject)
|
|
29
|
+
child.once('exit', (code) => reject(new Error(`Server exited with code ${code}\n${output}`)))
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const killProcessGroup = (child, signal) => {
|
|
34
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
process.kill(process.platform === 'win32' ? child.pid : -child.pid, signal)
|
|
39
|
+
} catch {
|
|
40
|
+
child.kill(signal)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const stopProcessGroup = async (child) => {
|
|
45
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
const exited = once(child, 'exit')
|
|
49
|
+
killProcessGroup(child, 'SIGTERM')
|
|
50
|
+
let timeout
|
|
51
|
+
const timedOut = new Promise((resolve) => {
|
|
52
|
+
timeout = setTimeout(resolve, 5_000, 'timeout')
|
|
53
|
+
})
|
|
54
|
+
const result = await Promise.race([exited.then(() => 'exited'), timedOut])
|
|
55
|
+
clearTimeout(timeout)
|
|
56
|
+
if (result === 'timeout') {
|
|
57
|
+
killProcessGroup(child, 'SIGKILL')
|
|
58
|
+
await exited
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
test('serves binary and partial responses returned by the shared process', async (context) => {
|
|
63
|
+
const directory = await mkdtemp(path.join(tmpdir(), 'lvce-http-response-'))
|
|
64
|
+
const file = path.join(directory, 'video.bin')
|
|
65
|
+
const content = Buffer.from([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
|
|
66
|
+
await writeFile(file, content)
|
|
67
|
+
|
|
68
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
69
|
+
detached: true,
|
|
70
|
+
env: {
|
|
71
|
+
...process.env,
|
|
72
|
+
PORT: '0',
|
|
73
|
+
},
|
|
74
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
75
|
+
})
|
|
76
|
+
context.after(async () => {
|
|
77
|
+
await stopProcessGroup(child)
|
|
78
|
+
await rm(directory, { force: true, recursive: true })
|
|
79
|
+
})
|
|
80
|
+
const port = await waitForListeningPort(child)
|
|
81
|
+
const url = `http://localhost:${port}/remote${encodeURI(file)}`
|
|
82
|
+
|
|
83
|
+
const response = await fetch(url)
|
|
84
|
+
assert.equal(response.status, 200)
|
|
85
|
+
assert.deepEqual(Buffer.from(await response.arrayBuffer()), content)
|
|
86
|
+
|
|
87
|
+
const partialResponse = await fetch(url, {
|
|
88
|
+
headers: {
|
|
89
|
+
Range: 'bytes=2-5',
|
|
90
|
+
},
|
|
91
|
+
})
|
|
92
|
+
assert.equal(partialResponse.status, 206)
|
|
93
|
+
assert.equal(partialResponse.headers.get('accept-ranges'), 'bytes')
|
|
94
|
+
assert.equal(partialResponse.headers.get('content-range'), 'bytes 2-5/10')
|
|
95
|
+
assert.deepEqual(Buffer.from(await partialResponse.arrayBuffer()), Buffer.from([2, 3, 4, 5]))
|
|
96
|
+
|
|
97
|
+
const headResponse = await fetch(url, { method: 'HEAD' })
|
|
98
|
+
assert.equal(headResponse.status, 200)
|
|
99
|
+
assert.equal((await headResponse.arrayBuffer()).byteLength, 0)
|
|
100
|
+
})
|