pinokiod 7.5.25 → 7.5.26

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.
@@ -13,6 +13,7 @@ const Environment = require("../environment")
13
13
  const Util = require('../util')
14
14
  const ShellRunTemplate = require('./shell_run_template')
15
15
  const PluginSources = require('../plugin_sources')
16
+ const AppLogSessions = require('../app_log_sessions')
16
17
 
17
18
  const escapeHtml = (value) => String(value || "")
18
19
  .replace(/&/g, "&")
@@ -36,6 +37,7 @@ class Api {
36
37
  this.mods = {}
37
38
  this.child_procs = {}
38
39
  this.resolved_actions = {}
40
+ this.logSessions = new AppLogSessions({ kernel })
39
41
  this.lproxy = new Lproxy()
40
42
  }
41
43
  async launcher_path(name) {
@@ -640,6 +642,14 @@ class Api {
640
642
  internal_completion: !!(options && options.internal_completion)
641
643
  })
642
644
  }
645
+ if (this.logSessions) {
646
+ await this.logSessions.finishRun({
647
+ id: req.params.id,
648
+ path: requestPath
649
+ }, {
650
+ internal_completion: !!(options && options.internal_completion)
651
+ })
652
+ }
643
653
  this.ondata({
644
654
  id: req.params.id || requestPath,
645
655
  type: "disconnect"
@@ -1386,7 +1396,7 @@ class Api {
1386
1396
  console.log(msg)
1387
1397
  } else {
1388
1398
  // 11. actually make the rpc call
1389
- result = await this.run(resolved.method, rpc, (stream, type) => {
1399
+ const runEndpoint = () => this.run(resolved.method, rpc, (stream, type) => {
1390
1400
  let m = {
1391
1401
  id: request.id || request.path,
1392
1402
  caller: request.caller,
@@ -1410,6 +1420,9 @@ class Api {
1410
1420
  // }
1411
1421
  this.ondata(m)
1412
1422
  })
1423
+ result = this.logSessions
1424
+ ? await this.logSessions.withRunContext(request, runEndpoint)
1425
+ : await runEndpoint()
1413
1426
  }
1414
1427
 
1415
1428
 
@@ -1423,6 +1436,9 @@ class Api {
1423
1436
  rpc,
1424
1437
  rawrpc
1425
1438
  })
1439
+ if (this.logSessions) {
1440
+ await this.logSessions.finishRun(request, { internal_completion: true })
1441
+ }
1426
1442
 
1427
1443
  // if there's an error, set the PINOKIO_SCRIPT_DEFAULT to false
1428
1444
 
@@ -1604,6 +1620,9 @@ class Api {
1604
1620
  rpc,
1605
1621
  rawrpc
1606
1622
  })
1623
+ if (this.logSessions) {
1624
+ await this.logSessions.finishRun(request, { internal_completion: true })
1625
+ }
1607
1626
  return // halt when there's an error
1608
1627
  }
1609
1628
  }
@@ -1657,6 +1676,9 @@ class Api {
1657
1676
  let response = await this.step(request, rawrpc, input, step, total, args)
1658
1677
  if (response) {
1659
1678
  if (response.cancelled) {
1679
+ if (this.logSessions) {
1680
+ await this.logSessions.finishRun(response.request || request)
1681
+ }
1660
1682
  return
1661
1683
  }
1662
1684
  if (response.rawrpc) {
@@ -1693,6 +1715,9 @@ class Api {
1693
1715
  if (this.kernel && typeof this.kernel.markAppLaunchFailed === "function") {
1694
1716
  this.kernel.markAppLaunchFailed(request.path, e)
1695
1717
  }
1718
+ if (this.logSessions) {
1719
+ await this.logSessions.finishRun(request, { internal_completion: true })
1720
+ }
1696
1721
  ondata({ raw: e.toString() })
1697
1722
  }
1698
1723
  }, concurrency)
@@ -1992,6 +2017,9 @@ class Api {
1992
2017
  return
1993
2018
  }
1994
2019
 
2020
+ if (this.logSessions) {
2021
+ await this.logSessions.startRun(request)
2022
+ }
1995
2023
  this.queue(request, steps[0], initialPayload, 0, steps.length, cwd, initialPayload)
1996
2024
 
1997
2025
  } else {
@@ -0,0 +1,424 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const crypto = require('crypto')
4
+ const { AsyncLocalStorage } = require('async_hooks')
5
+ const Environment = require('./environment')
6
+
7
+ class AppLogSessions {
8
+ constructor({ kernel, now = () => new Date().toISOString(), randomHex = () => crypto.randomBytes(3).toString('hex') }) {
9
+ this.kernel = kernel
10
+ this.now = now
11
+ this.randomHex = randomHex
12
+ this.context = new AsyncLocalStorage()
13
+ this.reservations = new Map()
14
+ this.routineSessions = new Map()
15
+ this.sessions = new Map()
16
+ this.activeRunsByKey = new Map()
17
+ this.indexQueues = new Map()
18
+ }
19
+
20
+ apiRoot() {
21
+ if (this.kernel && typeof this.kernel.path === 'function') {
22
+ return path.resolve(this.kernel.path('api'))
23
+ }
24
+ return path.resolve(this.kernel.homedir || '', 'api')
25
+ }
26
+
27
+ toPosix(value) {
28
+ return String(value || '').split(path.sep).filter(Boolean).join('/')
29
+ }
30
+
31
+ isPathWithin(parentPath, childPath) {
32
+ const relative = path.relative(path.resolve(parentPath), path.resolve(childPath))
33
+ return !relative || (!relative.startsWith('..') && !path.isAbsolute(relative))
34
+ }
35
+
36
+ sessionDir(appRoot) {
37
+ return path.resolve(appRoot, 'logs', 'sessions')
38
+ }
39
+
40
+ indexPath(appRoot) {
41
+ return path.resolve(this.sessionDir(appRoot), 'index.json')
42
+ }
43
+
44
+ manifestPath(appRoot, sessionId) {
45
+ if (!this.validSessionId(sessionId)) {
46
+ return null
47
+ }
48
+ return path.resolve(this.sessionDir(appRoot), `${sessionId}.json`)
49
+ }
50
+
51
+ sessionKey(appRoot, sessionId) {
52
+ return `${path.resolve(appRoot)}\u0000${sessionId}`
53
+ }
54
+
55
+ reservationKey(target) {
56
+ return `${path.resolve(target.appRoot)}\u0000${path.resolve(target.scriptPath)}`
57
+ }
58
+
59
+ validSessionId(sessionId) {
60
+ return typeof sessionId === 'string' && /^[A-Za-z0-9._-]+$/.test(sessionId)
61
+ }
62
+
63
+ async resolveAppRoot(appRoot) {
64
+ if (this.kernel && typeof this.kernel.exists === 'function') {
65
+ try {
66
+ const root = await Environment.get_root({ path: appRoot }, this.kernel)
67
+ if (root && root.root) {
68
+ return path.resolve(root.root)
69
+ }
70
+ } catch (_) {}
71
+ }
72
+ return appRoot
73
+ }
74
+
75
+ async resolveScript(scriptPath) {
76
+ if (typeof scriptPath !== 'string' || !scriptPath.trim()) {
77
+ return null
78
+ }
79
+ const absolute = path.resolve(scriptPath.split('?')[0])
80
+ const apiRoot = this.apiRoot()
81
+ if (!this.isPathWithin(apiRoot, absolute)) {
82
+ return null
83
+ }
84
+ const relative = path.relative(apiRoot, absolute)
85
+ const parts = relative.split(path.sep).filter(Boolean)
86
+ if (parts.length < 2 || !parts[0]) {
87
+ return null
88
+ }
89
+ const appRoot = await this.resolveAppRoot(path.resolve(apiRoot, parts[0]))
90
+ return {
91
+ appRoot,
92
+ scriptPath: absolute,
93
+ script: this.toPosix(parts.slice(1).join(path.sep))
94
+ }
95
+ }
96
+
97
+ requestKeys(requestOrRun) {
98
+ const keys = []
99
+ if (!requestOrRun || typeof requestOrRun !== 'object') {
100
+ return keys
101
+ }
102
+ for (const key of [requestOrRun.id, requestOrRun.path]) {
103
+ if (typeof key === 'string' && key.trim() && !keys.includes(key)) {
104
+ keys.push(key)
105
+ }
106
+ }
107
+ return keys
108
+ }
109
+
110
+ createSessionId(timestamp) {
111
+ const safeTime = String(timestamp || this.now()).replace(/[^A-Za-z0-9._-]/g, '-')
112
+ return `${safeTime}-${this.randomHex()}`
113
+ }
114
+
115
+ async readJson(filePath) {
116
+ try {
117
+ return JSON.parse(await fs.promises.readFile(filePath, 'utf8'))
118
+ } catch (_) {
119
+ return null
120
+ }
121
+ }
122
+
123
+ async writeJson(filePath, value) {
124
+ try {
125
+ await fs.promises.mkdir(path.dirname(filePath), { recursive: true })
126
+ await fs.promises.writeFile(filePath, JSON.stringify(value, null, 2))
127
+ } catch (_) {}
128
+ }
129
+
130
+ async loadIndex(appRoot) {
131
+ const parsed = await this.readJson(this.indexPath(appRoot))
132
+ if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.sessions)) {
133
+ return { version: 1, latest_session: null, sessions: [] }
134
+ }
135
+ return {
136
+ version: 1,
137
+ latest_session: this.validSessionId(parsed.latest_session) ? parsed.latest_session : null,
138
+ sessions: parsed.sessions
139
+ .filter((session) => session && typeof session === 'object' && this.validSessionId(session.id))
140
+ .map((session) => ({
141
+ id: session.id,
142
+ created_at: typeof session.created_at === 'string' ? session.created_at : null,
143
+ updated_at: typeof session.updated_at === 'string' ? session.updated_at : null,
144
+ runs: Array.isArray(session.runs) ? session.runs.filter((run) => typeof run === 'string') : []
145
+ }))
146
+ }
147
+ }
148
+
149
+ async loadSession(appRoot, sessionId) {
150
+ if (!this.validSessionId(sessionId)) {
151
+ return null
152
+ }
153
+ const cacheKey = this.sessionKey(appRoot, sessionId)
154
+ if (this.sessions.has(cacheKey)) {
155
+ return this.sessions.get(cacheKey)
156
+ }
157
+ const manifestPath = this.manifestPath(appRoot, sessionId)
158
+ const parsed = manifestPath ? await this.readJson(manifestPath) : null
159
+ if (!parsed || typeof parsed !== 'object' || parsed.id !== sessionId || !Array.isArray(parsed.runs)) {
160
+ return null
161
+ }
162
+ parsed.appRoot = appRoot
163
+ this.sessions.set(cacheKey, parsed)
164
+ return parsed
165
+ }
166
+
167
+ async updateIndex(appRoot, updater) {
168
+ const previous = this.indexQueues.get(appRoot) || Promise.resolve()
169
+ const next = previous.catch(() => {}).then(async () => {
170
+ const index = await this.loadIndex(appRoot)
171
+ const updated = updater(index) || index
172
+ await this.writeJson(this.indexPath(appRoot), updated)
173
+ })
174
+ this.indexQueues.set(appRoot, next.catch(() => {}))
175
+ return next
176
+ }
177
+
178
+ async persistSession(session, options = {}) {
179
+ if (!session || !session.appRoot || !this.validSessionId(session.id)) {
180
+ return
181
+ }
182
+ const manifest = {
183
+ version: 1,
184
+ id: session.id,
185
+ created_at: session.created_at,
186
+ updated_at: session.updated_at,
187
+ runs: session.runs.map((run) => ({
188
+ script: run.script,
189
+ started_at: run.started_at || null,
190
+ ended_at: run.ended_at || null,
191
+ logs: Array.isArray(run.logs) ? run.logs : []
192
+ }))
193
+ }
194
+ await this.writeJson(this.manifestPath(session.appRoot, session.id), manifest)
195
+
196
+ const summary = {
197
+ id: session.id,
198
+ created_at: session.created_at,
199
+ updated_at: session.updated_at,
200
+ runs: session.runs.map((run) => run.script).filter(Boolean)
201
+ }
202
+ await this.updateIndex(session.appRoot, (index) => {
203
+ const promote = !!options.promote || !index.latest_session
204
+ if (promote) {
205
+ const remaining = index.sessions.filter((entry) => entry.id !== session.id)
206
+ index.latest_session = session.id
207
+ index.sessions = [summary, ...remaining]
208
+ return index
209
+ }
210
+ let replaced = false
211
+ index.sessions = index.sessions.map((entry) => {
212
+ if (entry.id === session.id) {
213
+ replaced = true
214
+ return summary
215
+ }
216
+ return entry
217
+ })
218
+ if (!replaced) {
219
+ index.sessions.push(summary)
220
+ }
221
+ return index
222
+ })
223
+ }
224
+
225
+ async createSession(target, timestamp) {
226
+ const session = {
227
+ version: 1,
228
+ id: this.createSessionId(timestamp),
229
+ appRoot: target.appRoot,
230
+ created_at: timestamp,
231
+ updated_at: timestamp,
232
+ runs: []
233
+ }
234
+ this.sessions.set(this.sessionKey(session.appRoot, session.id), session)
235
+ return session
236
+ }
237
+
238
+ clearReservations(appRoot, sessionId = '') {
239
+ for (const [key, reservation] of this.reservations.entries()) {
240
+ if (reservation.appRoot !== appRoot) {
241
+ continue
242
+ }
243
+ if (sessionId && reservation.session_id !== sessionId) {
244
+ continue
245
+ }
246
+ this.reservations.delete(key)
247
+ }
248
+ if (!sessionId) {
249
+ this.routineSessions.delete(appRoot)
250
+ return
251
+ }
252
+ if (this.routineSessions.get(appRoot) === sessionId) {
253
+ this.routineSessions.delete(appRoot)
254
+ }
255
+ }
256
+
257
+ async reserveLaunch(scriptPathOrRequest, options = {}) {
258
+ const scriptPath = typeof scriptPathOrRequest === 'object' && scriptPathOrRequest
259
+ ? scriptPathOrRequest.path || scriptPathOrRequest.uri
260
+ : scriptPathOrRequest
261
+ const target = await this.resolveScript(scriptPath)
262
+ if (!target) {
263
+ return null
264
+ }
265
+
266
+ let session = null
267
+ const routineSessionId = this.routineSessions.get(target.appRoot)
268
+ if (this.validSessionId(routineSessionId)) {
269
+ session = await this.loadSession(target.appRoot, routineSessionId)
270
+ }
271
+ if (!session && options.existingRoutineOnly) {
272
+ return null
273
+ }
274
+ const reservation = {
275
+ session_id: session ? session.id : '',
276
+ appRoot: target.appRoot,
277
+ scriptPath: target.scriptPath,
278
+ script: target.script
279
+ }
280
+ this.reservations.set(this.reservationKey(target), reservation)
281
+ return reservation
282
+ }
283
+
284
+ async startRun(request) {
285
+ const target = await this.resolveScript(request && request.path)
286
+ if (!target) {
287
+ return null
288
+ }
289
+ const context = this.context.getStore()
290
+ const reservationKey = this.reservationKey(target)
291
+ const reservation = this.reservations.get(reservationKey)
292
+ let session = null
293
+ let promote = false
294
+ let routine = false
295
+
296
+ if (context && context.appRoot === target.appRoot && this.validSessionId(context.session_id)) {
297
+ session = await this.loadSession(target.appRoot, context.session_id)
298
+ }
299
+
300
+ if (!session && reservation) {
301
+ this.reservations.delete(reservationKey)
302
+ if (this.validSessionId(reservation.session_id)) {
303
+ session = await this.loadSession(target.appRoot, reservation.session_id)
304
+ }
305
+ if (!session) {
306
+ const timestamp = this.now()
307
+ session = await this.createSession(target, timestamp)
308
+ promote = true
309
+ }
310
+ routine = true
311
+ }
312
+
313
+ if (!session) {
314
+ this.clearReservations(target.appRoot)
315
+ const timestamp = this.now()
316
+ session = await this.createSession(target, timestamp)
317
+ promote = true
318
+ }
319
+ if (routine) {
320
+ this.routineSessions.set(target.appRoot, session.id)
321
+ }
322
+
323
+ const startedAt = session.runs.length === 0 ? session.created_at : this.now()
324
+ const run = {
325
+ id: `${session.id}:${session.runs.length}`,
326
+ session_id: session.id,
327
+ appRoot: target.appRoot,
328
+ scriptPath: target.scriptPath,
329
+ script: target.script,
330
+ started_at: startedAt,
331
+ ended_at: null,
332
+ logs: []
333
+ }
334
+ session.updated_at = startedAt
335
+ session.runs.push(run)
336
+ for (const key of this.requestKeys({ ...request, path: target.scriptPath })) {
337
+ this.activeRunsByKey.set(key, run)
338
+ }
339
+ await this.persistSession(session, { promote })
340
+ return run
341
+ }
342
+
343
+ activeRun(requestOrRun) {
344
+ if (requestOrRun && requestOrRun.session_id && requestOrRun.scriptPath) {
345
+ return requestOrRun
346
+ }
347
+ for (const key of this.requestKeys(requestOrRun)) {
348
+ const run = this.activeRunsByKey.get(key)
349
+ if (run) {
350
+ return run
351
+ }
352
+ }
353
+ return null
354
+ }
355
+
356
+ withRunContext(requestOrRun, fn) {
357
+ const run = this.activeRun(requestOrRun)
358
+ if (!run || typeof fn !== 'function') {
359
+ return typeof fn === 'function' ? fn() : undefined
360
+ }
361
+ return this.context.run({
362
+ session_id: run.session_id,
363
+ appRoot: run.appRoot
364
+ }, fn)
365
+ }
366
+
367
+ async finishRun(requestOrRun, options = {}) {
368
+ const run = this.activeRun(requestOrRun)
369
+ if (!run || run.ended_at) {
370
+ return
371
+ }
372
+ const session = await this.loadSession(run.appRoot, run.session_id)
373
+ if (!session) {
374
+ return
375
+ }
376
+ const endedAt = this.now()
377
+ run.ended_at = endedAt
378
+ session.updated_at = endedAt
379
+ for (const key of this.requestKeys({ ...requestOrRun, path: run.scriptPath })) {
380
+ this.activeRunsByKey.delete(key)
381
+ }
382
+ await this.persistSession(session)
383
+
384
+ if (!options || !options.internal_completion) {
385
+ this.clearReservations(run.appRoot, session.id)
386
+ }
387
+ }
388
+
389
+ async recordLogFile({ scriptPath, logFile, run: capturedRun = null }) {
390
+ const target = await this.resolveScript(scriptPath)
391
+ if (!target || typeof logFile !== 'string' || !logFile.trim()) {
392
+ return
393
+ }
394
+ const run = capturedRun
395
+ && capturedRun.session_id
396
+ && capturedRun.appRoot === target.appRoot
397
+ && capturedRun.scriptPath === target.scriptPath
398
+ ? capturedRun
399
+ : this.activeRunsByKey.get(target.scriptPath)
400
+ if (!run) {
401
+ return
402
+ }
403
+ const logsRoot = path.resolve(target.appRoot, 'logs')
404
+ const sessionsRoot = this.sessionDir(target.appRoot)
405
+ const absoluteLog = path.resolve(logFile)
406
+ if (!this.isPathWithin(logsRoot, absoluteLog) || this.isPathWithin(sessionsRoot, absoluteLog)) {
407
+ return
408
+ }
409
+ if (path.basename(absoluteLog) === 'latest') {
410
+ return
411
+ }
412
+ const relativeLog = this.toPosix(path.relative(target.appRoot, absoluteLog))
413
+ if (!run.logs.some((entry) => entry.path === relativeLog)) {
414
+ run.logs.push({ path: relativeLog })
415
+ const session = await this.loadSession(run.appRoot, run.session_id)
416
+ if (session) {
417
+ session.updated_at = this.now()
418
+ await this.persistSession(session)
419
+ }
420
+ }
421
+ }
422
+ }
423
+
424
+ module.exports = AppLogSessions
package/kernel/index.js CHANGED
@@ -311,9 +311,6 @@ class Kernel {
311
311
  }
312
312
  }
313
313
  }
314
- if (!filtered.includes(".")) {
315
- filtered.push(".")
316
- }
317
314
  config.dns[key] = filtered
318
315
 
319
316
  }
@@ -32,10 +32,12 @@ class LocalhostStaticRouter extends Processor {
32
32
  continue
33
33
  }
34
34
  const hasIndex = fs.existsSync(indexPath)
35
- const fileServerOptions = {}
36
- if (hasIndex) {
37
- fileServerOptions.index_names = ["index.html"]
35
+ if (!hasIndex) {
36
+ delete this.router.rewrite_mapping[api_name]
37
+ continue
38
38
  }
39
+ const fileServerOptions = {}
40
+ fileServerOptions.index_names = ["index.html"]
39
41
  const effectiveFileServerOptions = Object.keys(fileServerOptions).length ? { ...fileServerOptions } : undefined
40
42
  for(let domain in config.dns) {
41
43
  let localhost_match
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "7.5.25",
3
+ "version": "7.5.26",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {