@platformatic/runtime 1.33.0 → 1.35.1

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.
File without changes
File without changes
@@ -0,0 +1,4 @@
1
+ PLT_SERVER_HOSTNAME=127.0.0.1
2
+ PORT=3000
3
+ PLT_SERVER_LOGGER_LEVEL=info
4
+ PLT_EXAMPLE_ORIGIN=
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ async function default_1(app) {
4
+ }
5
+ exports.default = default_1;
6
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../plugin.ts"],"names":[],"mappings":";;AAGe,KAAK,oBAAW,GAAoB;AACnD,CAAC;AADD,4BACC"}
@@ -0,0 +1,3 @@
1
+ PLT_SERVER_HOSTNAME=127.0.0.1
2
+ PORT=3002
3
+ PLT_SERVER_LOGGER_LEVEL=info
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ async function default_1(fastify, opts) {
4
+ fastify.decorate('example', 'foobar');
5
+ }
6
+ exports.default = default_1;
7
+ //# sourceMappingURL=example.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"example.js","sourceRoot":"","sources":["../../plugins/example.ts"],"names":[],"mappings":";;AAGe,KAAK,oBAAW,OAAwB,EAAE,IAA0B;IACjF,OAAO,CAAC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAA;AACvC,CAAC;AAFD,4BAEC"}
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ async function default_1(fastify, opts) {
4
+ fastify.get('/', async (request, reply) => {
5
+ return { hello: fastify.example };
6
+ });
7
+ fastify.get('/titles', async (request, reply) => {
8
+ const movies = await fastify.client.getMovies({});
9
+ const titles = movies.map((movie) => movie.title);
10
+ return { titles };
11
+ });
12
+ }
13
+ exports.default = default_1;
14
+ //# sourceMappingURL=root.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"root.js","sourceRoot":"","sources":["../../routes/root.ts"],"names":[],"mappings":";;AAUe,KAAK,oBAAW,OAAwB,EAAE,IAA0B;IACjF,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACxC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,EAAE,CAAA;IACnC,CAAC,CAAC,CAAA;IAEF,OAAO,CAAC,GAAG,CAAC,SAAS,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QAC9C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAA;QACjD,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;QACjD,OAAO,EAAE,MAAM,EAAE,CAAA;IACnB,CAAC,CAAC,CAAA;AACJ,CAAC;AAVD,4BAUC"}
@@ -0,0 +1,4 @@
1
+ PLT_SERVER_HOSTNAME=127.0.0.1
2
+ PORT=3000
3
+ PLT_SERVER_LOGGER_LEVEL=info
4
+ PLT_EXAMPLE_ORIGIN=
@@ -0,0 +1,4 @@
1
+ PLT_SERVER_HOSTNAME=127.0.0.1
2
+ PORT=3001
3
+ PLT_SERVER_LOGGER_LEVEL=info
4
+ DATABASE_URL=sqlite://./db.sqlite
@@ -0,0 +1,3 @@
1
+ PLT_SERVER_HOSTNAME=127.0.0.1
2
+ PORT=3002
3
+ PLT_SERVER_LOGGER_LEVEL=info
package/lib/api-client.js CHANGED
@@ -1,9 +1,16 @@
1
1
  'use strict'
2
2
 
3
+ const { tmpdir } = require('node:os')
4
+ const { join } = require('node:path')
3
5
  const { once, EventEmitter } = require('node:events')
4
- const { randomUUID } = require('node:crypto')
5
- const errors = require('./errors')
6
+ const { randomUUID, createHash } = require('node:crypto')
7
+ const { createReadStream, watch } = require('node:fs')
8
+ const { readdir, readFile, stat, access } = require('node:fs/promises')
6
9
  const { setTimeout: sleep } = require('node:timers/promises')
10
+ const errors = require('./errors')
11
+ const ts = require('tail-file-stream')
12
+
13
+ const platformaticVersion = require('../package.json').version
7
14
 
8
15
  const MAX_LISTENERS_COUNT = 100
9
16
  const MAX_METRICS_QUEUE_LENGTH = 5 * 60 // 5 minutes in seconds
@@ -12,14 +19,18 @@ const COLLECT_METRICS_TIMEOUT = 1000
12
19
  class RuntimeApiClient extends EventEmitter {
13
20
  #exitCode
14
21
  #exitPromise
22
+ #configManager
23
+ #runtimeTmpDir
15
24
  #metrics
16
25
  #metricsTimeout
17
26
 
18
- constructor (worker) {
27
+ constructor (worker, configManager) {
19
28
  super()
20
29
  this.setMaxListeners(MAX_LISTENERS_COUNT)
21
30
 
22
31
  this.worker = worker
32
+ this.#configManager = configManager
33
+ this.#runtimeTmpDir = getRuntimeTmpDir(configManager.dirname)
23
34
  this.#exitPromise = this.#exitHandler()
24
35
  this.worker.on('message', (message) => {
25
36
  if (message.operationId) {
@@ -28,6 +39,37 @@ class RuntimeApiClient extends EventEmitter {
28
39
  })
29
40
  }
30
41
 
42
+ async #getRuntimePackageJson () {
43
+ const runtimeDir = this.#configManager.dirname
44
+ const packageJsonPath = join(runtimeDir, 'package.json')
45
+ const packageJsonFile = await readFile(packageJsonPath, 'utf8')
46
+ const packageJson = JSON.parse(packageJsonFile)
47
+ return packageJson
48
+ }
49
+
50
+ async getRuntimeMetadata () {
51
+ const packageJson = await this.#getRuntimePackageJson()
52
+ const entrypointDetails = await this.getEntrypointDetails()
53
+
54
+ return {
55
+ pid: process.pid,
56
+ cwd: process.cwd(),
57
+ argv: process.argv,
58
+ uptimeSeconds: Math.floor(process.uptime()),
59
+ execPath: process.execPath,
60
+ nodeVersion: process.version,
61
+ projectDir: this.#configManager.dirname,
62
+ packageName: packageJson.name ?? null,
63
+ packageVersion: packageJson.version ?? null,
64
+ url: entrypointDetails?.url ?? null,
65
+ platformaticVersion
66
+ }
67
+ }
68
+
69
+ getRuntimeConfig () {
70
+ return this.#configManager.current
71
+ }
72
+
31
73
  async start () {
32
74
  const address = await this.#sendCommand('plt:start-services')
33
75
  this.emit('start', address)
@@ -217,6 +259,162 @@ class RuntimeApiClient extends EventEmitter {
217
259
  }, COLLECT_METRICS_TIMEOUT).unref()
218
260
  }
219
261
 
262
+ async pipeLogsStream (writableStream, logger, startLogId, endLogId, runtimePID) {
263
+ endLogId = endLogId || Infinity
264
+ runtimePID = runtimePID ?? process.pid
265
+
266
+ const runtimeLogFiles = await this.#getRuntimeLogFiles(runtimePID)
267
+ if (runtimeLogFiles.length === 0) {
268
+ writableStream.end()
269
+ return
270
+ }
271
+
272
+ let latestFileId = parseInt(runtimeLogFiles.at(-1).slice('logs.'.length))
273
+
274
+ let waiting = false
275
+ let fileStream = null
276
+ let fileId = startLogId ?? latestFileId
277
+
278
+ const runtimeLogsDir = this.#getRuntimeLogsDir(runtimePID)
279
+
280
+ const watcher = watch(runtimeLogsDir, async (event, filename) => {
281
+ if (event === 'rename' && filename.startsWith('logs')) {
282
+ const logFileId = parseInt(filename.slice('logs.'.length))
283
+ if (logFileId > latestFileId) {
284
+ latestFileId = logFileId
285
+ if (waiting) {
286
+ streamLogFile(++fileId)
287
+ }
288
+ }
289
+ }
290
+ }).unref()
291
+
292
+ const streamLogFile = () => {
293
+ if (fileId > endLogId) {
294
+ writableStream.end()
295
+ return
296
+ }
297
+
298
+ const fileName = 'logs.' + fileId
299
+ const filePath = join(runtimeLogsDir, fileName)
300
+
301
+ const prevFileStream = fileStream
302
+
303
+ fileStream = ts.createReadStream(filePath)
304
+ fileStream.pipe(writableStream, { end: false })
305
+
306
+ if (prevFileStream) {
307
+ prevFileStream.unpipe(writableStream)
308
+ prevFileStream.destroy()
309
+ }
310
+
311
+ fileStream.on('error', (err) => {
312
+ logger.error(err, 'Error streaming log file')
313
+ fileStream.destroy()
314
+ watcher.close()
315
+ writableStream.end()
316
+ })
317
+
318
+ fileStream.on('data', () => {
319
+ waiting = false
320
+ })
321
+
322
+ fileStream.on('eof', () => {
323
+ if (fileId >= endLogId) {
324
+ writableStream.end()
325
+ return
326
+ }
327
+ if (latestFileId > fileId) {
328
+ streamLogFile(++fileId)
329
+ } else {
330
+ waiting = true
331
+ }
332
+ })
333
+
334
+ return fileStream
335
+ }
336
+
337
+ streamLogFile(fileId)
338
+
339
+ writableStream.on('close', () => {
340
+ watcher.close()
341
+ fileStream.destroy()
342
+ })
343
+ writableStream.on('error', () => {
344
+ watcher.close()
345
+ fileStream.destroy()
346
+ })
347
+ }
348
+
349
+ async getLogIds () {
350
+ const runtimesLogFiles = await this.#getAllLogsFiles()
351
+ const runtimesLogsIds = []
352
+
353
+ for (const runtime of runtimesLogFiles) {
354
+ const runtimeLogIds = []
355
+ for (const logFile of runtime.runtimeLogFiles) {
356
+ const logId = parseInt(logFile.slice('logs.'.length))
357
+ runtimeLogIds.push(logId)
358
+ }
359
+ runtimesLogsIds.push({
360
+ pid: runtime.runtimePID,
361
+ indexes: runtimeLogIds
362
+ })
363
+ }
364
+
365
+ return runtimesLogsIds
366
+ }
367
+
368
+ async getLogFileStream (logFileId, runtimePID) {
369
+ const runtimeLogsDir = this.#getRuntimeLogsDir(runtimePID)
370
+ const filePath = join(runtimeLogsDir, `logs.${logFileId}`)
371
+ return createReadStream(filePath)
372
+ }
373
+
374
+ #getRuntimeLogsDir (runtimePID) {
375
+ return join(this.#runtimeTmpDir, runtimePID.toString(), 'logs')
376
+ }
377
+
378
+ async #getRuntimeLogFiles (runtimePID) {
379
+ const runtimeLogsDir = this.#getRuntimeLogsDir(runtimePID)
380
+ const runtimeLogsFiles = await readdir(runtimeLogsDir)
381
+ return runtimeLogsFiles
382
+ .filter((file) => file.startsWith('logs'))
383
+ .sort((log1, log2) => {
384
+ const index1 = parseInt(log1.slice('logs.'.length))
385
+ const index2 = parseInt(log2.slice('logs.'.length))
386
+ return index1 - index2
387
+ })
388
+ }
389
+
390
+ async #getAllLogsFiles () {
391
+ try {
392
+ await access(this.#runtimeTmpDir)
393
+ } catch (error) {
394
+ return []
395
+ }
396
+
397
+ const runtimePIDs = await readdir(this.#runtimeTmpDir)
398
+ const runtimesLogFiles = []
399
+
400
+ for (const runtimePID of runtimePIDs) {
401
+ const runtimeLogsDir = this.#getRuntimeLogsDir(runtimePID)
402
+ const runtimeLogsDirStat = await stat(runtimeLogsDir)
403
+ const runtimeLogFiles = await this.#getRuntimeLogFiles(runtimePID)
404
+ const lastModified = runtimeLogsDirStat.mtime
405
+
406
+ runtimesLogFiles.push({
407
+ runtimePID: parseInt(runtimePID),
408
+ runtimeLogFiles,
409
+ lastModified
410
+ })
411
+ }
412
+
413
+ return runtimesLogFiles.sort(
414
+ (runtime1, runtime2) => runtime1.lastModified - runtime2.lastModified
415
+ )
416
+ }
417
+
220
418
  async #sendCommand (command, params = {}) {
221
419
  const operationId = randomUUID()
222
420
 
@@ -248,4 +446,19 @@ class RuntimeApiClient extends EventEmitter {
248
446
  }
249
447
  }
250
448
 
251
- module.exports = RuntimeApiClient
449
+ function getRuntimeTmpDir (runtimeDir) {
450
+ const platformaticTmpDir = join(tmpdir(), 'platformatic', 'applications')
451
+ const runtimeDirHash = createHash('md5').update(runtimeDir).digest('hex')
452
+ return join(platformaticTmpDir, runtimeDirHash)
453
+ }
454
+
455
+ function getRuntimeLogsDir (runtimeDir, runtimePID) {
456
+ const runtimeTmpDir = getRuntimeTmpDir(runtimeDir)
457
+ return join(runtimeTmpDir, runtimePID.toString(), 'logs')
458
+ }
459
+
460
+ module.exports = {
461
+ RuntimeApiClient,
462
+ getRuntimeTmpDir,
463
+ getRuntimeLogsDir
464
+ }
package/lib/api.js CHANGED
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { once } = require('node:events')
3
4
  const { getGlobalDispatcher, setGlobalDispatcher } = require('undici')
4
5
  const { createFastifyInterceptor } = require('fastify-undici-dispatcher')
5
6
  const { PlatformaticApp } = require('./app')
@@ -84,9 +85,11 @@ class RuntimeApi {
84
85
  }))
85
86
 
86
87
  for (const service of services) {
87
- if (service.getStatus() === 'started') {
88
- return
89
- }
88
+ const serviceStatus = service.getStatus()
89
+ if (
90
+ serviceStatus === 'starting' ||
91
+ serviceStatus === 'started'
92
+ ) return
90
93
  }
91
94
 
92
95
  if (this.#dispatcher) {
@@ -159,6 +162,11 @@ class RuntimeApi {
159
162
  const stopServiceReqs = []
160
163
  for (const service of this.#services.values()) {
161
164
  const serviceStatus = service.getStatus()
165
+ if (serviceStatus === 'starting') {
166
+ stopServiceReqs.push(
167
+ once(service, 'start').then(() => service.stop())
168
+ )
169
+ }
162
170
  if (serviceStatus === 'started') {
163
171
  stopServiceReqs.push(service.stop())
164
172
  }
@@ -169,10 +177,13 @@ class RuntimeApi {
169
177
  async #restartServices () {
170
178
  let entrypointUrl = null
171
179
  for (const service of this.#services.values()) {
172
- if (service.server) {
173
- await service.restart(true)
180
+ const serviceStatus = service.getStatus()
181
+ if (serviceStatus === 'starting') {
182
+ await once(service, 'start')
174
183
  }
175
184
 
185
+ await service.restart(true)
186
+
176
187
  if (service.appConfig.entrypoint) {
177
188
  entrypointUrl = service.server.url
178
189
  }
@@ -236,18 +247,19 @@ class RuntimeApi {
236
247
  #getServiceConfig ({ id }) {
237
248
  const service = this.#getServiceById(id)
238
249
 
239
- const { config } = service
240
- if (!config) {
250
+ const serviceStatus = service.getStatus()
251
+ if (serviceStatus !== 'started') {
241
252
  throw new errors.ServiceNotStartedError(id)
242
253
  }
243
254
 
244
- return config.configManager.current
255
+ return service.config.configManager.current
245
256
  }
246
257
 
247
258
  async #getServiceOpenapiSchema ({ id }) {
248
259
  const service = this.#getServiceById(id)
249
260
 
250
- if (!service.config) {
261
+ const serviceStatus = service.getStatus()
262
+ if (serviceStatus !== 'started') {
251
263
  throw new errors.ServiceNotStartedError(id)
252
264
  }
253
265
 
@@ -267,7 +279,8 @@ class RuntimeApi {
267
279
  async #getServiceGraphqlSchema ({ id }) {
268
280
  const service = this.#getServiceById(id)
269
281
 
270
- if (!service.config) {
282
+ const serviceStatus = service.getStatus()
283
+ if (serviceStatus !== 'started') {
271
284
  throw new errors.ServiceNotStartedError(id)
272
285
  }
273
286
 
@@ -293,7 +306,8 @@ class RuntimeApi {
293
306
  }
294
307
  }
295
308
 
296
- if (!entrypoint.config) {
309
+ const entrypointStatus = entrypoint.getStatus()
310
+ if (entrypointStatus !== 'started') {
297
311
  throw new errors.ServiceNotStartedError(entrypoint.id)
298
312
  }
299
313
 
@@ -313,11 +327,23 @@ class RuntimeApi {
313
327
 
314
328
  async #startService ({ id }) {
315
329
  const service = this.#getServiceById(id)
316
- await service.start()
330
+ const serviceStatus = service.getStatus()
331
+
332
+ if (serviceStatus === 'starting') {
333
+ await once(service, 'start')
334
+ } else {
335
+ await service.start()
336
+ }
317
337
  }
318
338
 
319
339
  async #stopService ({ id }) {
320
340
  const service = this.#getServiceById(id)
341
+ const serviceStatus = service.getStatus()
342
+
343
+ if (serviceStatus === 'starting') {
344
+ await once(service, 'start')
345
+ }
346
+
321
347
  await service.stop()
322
348
  }
323
349
 
package/lib/app.js CHANGED
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
- const { once } = require('node:events')
4
3
  const { dirname } = require('node:path')
4
+ const { EventEmitter, once } = require('node:events')
5
5
  const { FileWatcher } = require('@platformatic/utils')
6
6
  const debounce = require('debounce')
7
7
  const { snakeCase } = require('change-case-all')
@@ -9,10 +9,10 @@ const { buildServer } = require('./build-server')
9
9
  const { loadConfig } = require('./load-config')
10
10
  const errors = require('./errors')
11
11
 
12
- class PlatformaticApp {
12
+ class PlatformaticApp extends EventEmitter {
13
13
  #hotReload
14
14
  #loaderPort
15
- #restarting
15
+ #starting
16
16
  #started
17
17
  #originalWatch
18
18
  #fileWatcher
@@ -23,13 +23,14 @@ class PlatformaticApp {
23
23
  #hasManagementApi
24
24
 
25
25
  constructor (appConfig, loaderPort, logger, telemetryConfig, serverConfig, hasManagementApi) {
26
+ super()
26
27
  this.appConfig = appConfig
27
28
  this.config = null
28
29
  this.#hotReload = false
29
30
  this.#loaderPort = loaderPort
30
- this.#restarting = false
31
- this.server = null
31
+ this.#starting = false
32
32
  this.#started = false
33
+ this.server = null
33
34
  this.#originalWatch = null
34
35
  this.#fileWatcher = null
35
36
  this.#hasManagementApi = !!hasManagementApi
@@ -47,19 +48,18 @@ class PlatformaticApp {
47
48
  }
48
49
 
49
50
  getStatus () {
50
- if (this.#started) {
51
- return 'started'
52
- } else {
53
- return 'stopped'
54
- }
51
+ if (this.#starting) return 'starting'
52
+ if (this.#started) return 'started'
53
+ return 'stopped'
55
54
  }
56
55
 
57
56
  async restart (force) {
58
- if (this.#restarting || !this.#started || (!this.#hotReload && !force)) {
57
+ if (this.#starting || !this.#started || (!this.#hotReload && !force)) {
59
58
  return
60
59
  }
61
60
 
62
- this.#restarting = true
61
+ this.#starting = true
62
+ this.#started = false
63
63
 
64
64
  // The CJS cache should not be cleared from the loader because v20 moved
65
65
  // the loader to a different thread with a different CJS cache.
@@ -84,15 +84,18 @@ class PlatformaticApp {
84
84
  this.#logger.error({ err })
85
85
  }
86
86
 
87
- this.#restarting = false
87
+ this.#started = true
88
+ this.#starting = false
89
+ this.emit('start')
90
+ this.emit('restart')
88
91
  }
89
92
 
90
93
  async start () {
91
- if (this.#started) {
94
+ if (this.#starting || this.#started) {
92
95
  throw new errors.ApplicationAlreadyStartedError()
93
96
  }
94
97
 
95
- this.#started = true
98
+ this.#starting = true
96
99
 
97
100
  await this.#initializeConfig()
98
101
  await this.#updateConfig()
@@ -136,10 +139,14 @@ class PlatformaticApp {
136
139
  // Make sure the server has run all the onReady hooks before returning.
137
140
  await this.server.ready()
138
141
  }
142
+
143
+ this.#started = true
144
+ this.#starting = false
145
+ this.emit('start')
139
146
  }
140
147
 
141
148
  async stop () {
142
- if (!this.#started) {
149
+ if (!this.#started || this.#starting) {
143
150
  throw new errors.ApplicationNotStartedError()
144
151
  }
145
152
 
@@ -147,6 +154,8 @@ class PlatformaticApp {
147
154
  await this.server.close()
148
155
 
149
156
  this.#started = false
157
+ this.#starting = false
158
+ this.emit('stop')
150
159
  }
151
160
 
152
161
  async handleProcessLevelEvent ({ signal, err }) {
@@ -172,6 +181,10 @@ class PlatformaticApp {
172
181
  this.server.log.info({ signal }, 'received signal')
173
182
  }
174
183
 
184
+ if (this.#starting) {
185
+ await once(this, 'start')
186
+ }
187
+
175
188
  if (this.#started) {
176
189
  await this.stop()
177
190
  this.server.log.info('server stopped')
package/lib/config.js CHANGED
@@ -13,7 +13,15 @@ async function _transformConfig (configManager) {
13
13
  const services = config.services ?? []
14
14
 
15
15
  if (config.autoload) {
16
- const { path, exclude = [], mappings = {} } = config.autoload
16
+ const { exclude = [], mappings = {} } = config.autoload
17
+ let { path } = config.autoload
18
+
19
+ // This is a hack, but it's the only way to not fix the paths for the autoloaded services
20
+ // while we are upgrading the config
21
+ if (configManager._fixPaths) {
22
+ path = pathResolve(configManager.dirname, path)
23
+ }
24
+
17
25
  const entries = await readdir(path, { withFileTypes: true })
18
26
 
19
27
  for (let i = 0; i < entries.length; ++i) {
@@ -46,6 +54,7 @@ async function _transformConfig (configManager) {
46
54
  }
47
55
 
48
56
  configManager.current.allowCycles = !!configManager.current.allowCycles
57
+
49
58
  configManager.current.serviceMap = new Map()
50
59
  configManager.current.inspectorOptions = undefined
51
60
 
@@ -54,7 +63,9 @@ async function _transformConfig (configManager) {
54
63
  for (let i = 0; i < services.length; ++i) {
55
64
  const service = services[i]
56
65
 
57
- service.config = pathResolve(service.path, service.config)
66
+ if (configManager._fixPaths) {
67
+ service.config = pathResolve(service.path, service.config)
68
+ }
58
69
  service.entrypoint = service.id === config.entrypoint
59
70
  service.hotReload = !!config.hotReload
60
71
  service.dependencies = []
@@ -74,6 +85,7 @@ async function _transformConfig (configManager) {
74
85
  }
75
86
 
76
87
  configManager.current.services = services
88
+
77
89
  await parseClientsAndComposer(configManager)
78
90
 
79
91
  if (!configManager.current.allowCycles) {
@@ -1,18 +1,17 @@
1
1
  'use strict'
2
2
 
3
- const { tmpdir, platform } = require('node:os')
3
+ const { tmpdir } = require('node:os')
4
+ const { platform } = require('node:os')
4
5
  const { join } = require('node:path')
5
- const { readFile, mkdir, rm } = require('node:fs/promises')
6
+ const { mkdir, rm } = require('node:fs/promises')
6
7
  const fastify = require('fastify')
7
8
  const ws = require('ws')
9
+ const { getRuntimeLogsDir } = require('./api-client.js')
8
10
  const errors = require('./errors')
9
- const { pipeLogsStream, getLogFileStream, getLogIndexes } = require('./logs')
10
- const platformaticVersion = require('../package.json').version
11
11
 
12
12
  const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
13
- const runtimeTmpDir = join(PLATFORMATIC_TMP_DIR, process.pid.toString())
14
13
 
15
- async function createManagementApi (configManager, runtimeApiClient) {
14
+ async function createManagementApi (runtimeApiClient) {
16
15
  const app = fastify()
17
16
  app.log.warn(
18
17
  'Runtime Management API is in the experimental stage. ' +
@@ -21,37 +20,15 @@ async function createManagementApi (configManager, runtimeApiClient) {
21
20
  'Use of the feature is not recommended in production environments.'
22
21
  )
23
22
 
24
- async function getRuntimePackageJson (cwd) {
25
- const packageJsonPath = join(cwd, 'package.json')
26
- const packageJsonFile = await readFile(packageJsonPath, 'utf8')
27
- const packageJson = JSON.parse(packageJsonFile)
28
- return packageJson
29
- }
30
-
31
23
  app.register(require('@fastify/websocket'))
32
24
 
33
25
  app.register(async (app) => {
34
26
  app.get('/metadata', async () => {
35
- const packageJson = await getRuntimePackageJson(configManager.dirname).catch(() => ({}))
36
- const entrypointDetails = await runtimeApiClient.getEntrypointDetails().catch(() => null)
37
-
38
- return {
39
- pid: process.pid,
40
- cwd: process.cwd(),
41
- argv: process.argv,
42
- uptimeSeconds: Math.floor(process.uptime()),
43
- execPath: process.execPath,
44
- nodeVersion: process.version,
45
- projectDir: configManager.dirname,
46
- packageName: packageJson.name ?? null,
47
- packageVersion: packageJson.version ?? null,
48
- url: entrypointDetails?.url ?? null,
49
- platformaticVersion
50
- }
27
+ return runtimeApiClient.getRuntimeMetadata()
51
28
  })
52
29
 
53
30
  app.get('/config', async () => {
54
- return configManager.current
31
+ return runtimeApiClient.getRuntimeConfig()
55
32
  })
56
33
 
57
34
  app.get('/env', async () => {
@@ -100,6 +77,11 @@ async function createManagementApi (configManager, runtimeApiClient) {
100
77
  const { id, '*': requestUrl } = request.params
101
78
  app.log.debug('proxy request', { id, requestUrl })
102
79
 
80
+ delete request.headers.connection
81
+ delete request.headers['content-length']
82
+ delete request.headers['content-encoding']
83
+ delete request.headers['transfer-encoding']
84
+
103
85
  const injectParams = {
104
86
  method: request.method,
105
87
  url: requestUrl || '/',
@@ -142,43 +124,67 @@ async function createManagementApi (configManager, runtimeApiClient) {
142
124
  })
143
125
 
144
126
  app.get('/logs/live', { websocket: true }, async (socket, req) => {
145
- const startLogIndex = req.query.start ? parseInt(req.query.start) : null
127
+ const startLogId = req.query.start ? parseInt(req.query.start) : null
146
128
 
147
- if (startLogIndex) {
148
- const logIndexes = await getLogIndexes()
149
- if (!logIndexes.includes(startLogIndex)) {
150
- throw new errors.LogFileNotFound(startLogIndex)
129
+ if (startLogId) {
130
+ const logIds = await runtimeApiClient.getLogIds()
131
+ if (!logIds.includes(startLogId)) {
132
+ throw new errors.LogFileNotFound(startLogId)
151
133
  }
152
134
  }
153
135
 
154
136
  const stream = ws.createWebSocketStream(socket)
155
- pipeLogsStream(stream, req.log, startLogIndex)
137
+ runtimeApiClient.pipeLogsStream(stream, req.log, startLogId)
156
138
  })
157
139
 
158
- app.get('/logs/indexes', async () => {
159
- const logIndexes = await getLogIndexes()
160
- return { indexes: logIndexes }
140
+ app.get('/logs/indexes', async (req) => {
141
+ const returnAllIds = req.query.all === 'true'
142
+
143
+ const runtimesLogsIds = await runtimeApiClient.getLogIds()
144
+ if (returnAllIds) {
145
+ return runtimesLogsIds
146
+ }
147
+
148
+ if (runtimesLogsIds.length === 0) {
149
+ return []
150
+ }
151
+
152
+ return { indexes: runtimesLogsIds.at(-1).indexes }
161
153
  })
162
154
 
163
155
  app.get('/logs/all', async (req, reply) => {
164
- const logIndexes = await getLogIndexes()
165
- const startLogIndex = logIndexes.at(0)
166
- const endLogIndex = logIndexes.at(-1)
156
+ const runtimePID = parseInt(req.query.pid) || process.pid
157
+
158
+ const allLogIds = await runtimeApiClient.getLogIds()
159
+ const logsIds = allLogIds.find((logs) => logs.pid === runtimePID)
160
+ const startLogId = logsIds.indexes.at(0)
161
+ const endLogId = logsIds.indexes.at(-1)
167
162
 
168
163
  reply.hijack()
169
- pipeLogsStream(reply.raw, req.log, startLogIndex, endLogIndex)
164
+
165
+ runtimeApiClient.pipeLogsStream(
166
+ reply.raw,
167
+ req.log,
168
+ startLogId,
169
+ endLogId,
170
+ runtimePID
171
+ )
170
172
  })
171
173
 
172
174
  app.get('/logs/:id', async (req) => {
173
- const { id } = req.params
175
+ const logId = parseInt(req.params.id)
176
+ const runtimePID = parseInt(req.query.pid) || process.pid
174
177
 
175
- const logIndex = parseInt(id)
176
- const logIndexes = await getLogIndexes()
177
- if (!logIndexes.includes(logIndex)) {
178
- throw new errors.LogFileNotFound(logIndex)
178
+ const allLogIds = await runtimeApiClient.getLogIds()
179
+ const runtimeLogsIds = allLogIds.find((logs) => logs.pid === runtimePID)
180
+ if (!runtimeLogsIds || !runtimeLogsIds.indexes.includes(logId)) {
181
+ throw new errors.LogFileNotFound(logId)
179
182
  }
180
183
 
181
- const logFileStream = await getLogFileStream(logIndex)
184
+ const logFileStream = await runtimeApiClient.getLogFileStream(
185
+ logId,
186
+ runtimePID
187
+ )
182
188
  return logFileStream
183
189
  })
184
190
  }, { prefix: '/api/v1' })
@@ -186,35 +192,35 @@ async function createManagementApi (configManager, runtimeApiClient) {
186
192
  return app
187
193
  }
188
194
 
189
- async function startManagementApi (configManager, runtimeApiClient) {
195
+ async function startManagementApi (runtimeApiClient, configManager) {
190
196
  const runtimePID = process.pid
191
197
 
192
- let socketPath = null
193
- if (platform() === 'win32') {
194
- socketPath = '\\\\.\\pipe\\platformatic-' + runtimePID
195
- } else {
196
- socketPath = join(runtimeTmpDir, 'socket')
197
- }
198
-
199
198
  try {
200
- await rm(runtimeTmpDir, { recursive: true, force: true }).catch((err) => {
201
- if (err.code !== 'ENOENT') {
202
- throw new errors.FailedToUnlinkManagementApiSocket(err.message)
203
- }
204
- })
205
- await mkdir(runtimeTmpDir, { recursive: true })
199
+ const runtimePIDDir = join(PLATFORMATIC_TMP_DIR, runtimePID.toString())
200
+ if (platform() !== 'win32') {
201
+ await rm(runtimePIDDir, { recursive: true, force: true }).catch()
202
+ await mkdir(runtimePIDDir, { recursive: true })
203
+ }
206
204
 
207
- const managementApi = await createManagementApi(
208
- configManager,
209
- runtimeApiClient
210
- )
205
+ const runtimeLogsDir = getRuntimeLogsDir(configManager.dirname, process.pid)
206
+ await rm(runtimeLogsDir, { recursive: true, force: true }).catch()
207
+ await mkdir(runtimeLogsDir, { recursive: true })
211
208
 
212
- if (platform() !== 'win32') {
213
- managementApi.addHook('onClose', async () => {
214
- await rm(runtimeTmpDir, { recursive: true, force: true }).catch()
215
- })
209
+ let socketPath = null
210
+ if (platform() === 'win32') {
211
+ socketPath = '\\\\.\\pipe\\platformatic-' + runtimePID.toString()
212
+ } else {
213
+ socketPath = join(runtimePIDDir, 'socket')
216
214
  }
217
215
 
216
+ const managementApi = await createManagementApi(runtimeApiClient)
217
+
218
+ managementApi.addHook('onClose', async () => {
219
+ if (platform() !== 'win32') {
220
+ await rm(runtimePIDDir, { recursive: true, force: true }).catch()
221
+ }
222
+ })
223
+
218
224
  await managementApi.listen({ path: socketPath })
219
225
  return managementApi
220
226
  /* c8 ignore next 4 */
package/lib/schema.js CHANGED
@@ -10,6 +10,9 @@ const platformaticRuntimeSchema = {
10
10
  $schema: 'http://json-schema.org/draft-07/schema#',
11
11
  type: 'object',
12
12
  properties: {
13
+ $schema: {
14
+ type: 'string'
15
+ },
13
16
  autoload: {
14
17
  type: 'object',
15
18
  additionalProperties: false,
@@ -49,29 +52,6 @@ const platformaticRuntimeSchema = {
49
52
  },
50
53
  telemetry,
51
54
  server,
52
- services: {
53
- type: 'array',
54
- default: [],
55
- items: {
56
- type: 'object',
57
- required: ['id', 'path', 'config'],
58
- properties: {
59
- id: {
60
- type: 'string'
61
- },
62
- path: {
63
- type: 'string',
64
- resolvePath: true
65
- },
66
- config: {
67
- type: 'string'
68
- },
69
- useHttp: {
70
- type: 'boolean'
71
- }
72
- }
73
- }
74
- },
75
55
  entrypoint: {
76
56
  type: 'string'
77
57
  },
@@ -144,9 +124,6 @@ const platformaticRuntimeSchema = {
144
124
  }
145
125
  }
146
126
  },
147
- $schema: {
148
- type: 'string'
149
- },
150
127
  managementApi: {
151
128
  anyOf: [
152
129
  { type: 'boolean' },
@@ -194,6 +171,28 @@ const platformaticRuntimeSchema = {
194
171
  additionalProperties: false
195
172
  }
196
173
  ]
174
+ },
175
+ services: {
176
+ type: 'array',
177
+ items: {
178
+ type: 'object',
179
+ required: ['id', 'path', 'config'],
180
+ properties: {
181
+ id: {
182
+ type: 'string'
183
+ },
184
+ path: {
185
+ type: 'string',
186
+ resolvePath: true
187
+ },
188
+ config: {
189
+ type: 'string'
190
+ },
191
+ useHttp: {
192
+ type: 'boolean'
193
+ }
194
+ }
195
+ }
197
196
  }
198
197
  },
199
198
  anyOf: [
package/lib/start.js CHANGED
@@ -13,7 +13,7 @@ const { loadConfig } = require('./load-config')
13
13
  const { startManagementApi } = require('./management-api')
14
14
  const { startPrometheusServer } = require('./prom-server.js')
15
15
  const { parseInspectorOptions, wrapConfigInRuntimeConfig } = require('./config')
16
- const RuntimeApiClient = require('./api-client.js')
16
+ const { RuntimeApiClient, getRuntimeLogsDir } = require('./api-client.js')
17
17
  const errors = require('./errors')
18
18
  const pkg = require('../package.json')
19
19
 
@@ -41,6 +41,7 @@ async function buildRuntime (configManager, env = process.env) {
41
41
  }
42
42
 
43
43
  const dirname = configManager.dirname
44
+ const runtimeLogsDir = getRuntimeLogsDir(dirname, process.pid)
44
45
 
45
46
  // The configManager cannot be transferred to the worker, so remove it.
46
47
  delete config.configManager
@@ -49,7 +50,7 @@ async function buildRuntime (configManager, env = process.env) {
49
50
  /* c8 ignore next */
50
51
  execArgv: config.hotReload ? kWorkerExecArgv : [],
51
52
  transferList: config.loggingPort ? [config.loggingPort] : [],
52
- workerData: { config, dirname },
53
+ workerData: { config, dirname, runtimeLogsDir },
53
54
  env
54
55
  })
55
56
 
@@ -101,13 +102,14 @@ async function buildRuntime (configManager, env = process.env) {
101
102
 
102
103
  await once(worker, 'message') // plt:init
103
104
 
104
- const runtimeApiClient = new RuntimeApiClient(worker)
105
+ const runtimeApiClient = new RuntimeApiClient(
106
+ worker,
107
+ configManager,
108
+ runtimeLogsDir
109
+ )
105
110
 
106
111
  if (config.managementApi) {
107
- managementApi = await startManagementApi(
108
- configManager,
109
- runtimeApiClient
110
- )
112
+ managementApi = await startManagementApi(runtimeApiClient, configManager)
111
113
  runtimeApiClient.managementApi = managementApi
112
114
  runtimeApiClient.on('start', () => {
113
115
  runtimeApiClient.startCollectingMetrics()
package/lib/worker.js CHANGED
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const inspector = require('node:inspector')
4
- const { tmpdir } = require('node:os')
5
4
  const { register, createRequire } = require('node:module')
6
5
  const { isatty } = require('node:tty')
7
6
  const { join } = require('node:path')
@@ -19,8 +18,6 @@ const { MessagePortWritable } = require('./message-port-writable')
19
18
  const loadInterceptors = require('./interceptors')
20
19
  let loaderPort
21
20
 
22
- const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
23
-
24
21
  if (typeof register === 'function' && workerData.config.loaderFile) {
25
22
  const { port1, port2 } = new MessageChannel()
26
23
  register(workerData.config.loaderFile, {
@@ -69,7 +66,7 @@ function createLogger (config) {
69
66
  logsLimitCount = 1
70
67
  }
71
68
 
72
- const logsPath = join(PLATFORMATIC_TMP_DIR, process.pid.toString(), 'logs')
69
+ const logsPath = join(workerData.runtimeLogsDir, 'logs')
73
70
  const pinoRoll = pino.transport({
74
71
  target: 'pino-roll',
75
72
  options: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformatic/runtime",
3
- "version": "1.33.0",
3
+ "version": "1.35.1",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -20,7 +20,7 @@
20
20
  "@fastify/express": "^2.3.0",
21
21
  "@fastify/formbody": "^7.4.0",
22
22
  "@matteo.collina/tspl": "^0.1.1",
23
- "borp": "^0.10.0",
23
+ "borp": "^0.11.0",
24
24
  "c8": "^9.1.0",
25
25
  "execa": "^8.0.1",
26
26
  "express": "^4.18.3",
@@ -33,8 +33,8 @@
33
33
  "typescript": "^5.4.2",
34
34
  "undici-oidc-interceptor": "^0.5.0",
35
35
  "why-is-node-running": "^2.2.2",
36
- "@platformatic/sql-graphql": "1.33.0",
37
- "@platformatic/sql-mapper": "1.33.0"
36
+ "@platformatic/sql-mapper": "1.35.1",
37
+ "@platformatic/sql-graphql": "1.35.1"
38
38
  },
39
39
  "dependencies": {
40
40
  "ws": "^8.16.0",
@@ -62,13 +62,13 @@
62
62
  "tail-file-stream": "^0.1.0",
63
63
  "undici": "^6.9.0",
64
64
  "why-is-node-running": "^2.2.2",
65
- "@platformatic/composer": "1.33.0",
66
- "@platformatic/config": "1.33.0",
67
- "@platformatic/db": "1.33.0",
68
- "@platformatic/telemetry": "1.33.0",
69
- "@platformatic/service": "1.33.0",
70
- "@platformatic/generators": "1.33.0",
71
- "@platformatic/utils": "1.33.0"
65
+ "@platformatic/composer": "1.35.1",
66
+ "@platformatic/generators": "1.35.1",
67
+ "@platformatic/db": "1.35.1",
68
+ "@platformatic/config": "1.35.1",
69
+ "@platformatic/telemetry": "1.35.1",
70
+ "@platformatic/service": "1.35.1",
71
+ "@platformatic/utils": "1.35.1"
72
72
  },
73
73
  "standard": {
74
74
  "ignore": [
package/lib/logs.js DELETED
@@ -1,123 +0,0 @@
1
- 'use strict'
2
-
3
- const { tmpdir } = require('node:os')
4
- const { join } = require('node:path')
5
- const { createReadStream, watch } = require('node:fs')
6
- const { readdir } = require('node:fs/promises')
7
- const ts = require('tail-file-stream')
8
-
9
- const PLATFORMATIC_TMP_DIR = join(tmpdir(), 'platformatic', 'runtimes')
10
- const runtimeTmpDir = join(PLATFORMATIC_TMP_DIR, process.pid.toString())
11
-
12
- async function getLogFiles () {
13
- const runtimeTmpFiles = await readdir(runtimeTmpDir)
14
- const runtimeLogFiles = runtimeTmpFiles
15
- .filter((file) => file.startsWith('logs'))
16
- .sort((log1, log2) => {
17
- const index1 = parseInt(log1.slice('logs.'.length))
18
- const index2 = parseInt(log2.slice('logs.'.length))
19
- return index1 - index2
20
- })
21
- return runtimeLogFiles
22
- }
23
-
24
- async function pipeLogsStream (writableStream, logger, startLogIndex, endLogIndex) {
25
- endLogIndex = endLogIndex || Infinity
26
-
27
- const runtimeLogFiles = await getLogFiles()
28
- if (runtimeLogFiles.length === 0) {
29
- writableStream.end()
30
- return
31
- }
32
-
33
- let latestFileIndex = parseInt(runtimeLogFiles.at(-1).slice('logs.'.length))
34
-
35
- let waiting = false
36
- let fileStream = null
37
- let fileIndex = startLogIndex ?? latestFileIndex
38
-
39
- const watcher = watch(runtimeTmpDir, async (event, filename) => {
40
- if (event === 'rename' && filename.startsWith('logs')) {
41
- const logFileIndex = parseInt(filename.slice('logs.'.length))
42
- if (logFileIndex > latestFileIndex) {
43
- latestFileIndex = logFileIndex
44
- if (waiting) {
45
- streamLogFile(++fileIndex)
46
- }
47
- }
48
- }
49
- }).unref()
50
-
51
- const streamLogFile = () => {
52
- if (fileIndex > endLogIndex) {
53
- writableStream.end()
54
- return
55
- }
56
-
57
- const fileName = 'logs.' + fileIndex
58
- const filePath = join(runtimeTmpDir, fileName)
59
-
60
- const prevFileStream = fileStream
61
-
62
- fileStream = ts.createReadStream(filePath)
63
- fileStream.pipe(writableStream, { end: false })
64
-
65
- if (prevFileStream) {
66
- prevFileStream.unpipe(writableStream)
67
- prevFileStream.destroy()
68
- }
69
-
70
- fileStream.on('error', (err) => {
71
- logger.error(err, 'Error streaming log file')
72
- fileStream.destroy()
73
- watcher.close()
74
- writableStream.end()
75
- })
76
-
77
- fileStream.on('data', () => {
78
- waiting = false
79
- })
80
-
81
- fileStream.on('eof', () => {
82
- if (fileIndex >= endLogIndex) {
83
- writableStream.end()
84
- return
85
- }
86
- if (latestFileIndex > fileIndex) {
87
- streamLogFile(++fileIndex)
88
- } else {
89
- waiting = true
90
- }
91
- })
92
-
93
- return fileStream
94
- }
95
-
96
- streamLogFile(fileIndex)
97
-
98
- writableStream.on('close', () => {
99
- watcher.close()
100
- fileStream.destroy()
101
- })
102
- writableStream.on('error', () => {
103
- watcher.close()
104
- fileStream.destroy()
105
- })
106
- }
107
-
108
- async function getLogIndexes () {
109
- const runtimeLogFiles = await getLogFiles()
110
- return runtimeLogFiles
111
- .map((file) => parseInt(file.slice('logs.'.length)))
112
- }
113
-
114
- async function getLogFileStream (logFileIndex) {
115
- const filePath = join(runtimeTmpDir, `logs.${logFileIndex}`)
116
- return createReadStream(filePath)
117
- }
118
-
119
- module.exports = {
120
- pipeLogsStream,
121
- getLogFileStream,
122
- getLogIndexes
123
- }