@platformatic/basic 2.0.0-alpha.10
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/LICENSE +201 -0
- package/NOTICE +13 -0
- package/README.md +13 -0
- package/config.d.ts +11 -0
- package/eslint.config.js +5 -0
- package/index.js +140 -0
- package/lib/base.js +250 -0
- package/lib/errors.js +21 -0
- package/lib/schema.js +80 -0
- package/lib/utils.js +73 -0
- package/lib/worker/child-manager.js +219 -0
- package/lib/worker/child-process.js +232 -0
- package/lib/worker/child-transport.js +54 -0
- package/lib/worker/listeners.js +51 -0
- package/package.json +51 -0
- package/schema.json +12 -0
package/lib/schema.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { schemaComponents as utilsSchemaComponents } from '@platformatic/utils'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
|
|
4
|
+
export const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf-8'))
|
|
5
|
+
|
|
6
|
+
const application = {
|
|
7
|
+
type: 'object',
|
|
8
|
+
properties: {
|
|
9
|
+
basePath: {
|
|
10
|
+
type: 'string'
|
|
11
|
+
},
|
|
12
|
+
outputDirectory: {
|
|
13
|
+
type: 'string',
|
|
14
|
+
default: 'dist'
|
|
15
|
+
},
|
|
16
|
+
include: {
|
|
17
|
+
type: 'array',
|
|
18
|
+
items: {
|
|
19
|
+
type: 'string'
|
|
20
|
+
},
|
|
21
|
+
default: ['dist']
|
|
22
|
+
},
|
|
23
|
+
commands: {
|
|
24
|
+
type: 'object',
|
|
25
|
+
properties: {
|
|
26
|
+
install: {
|
|
27
|
+
type: 'string',
|
|
28
|
+
default: 'npm ci --omit-dev'
|
|
29
|
+
},
|
|
30
|
+
// All the following options purposely don't have a default so
|
|
31
|
+
// that stackables can detect if the user explicitly set them.
|
|
32
|
+
build: {
|
|
33
|
+
type: 'string'
|
|
34
|
+
},
|
|
35
|
+
development: {
|
|
36
|
+
type: 'string'
|
|
37
|
+
},
|
|
38
|
+
production: {
|
|
39
|
+
type: 'string'
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
default: {},
|
|
43
|
+
additionalProperties: false
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
default: {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const watch = {
|
|
51
|
+
anyOf: [
|
|
52
|
+
utilsSchemaComponents.watch,
|
|
53
|
+
{
|
|
54
|
+
type: 'boolean'
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: 'string'
|
|
58
|
+
}
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const schemaComponents = { application, watch }
|
|
63
|
+
|
|
64
|
+
export const schema = {
|
|
65
|
+
$id: `https://schemas.platformatic.dev/@platformatic/basic/${packageJson.version}.json`,
|
|
66
|
+
$schema: 'http://json-schema.org/draft-07/schema#',
|
|
67
|
+
title: 'Platformatic Stackable',
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
$schema: {
|
|
71
|
+
type: 'string'
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
additionalProperties: true
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* c8 ignore next 3 */
|
|
78
|
+
if (process.argv[1] === import.meta.filename) {
|
|
79
|
+
console.log(JSON.stringify(schema, null, 2))
|
|
80
|
+
}
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { pathToFileURL } from 'node:url'
|
|
3
|
+
import { request } from 'undici'
|
|
4
|
+
|
|
5
|
+
export function getServerUrl (server) {
|
|
6
|
+
const { family, address, port } = server.address()
|
|
7
|
+
|
|
8
|
+
return new URL(family === 'IPv6' ? `http://[${address}]:${port}` : `http://${address}:${port}`).origin
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function injectViaRequest (baseUrl, injectParams, onInject) {
|
|
12
|
+
const url = new URL(injectParams.url, baseUrl).href
|
|
13
|
+
const requestParams = { method: injectParams.method, headers: injectParams.headers }
|
|
14
|
+
|
|
15
|
+
if (injectParams.body) {
|
|
16
|
+
const body = injectParams.body
|
|
17
|
+
requestParams.body = typeof body === 'object' ? JSON.stringify(body) : body
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const { statusCode, headers, body } = await request(url, requestParams)
|
|
22
|
+
|
|
23
|
+
const rawPayload = Buffer.from(await body.arrayBuffer())
|
|
24
|
+
const payload = rawPayload.toString()
|
|
25
|
+
const response = { statusCode, headers, body: payload, payload, rawPayload }
|
|
26
|
+
|
|
27
|
+
if (onInject) {
|
|
28
|
+
return onInject(null, response)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return response
|
|
32
|
+
} catch (error) {
|
|
33
|
+
if (onInject) {
|
|
34
|
+
onInject(error)
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
throw error
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function ensureFileUrl (pathOrUrl) {
|
|
43
|
+
if (!pathOrUrl) {
|
|
44
|
+
return pathOrUrl
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
pathOrUrl = pathOrUrl.toString()
|
|
48
|
+
|
|
49
|
+
if (pathOrUrl.startsWith('file://')) {
|
|
50
|
+
return pathOrUrl
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return pathToFileURL(pathOrUrl)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// This is to avoid common path/URL problems on Windows
|
|
57
|
+
export function importFile (path) {
|
|
58
|
+
return import(ensureFileUrl(path))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolvePackage (root, pkg) {
|
|
62
|
+
const require = createRequire(root)
|
|
63
|
+
|
|
64
|
+
return require.resolve(pkg, { paths: [root, ...require.main.paths] })
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function cleanBasePath (basePath) {
|
|
68
|
+
return basePath ? `/${basePath}`.replaceAll(/\/+/g, '/').replace(/\/$/, '') : '/'
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function ensureTrailingSlash (basePath) {
|
|
72
|
+
return basePath ? `${basePath}${basePath.endsWith('/') ? '' : '/'}` : '/'
|
|
73
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { ITC, generateNotification } from '@platformatic/itc'
|
|
2
|
+
import { createDirectory, errors } from '@platformatic/utils'
|
|
3
|
+
import { once } from 'node:events'
|
|
4
|
+
import { rm, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { createServer } from 'node:http'
|
|
6
|
+
import { register } from 'node:module'
|
|
7
|
+
import { platform, tmpdir } from 'node:os'
|
|
8
|
+
import { dirname, resolve } from 'node:path'
|
|
9
|
+
import { workerData } from 'node:worker_threads'
|
|
10
|
+
import { request } from 'undici'
|
|
11
|
+
import { WebSocketServer } from 'ws'
|
|
12
|
+
import { exitCodes } from '../errors.js'
|
|
13
|
+
import { ensureFileUrl } from '../utils.js'
|
|
14
|
+
|
|
15
|
+
export const isWindows = platform() === 'win32'
|
|
16
|
+
|
|
17
|
+
// In theory we could use the context.id to namespace even more, but due to
|
|
18
|
+
// UNIX socket length limitation on MacOS, we don't.
|
|
19
|
+
function generateChildrenId (context) {
|
|
20
|
+
return [process.pid, Date.now()].join('-')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getSocketPath (id) {
|
|
24
|
+
let socketPath = null
|
|
25
|
+
if (platform() === 'win32') {
|
|
26
|
+
socketPath = `\\\\.\\pipe\\plt-${id}`
|
|
27
|
+
} else {
|
|
28
|
+
// As stated in https://nodejs.org/dist/latest-v20.x/docs/api/net.html#identifying-paths-for-ipc-connections,
|
|
29
|
+
// Node will take care of deleting the file for us
|
|
30
|
+
socketPath = resolve(tmpdir(), 'platformatic', 'runtimes', `${id}.socket`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return socketPath
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class ChildManager extends ITC {
|
|
37
|
+
#id
|
|
38
|
+
#loader
|
|
39
|
+
#context
|
|
40
|
+
#scripts
|
|
41
|
+
#logger
|
|
42
|
+
#server
|
|
43
|
+
#socketPath
|
|
44
|
+
#clients
|
|
45
|
+
#requests
|
|
46
|
+
#currentClient
|
|
47
|
+
#listener
|
|
48
|
+
#originalNodeOptions
|
|
49
|
+
#dataPath
|
|
50
|
+
|
|
51
|
+
constructor (opts) {
|
|
52
|
+
let { loader, context, scripts, handlers, ...itcOpts } = opts
|
|
53
|
+
|
|
54
|
+
context ??= {}
|
|
55
|
+
scripts ??= []
|
|
56
|
+
|
|
57
|
+
super({
|
|
58
|
+
name: 'child-manager',
|
|
59
|
+
...itcOpts,
|
|
60
|
+
handlers: {
|
|
61
|
+
log: message => {
|
|
62
|
+
return this.#log(message)
|
|
63
|
+
},
|
|
64
|
+
fetch: request => {
|
|
65
|
+
return this.#fetch(request)
|
|
66
|
+
},
|
|
67
|
+
...itcOpts.handlers
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
this.#id = generateChildrenId(context)
|
|
72
|
+
this.#loader = loader
|
|
73
|
+
this.#context = context
|
|
74
|
+
this.#scripts = scripts
|
|
75
|
+
this.#originalNodeOptions = process.env.NODE_OPTIONS
|
|
76
|
+
this.#logger = globalThis.platformatic.logger
|
|
77
|
+
this.#server = createServer()
|
|
78
|
+
this.#socketPath ??= getSocketPath(this.#id)
|
|
79
|
+
this.#clients = new Set()
|
|
80
|
+
this.#requests = new Map()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async listen () {
|
|
84
|
+
super.listen()
|
|
85
|
+
|
|
86
|
+
if (!isWindows) {
|
|
87
|
+
await createDirectory(dirname(this.#socketPath))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const wssServer = new WebSocketServer({ server: this.#server })
|
|
91
|
+
|
|
92
|
+
wssServer.on('connection', ws => {
|
|
93
|
+
this.#clients.add(ws)
|
|
94
|
+
|
|
95
|
+
ws.on('message', raw => {
|
|
96
|
+
try {
|
|
97
|
+
const message = JSON.parse(raw)
|
|
98
|
+
this.#requests.set(message.reqId, ws)
|
|
99
|
+
this.#listener(message)
|
|
100
|
+
} catch (error) {
|
|
101
|
+
this.#handleUnexpectedError(error, 'Handling a message failed.', exitCodes.MANAGER_MESSAGE_HANDLING_FAILED)
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
ws.on('close', () => {
|
|
106
|
+
this.#clients.delete(ws)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
ws.on('error', error => {
|
|
110
|
+
this.#handleUnexpectedError(
|
|
111
|
+
error,
|
|
112
|
+
'Error while communicating with the children process.',
|
|
113
|
+
exitCodes.MANAGER_SOCKET_ERROR
|
|
114
|
+
)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
this.#server.listen({ path: this.#socketPath }, resolve).on('error', reject)
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async close (signal) {
|
|
124
|
+
await rm(this.#dataPath)
|
|
125
|
+
|
|
126
|
+
for (const client of this.#clients) {
|
|
127
|
+
this.#currentClient = client
|
|
128
|
+
this._send(generateNotification('close', signal))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
super.close()
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async inject () {
|
|
135
|
+
await this.listen()
|
|
136
|
+
|
|
137
|
+
// Serialize data into a JSON file for the stackable to use
|
|
138
|
+
this.#dataPath = resolve(tmpdir(), 'platformatic', 'runtimes', `${this.#id}.json`)
|
|
139
|
+
await createDirectory(dirname(this.#dataPath))
|
|
140
|
+
|
|
141
|
+
// We write all the data to a JSON file
|
|
142
|
+
await writeFile(
|
|
143
|
+
this.#dataPath,
|
|
144
|
+
JSON.stringify(
|
|
145
|
+
{
|
|
146
|
+
data: this.#context,
|
|
147
|
+
loader: ensureFileUrl(this.#loader),
|
|
148
|
+
scripts: this.#scripts.map(s => ensureFileUrl(s))
|
|
149
|
+
},
|
|
150
|
+
null,
|
|
151
|
+
2
|
|
152
|
+
),
|
|
153
|
+
'utf-8'
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
process.env.PLT_MANAGER_ID = this.#id
|
|
157
|
+
process.env.NODE_OPTIONS =
|
|
158
|
+
`--import="${new URL('./child-process.js', import.meta.url)}" ${process.env.NODE_OPTIONS ?? ''}`.trim()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async eject () {
|
|
162
|
+
process.env.NODE_OPTIONS = this.#originalNodeOptions
|
|
163
|
+
process.env.PLT_MANAGER_ID = ''
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
register () {
|
|
167
|
+
register(this.#loader, { data: this.#context })
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
send (client, name, message) {
|
|
171
|
+
this.#currentClient = client
|
|
172
|
+
super.send(name, message)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
_send (message) {
|
|
176
|
+
if (!this.#currentClient) {
|
|
177
|
+
this.#currentClient = this.#requests.get(message.reqId)
|
|
178
|
+
this.#requests.delete(message.reqId)
|
|
179
|
+
|
|
180
|
+
if (!this.#currentClient) {
|
|
181
|
+
return
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this.#currentClient.send(JSON.stringify(message))
|
|
186
|
+
this.#currentClient = null
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_setupListener (listener) {
|
|
190
|
+
this.#listener = listener
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
_createClosePromise () {
|
|
194
|
+
return once(this.#server, 'exit')
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_close () {
|
|
198
|
+
this.#server.close()
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
#log (message) {
|
|
202
|
+
const logs = Array.isArray(message.logs) ? message.logs : [message.logs]
|
|
203
|
+
workerData.loggingPort.postMessage({ logs: logs.map(m => JSON.stringify(m)) })
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async #fetch (opts) {
|
|
207
|
+
const { statusCode, headers, body } = await request(opts)
|
|
208
|
+
|
|
209
|
+
const rawPayload = Buffer.from(await body.arrayBuffer())
|
|
210
|
+
const payload = rawPayload.toString()
|
|
211
|
+
|
|
212
|
+
return { statusCode, headers, body: payload, payload, rawPayload }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#handleUnexpectedError (error, message, exitCode) {
|
|
216
|
+
this.#logger.error({ err: errors.ensureLoggableError(error) }, message)
|
|
217
|
+
process.exit(exitCode)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { ITC } from '@platformatic/itc'
|
|
2
|
+
import { createPinoWritable, errors } from '@platformatic/utils'
|
|
3
|
+
import { tracingChannel } from 'node:diagnostics_channel'
|
|
4
|
+
import { once } from 'node:events'
|
|
5
|
+
import { readFile } from 'node:fs/promises'
|
|
6
|
+
import { register } from 'node:module'
|
|
7
|
+
import { platform, tmpdir } from 'node:os'
|
|
8
|
+
import { basename, resolve } from 'node:path'
|
|
9
|
+
import { isMainThread } from 'node:worker_threads'
|
|
10
|
+
import pino from 'pino'
|
|
11
|
+
import { getGlobalDispatcher, setGlobalDispatcher } from 'undici'
|
|
12
|
+
import { WebSocket } from 'ws'
|
|
13
|
+
import { exitCodes } from '../errors.js'
|
|
14
|
+
import { importFile } from '../utils.js'
|
|
15
|
+
import { getSocketPath, isWindows } from './child-manager.js'
|
|
16
|
+
|
|
17
|
+
function createInterceptor (itc) {
|
|
18
|
+
return function (dispatch) {
|
|
19
|
+
return async (opts, handler) => {
|
|
20
|
+
let url = opts.origin
|
|
21
|
+
if (!(url instanceof URL)) {
|
|
22
|
+
url = new URL(opts.path, url)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Other URLs are handled normally
|
|
26
|
+
if (!url.hostname.endsWith('.plt.local')) {
|
|
27
|
+
return dispatch(opts, handler)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const headers = {
|
|
31
|
+
...opts?.headers
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
delete headers.connection
|
|
35
|
+
delete headers['transfer-encoding']
|
|
36
|
+
headers.host = url.host
|
|
37
|
+
|
|
38
|
+
const requestOpts = {
|
|
39
|
+
...opts,
|
|
40
|
+
headers
|
|
41
|
+
}
|
|
42
|
+
delete requestOpts.dispatcher
|
|
43
|
+
|
|
44
|
+
itc
|
|
45
|
+
.send('fetch', requestOpts)
|
|
46
|
+
.then(res => {
|
|
47
|
+
if (res.rawPayload && !Buffer.isBuffer(res.rawPayload)) {
|
|
48
|
+
res.rawPayload = Buffer.from(res.rawPayload.data)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const headers = []
|
|
52
|
+
for (const [key, value] of Object.entries(res.headers)) {
|
|
53
|
+
if (Array.isArray(value)) {
|
|
54
|
+
for (const v of value) {
|
|
55
|
+
headers.push(key)
|
|
56
|
+
headers.push(v)
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
headers.push(key)
|
|
60
|
+
headers.push(value)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
handler.onHeaders(res.statusCode, headers, () => {}, res.statusMessage)
|
|
65
|
+
handler.onData(res.rawPayload)
|
|
66
|
+
handler.onComplete([])
|
|
67
|
+
})
|
|
68
|
+
.catch(e => {
|
|
69
|
+
handler.onError(new Error(e.message))
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class ChildProcess extends ITC {
|
|
78
|
+
#listener
|
|
79
|
+
#socket
|
|
80
|
+
#child
|
|
81
|
+
#logger
|
|
82
|
+
#pendingMessages
|
|
83
|
+
|
|
84
|
+
constructor () {
|
|
85
|
+
super({ throwOnMissingHandler: false, name: `${process.env.PLT_MANAGER_ID}-child-process` })
|
|
86
|
+
|
|
87
|
+
const protocol = platform() === 'win32' ? 'ws+unix:' : 'ws+unix://'
|
|
88
|
+
this.#socket = new WebSocket(`${protocol}${getSocketPath(process.env.PLT_MANAGER_ID)}`)
|
|
89
|
+
this.#pendingMessages = []
|
|
90
|
+
|
|
91
|
+
this.listen()
|
|
92
|
+
this.#setupLogger()
|
|
93
|
+
this.#setupHandlers()
|
|
94
|
+
this.#setupServer()
|
|
95
|
+
this.#setupInterceptors()
|
|
96
|
+
|
|
97
|
+
this.on('close', signal => {
|
|
98
|
+
process.kill(process.pid, signal)
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_setupListener (listener) {
|
|
103
|
+
this.#listener = listener
|
|
104
|
+
|
|
105
|
+
this.#socket.on('open', () => {
|
|
106
|
+
// Never hang the process on this socket.
|
|
107
|
+
this.#socket._socket.unref()
|
|
108
|
+
|
|
109
|
+
for (const message of this.#pendingMessages) {
|
|
110
|
+
this.#socket.send(message)
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
this.#socket.on('message', message => {
|
|
115
|
+
try {
|
|
116
|
+
this.#listener(JSON.parse(message))
|
|
117
|
+
} catch (error) {
|
|
118
|
+
this.#logger.error({ err: errors.ensureLoggableError(error) }, 'Handling a message failed.')
|
|
119
|
+
process.exit(exitCodes.PROCESS_MESSAGE_HANDLING_FAILED)
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
this.#socket.on('error', error => {
|
|
124
|
+
process._rawDebug(error)
|
|
125
|
+
// There is nothing to log here as the connection with the parent thread is lost. Exit with a special code
|
|
126
|
+
process.exit(exitCodes.PROCESS_SOCKET_ERROR)
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_send (message) {
|
|
131
|
+
if (this.#socket.readyState === WebSocket.CONNECTING) {
|
|
132
|
+
this.#pendingMessages.push(JSON.stringify(message))
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.#socket.send(JSON.stringify(message))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_createClosePromise () {
|
|
140
|
+
return once(this.#socket, 'close')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
_close () {
|
|
144
|
+
this.#socket.close()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#setupLogger () {
|
|
148
|
+
// Since this is executed by user code, make sure we only override this in the main thread
|
|
149
|
+
// The rest will be intercepted by the BaseStackable.
|
|
150
|
+
if (isMainThread) {
|
|
151
|
+
this.#logger = pino({
|
|
152
|
+
level: 'info',
|
|
153
|
+
name: globalThis.platformatic.id,
|
|
154
|
+
transport: {
|
|
155
|
+
target: new URL('./child-transport.js', import.meta.url).toString(),
|
|
156
|
+
options: { id: globalThis.platformatic.id }
|
|
157
|
+
}
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
Reflect.defineProperty(process, 'stdout', { value: createPinoWritable(this.#logger, 'info') })
|
|
161
|
+
Reflect.defineProperty(process, 'stderr', { value: createPinoWritable(this.#logger, 'error') })
|
|
162
|
+
} else {
|
|
163
|
+
this.#logger = pino({ level: 'info', name: globalThis.platformatic.id })
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#setupServer () {
|
|
168
|
+
const subscribers = {
|
|
169
|
+
asyncStart ({ options }) {
|
|
170
|
+
const port = globalThis.platformatic.port
|
|
171
|
+
|
|
172
|
+
if (port !== false) {
|
|
173
|
+
options.port = typeof port === 'number' ? port : 0
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
asyncEnd: ({ server }) => {
|
|
177
|
+
tracingChannel('net.server.listen').unsubscribe(subscribers)
|
|
178
|
+
|
|
179
|
+
const { family, address, port } = server.address()
|
|
180
|
+
const url = new URL(family === 'IPv6' ? `http://[${address}]:${port}` : `http://${address}:${port}`).origin
|
|
181
|
+
|
|
182
|
+
this.notify('url', url)
|
|
183
|
+
},
|
|
184
|
+
error: error => {
|
|
185
|
+
tracingChannel('net.server.listen').unsubscribe(subscribers)
|
|
186
|
+
this.notify('error', error)
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
tracingChannel('net.server.listen').subscribe(subscribers)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#setupInterceptors () {
|
|
194
|
+
setGlobalDispatcher(getGlobalDispatcher().compose(createInterceptor(this)))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#setupHandlers () {
|
|
198
|
+
function handleUnhandled (type, err) {
|
|
199
|
+
process._rawDebug(globalThis.platformatic.id, err)
|
|
200
|
+
this.#logger.error(
|
|
201
|
+
{ err: errors.ensureLoggableError(err) },
|
|
202
|
+
`Child process for service ${globalThis.platformatic.id} threw an ${type}.`
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
process.exit(exitCodes.PROCESS_UNHANDLED_ERROR)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
process.on('uncaughtException', handleUnhandled.bind(this, 'uncaught exception'))
|
|
209
|
+
process.on('unhandledRejection', handleUnhandled.bind(this, 'unhandled rejection'))
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function main () {
|
|
214
|
+
const dataPath = resolve(tmpdir(), 'platformatic', 'runtimes', `${process.env.PLT_MANAGER_ID}.json`)
|
|
215
|
+
const { data, loader, scripts } = JSON.parse(await readFile(dataPath))
|
|
216
|
+
|
|
217
|
+
globalThis.platformatic = data
|
|
218
|
+
|
|
219
|
+
if (loader) {
|
|
220
|
+
register(loader, { data })
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const script of scripts) {
|
|
224
|
+
await importFile(script)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
globalThis[Symbol.for('plt.children.itc')] = new ChildProcess()
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!isWindows || basename(process.argv.at(-1)) !== 'npm-prefix.js') {
|
|
231
|
+
await main()
|
|
232
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { generateRequest } from '@platformatic/itc'
|
|
2
|
+
import { errors } from '@platformatic/utils'
|
|
3
|
+
import { once } from 'node:events'
|
|
4
|
+
import { platform } from 'node:os'
|
|
5
|
+
import { workerData } from 'node:worker_threads'
|
|
6
|
+
import build from 'pino-abstract-transport'
|
|
7
|
+
import { WebSocket } from 'ws'
|
|
8
|
+
import { getSocketPath } from './child-manager.js'
|
|
9
|
+
|
|
10
|
+
function logDirectError (message, error) {
|
|
11
|
+
process._rawDebug(`Logger thread for child process of service ${workerData.id} ${message}.`, {
|
|
12
|
+
error: errors.ensureLoggableError(error)
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function handleUnhandled (type, error) {
|
|
17
|
+
logDirectError(`threw an ${type}`, error)
|
|
18
|
+
process.exit(6)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
process.on('uncaughtException', handleUnhandled.bind(null, 'uncaught exception'))
|
|
22
|
+
process.on('unhandledRejection', handleUnhandled.bind(null, 'unhandled rejection'))
|
|
23
|
+
|
|
24
|
+
export default async function (opts) {
|
|
25
|
+
try {
|
|
26
|
+
const protocol = platform() === 'win32' ? 'ws+unix:' : 'ws+unix://'
|
|
27
|
+
const socket = new WebSocket(`${protocol}${getSocketPath(process.env.PLT_MANAGER_ID)}`)
|
|
28
|
+
|
|
29
|
+
await once(socket, 'open')
|
|
30
|
+
|
|
31
|
+
// Do not process responses but empty the socket inbound queue
|
|
32
|
+
socket.on('message', () => {})
|
|
33
|
+
|
|
34
|
+
socket.on('error', error => {
|
|
35
|
+
logDirectError('threw a socket error', error)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return build(
|
|
39
|
+
async function (source) {
|
|
40
|
+
for await (const obj of source) {
|
|
41
|
+
socket.send(JSON.stringify(generateRequest('log', { logs: [obj] })))
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
close (_, cb) {
|
|
46
|
+
socket.close()
|
|
47
|
+
cb()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
} catch (error) {
|
|
52
|
+
logDirectError('threw a connection error', error)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { withResolvers } from '@platformatic/utils'
|
|
2
|
+
import { subscribe, tracingChannel, unsubscribe } from 'node:diagnostics_channel'
|
|
3
|
+
|
|
4
|
+
export function createServerListener (overridePort = true) {
|
|
5
|
+
const { promise, resolve, reject } = withResolvers()
|
|
6
|
+
|
|
7
|
+
const subscribers = {
|
|
8
|
+
asyncStart ({ options }) {
|
|
9
|
+
if (overridePort !== false) {
|
|
10
|
+
options.port = typeof overridePort === 'number' ? overridePort : 0
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
asyncEnd ({ server }) {
|
|
14
|
+
resolve(server)
|
|
15
|
+
},
|
|
16
|
+
error (error) {
|
|
17
|
+
cancel()
|
|
18
|
+
reject(error)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function cancel () {
|
|
23
|
+
tracingChannel('net.server.listen').unsubscribe(subscribers)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
tracingChannel('net.server.listen').subscribe(subscribers)
|
|
27
|
+
promise.finally(cancel)
|
|
28
|
+
promise.cancel = resolve.bind(null, null)
|
|
29
|
+
|
|
30
|
+
return promise
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createChildProcessListener () {
|
|
34
|
+
const { promise, resolve } = withResolvers()
|
|
35
|
+
|
|
36
|
+
const handler = ({ process: child }) => {
|
|
37
|
+
unsubscribe('child_process', handler)
|
|
38
|
+
resolve(child)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function cancel () {
|
|
42
|
+
unsubscribe('child_process', handler)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
subscribe('child_process', handler)
|
|
46
|
+
|
|
47
|
+
promise.finally(cancel)
|
|
48
|
+
promise.cancel = resolve.bind(null, null)
|
|
49
|
+
|
|
50
|
+
return promise
|
|
51
|
+
}
|