pinokiod 7.3.15 → 7.4.0

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.
Files changed (48) hide show
  1. package/kernel/api/index.js +111 -15
  2. package/kernel/api/script/index.js +10 -0
  3. package/kernel/autolaunch.js +111 -0
  4. package/kernel/environment.js +89 -1
  5. package/kernel/git.js +9 -19
  6. package/kernel/index.js +142 -43
  7. package/kernel/launch_requirements.js +1115 -0
  8. package/kernel/ready.js +231 -0
  9. package/kernel/script.js +16 -0
  10. package/kernel/shells.js +9 -1
  11. package/kernel/workspace_status.js +111 -45
  12. package/package.json +1 -1
  13. package/server/autolaunch.js +725 -0
  14. package/server/index.js +244 -411
  15. package/server/views/app.ejs +256 -152
  16. package/server/views/autolaunch.ejs +363 -75
  17. package/server/views/index.ejs +550 -26
  18. package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
  19. package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
  20. package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
  21. package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
  22. package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
  23. package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
  24. package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
  25. package/server/views/partials/home_action_modal.ejs +4 -1
  26. package/server/views/partials/launch_requirements_status_client.ejs +271 -0
  27. package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
  28. package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
  29. package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
  30. package/server/views/terminal.ejs +196 -2
  31. package/test/home-autolaunch-live-ui.test.js +455 -0
  32. package/test/launch-requirements-browser.test.js +579 -0
  33. package/test/launch-requirements-contract-coverage.test.js +627 -0
  34. package/test/launch-requirements-real-browser.js +625 -0
  35. package/test/launch-requirements-status-client.test.js +132 -0
  36. package/test/launch-requirements.test.js +1806 -0
  37. package/test/launch-settings-ui.test.js +370 -0
  38. package/test/ready-state.test.js +49 -0
  39. package/test/server-autolaunch.test.js +1052 -0
  40. package/test/startup-git-index-benchmark.js +409 -0
  41. package/test/startup-git-index-browser.js +320 -0
  42. package/test/startup-git-index-performance.test.js +380 -0
  43. package/test/startup-git-index-refactor.test.js +450 -0
  44. package/test/startup-git-index-route.test.js +588 -0
  45. package/test/universal-launcher.smoke.spec.js +10 -9
  46. package/test/workspace-gitignore-benchmark.js +815 -0
  47. package/test/workspace-gitignore-path-scoped.test.js +256 -0
  48. package/spec/INSTRUCTION_SYNC.md +0 -432
@@ -0,0 +1,1115 @@
1
+ const path = require('path')
2
+ const Environment = require('./environment')
3
+
4
+ class LaunchRequirements {
5
+ constructor(kernel) {
6
+ this.kernel = kernel
7
+ this.status = {
8
+ targets: {},
9
+ startup: {
10
+ running: false,
11
+ apps: {}
12
+ }
13
+ }
14
+ this.cancelled = new Set()
15
+ this.inflightStarts = new Map()
16
+ this.activeLaunches = new Map()
17
+ this.activeLaunchCounter = 0
18
+ }
19
+ normalizeAppId(value) {
20
+ return this.kernel && typeof this.kernel.normalizeAppId === "function"
21
+ ? this.kernel.normalizeAppId(value)
22
+ : ""
23
+ }
24
+ blocked(reason, extra = {}) {
25
+ const error = new Error(reason)
26
+ error.blocked_reason = reason
27
+ Object.assign(error, extra)
28
+ return error
29
+ }
30
+ cancelledError(appId) {
31
+ const error = new Error("Launch cancelled")
32
+ error.cancelled = true
33
+ error.app_id = appId
34
+ return error
35
+ }
36
+ controlResult(action, extra = {}) {
37
+ return {
38
+ action,
39
+ ...extra
40
+ }
41
+ }
42
+ blockedResult(errorOrReason, extra = {}) {
43
+ const reason = errorOrReason && errorOrReason.blocked_reason
44
+ ? errorOrReason.blocked_reason
45
+ : String(errorOrReason || "Launch requirements blocked")
46
+ return this.controlResult("blocked", {
47
+ reason,
48
+ blocked_reason: reason,
49
+ app_id: errorOrReason && errorOrReason.app_id ? errorOrReason.app_id : extra.app_id,
50
+ ...extra
51
+ })
52
+ }
53
+ cancelledResult(appId) {
54
+ const id = this.normalizeAppId(appId)
55
+ if (id) {
56
+ this.clearAttemptStartupStatus(id)
57
+ }
58
+ return this.controlResult("cancelled", { app_id: id || appId })
59
+ }
60
+ handledResult(appId, status) {
61
+ return this.controlResult("handled", {
62
+ app_id: appId,
63
+ state: status && status.state ? status.state : ""
64
+ })
65
+ }
66
+ resultFromError(error, fallbackAppId = "") {
67
+ if (error && error.cancelled) {
68
+ return this.cancelledResult(error.app_id || fallbackAppId)
69
+ }
70
+ if (error && error.blocked_reason) {
71
+ return this.blockedResult(error, { app_id: error.app_id || fallbackAppId })
72
+ }
73
+ return null
74
+ }
75
+ statusChannel(appId) {
76
+ const id = this.normalizeAppId(appId)
77
+ return id ? `kernel.launch_requirements:${id}` : ""
78
+ }
79
+ emitStatus(appId) {
80
+ const channel = this.statusChannel(appId)
81
+ if (!channel || !this.kernel || !this.kernel.api || typeof this.kernel.api.ondata !== "function") {
82
+ return
83
+ }
84
+ this.kernel.api.ondata({
85
+ id: channel,
86
+ kernel: true,
87
+ type: "launch.requirements",
88
+ data: {
89
+ status: this.getStatus(appId)
90
+ }
91
+ })
92
+ }
93
+ activePreparationStates() {
94
+ return new Set(["pending", "waiting", "starting", "blocked"])
95
+ }
96
+ beginLaunchOperation(launchPath, meta = {}) {
97
+ if (!launchPath || typeof launchPath !== "string") {
98
+ return null
99
+ }
100
+ const key = path.resolve(launchPath)
101
+ const token = {
102
+ id: ++this.activeLaunchCounter,
103
+ key,
104
+ started_at: Date.now(),
105
+ ...meta
106
+ }
107
+ if (!this.activeLaunches.has(key)) {
108
+ this.activeLaunches.set(key, new Set())
109
+ }
110
+ this.activeLaunches.get(key).add(token)
111
+ return token
112
+ }
113
+ endLaunchOperation(token) {
114
+ if (!token || !token.key || !this.activeLaunches.has(token.key)) {
115
+ return false
116
+ }
117
+ const active = this.activeLaunches.get(token.key)
118
+ active.delete(token)
119
+ if (active.size === 0) {
120
+ this.activeLaunches.delete(token.key)
121
+ }
122
+ return true
123
+ }
124
+ hasActiveLaunch(config, owner) {
125
+ const key = this.inflightKey(config)
126
+ if (!key || !this.activeLaunches.has(key)) {
127
+ return false
128
+ }
129
+ const active = this.activeLaunches.get(key)
130
+ for (const token of active) {
131
+ if (token !== owner) {
132
+ return true
133
+ }
134
+ }
135
+ return false
136
+ }
137
+ startupHomeStatus() {
138
+ if (!this.status.startup || !this.status.startup.apps) {
139
+ this.status.startup = {
140
+ running: false,
141
+ apps: {}
142
+ }
143
+ }
144
+ return this.status.startup
145
+ }
146
+ replaceStartupHomeStatus(status) {
147
+ this.status.startup = {
148
+ running: !!(status && status.running),
149
+ ...(status || {}),
150
+ apps: status && status.apps ? status.apps : {}
151
+ }
152
+ return this.startupHomeStatus()
153
+ }
154
+ markStartupStarted(appId) {
155
+ const id = this.normalizeAppId(appId)
156
+ const startupStatus = this.startupHomeStatus()
157
+ const status = id && startupStatus.apps ? startupStatus.apps[id] : null
158
+ if (!status || status.state === "blocked") {
159
+ return
160
+ }
161
+ startupStatus.apps[id] = {
162
+ ...this.stripProgress(status),
163
+ state: "starting",
164
+ started_at: status.started_at || Date.now(),
165
+ waiting_for: [],
166
+ process_started: true
167
+ }
168
+ }
169
+ markStartupReady(appId) {
170
+ const id = this.normalizeAppId(appId)
171
+ const startupStatus = this.startupHomeStatus()
172
+ const status = id && startupStatus.apps ? startupStatus.apps[id] : null
173
+ if (!status || status.state === "blocked") {
174
+ return
175
+ }
176
+ startupStatus.apps[id] = {
177
+ ...this.stripProgress(status),
178
+ state: "ready",
179
+ ready_at: status.ready_at || Date.now(),
180
+ waiting_for: [],
181
+ process_started: true
182
+ }
183
+ }
184
+ markStartupStopped(appId) {
185
+ const id = this.normalizeAppId(appId)
186
+ const startupStatus = this.startupHomeStatus()
187
+ const status = id && startupStatus.apps ? startupStatus.apps[id] : null
188
+ if (!status) {
189
+ return
190
+ }
191
+ delete startupStatus.apps[id]
192
+ }
193
+ activeTargetStatus(appId, launchPath) {
194
+ const id = this.normalizeAppId(appId)
195
+ const target = id ? this.status.targets[id] : null
196
+ const activeStates = this.activePreparationStates()
197
+ if (!target || !activeStates.has(target.state) || !launchPath || !target.launch_path) {
198
+ return null
199
+ }
200
+ return path.resolve(target.launch_path) === path.resolve(launchPath) ? target : null
201
+ }
202
+ hasRuntimeForLaunchPath(launchPath) {
203
+ const targetInfo = this.targetForLaunchPath(launchPath)
204
+ if (!targetInfo || !targetInfo.appId) {
205
+ return false
206
+ }
207
+ const appId = targetInfo.appId
208
+ if (this.status.targets[appId]) {
209
+ return true
210
+ }
211
+ for (const targetId of Object.keys(this.status.targets)) {
212
+ if (this.referencesApp(this.status.targets[targetId], appId)) {
213
+ return true
214
+ }
215
+ }
216
+ const startupApps = this.status.startup && this.status.startup.apps ? this.status.startup.apps : {}
217
+ if (startupApps[appId]) {
218
+ return true
219
+ }
220
+ return Object.keys(startupApps).some((statusId) => this.referencesApp(startupApps[statusId], appId))
221
+ }
222
+ async appMap() {
223
+ const apps = this.kernel && this.kernel.api && typeof this.kernel.api.listApps === "function"
224
+ ? await this.kernel.api.listApps()
225
+ : []
226
+ return new Map(apps
227
+ .filter((app) => app && app.id)
228
+ .map((app) => [app.id, app]))
229
+ }
230
+ async configFor(appId, apps, override = {}) {
231
+ const id = this.normalizeAppId(appId)
232
+ if (!id || !apps.has(id)) {
233
+ return null
234
+ }
235
+ const app = apps.get(id)
236
+ const appRoot = path.resolve(this.kernel.api.userdir, id)
237
+ const env = await Environment.get(appRoot, this.kernel)
238
+ const configuredLaunchScript = typeof env.PINOKIO_SCRIPT_AUTOLAUNCH === "string"
239
+ ? env.PINOKIO_SCRIPT_AUTOLAUNCH.trim()
240
+ : ""
241
+ const launchScript = typeof override.launchScript === "string"
242
+ ? override.launchScript.trim()
243
+ : configuredLaunchScript
244
+ const launchPath = typeof override.launchPath === "string" && override.launchPath
245
+ ? path.resolve(override.launchPath)
246
+ : (launchScript ? path.resolve(appRoot, launchScript) : "")
247
+ const dependencies = Environment.getScriptRequirements(env)
248
+ .map((dependencyId) => this.normalizeAppId(dependencyId))
249
+ .filter((dependencyId, index, list) => dependencyId && dependencyId !== id && list.indexOf(dependencyId) === index)
250
+ return {
251
+ id,
252
+ title: app.title || app.name || id,
253
+ icon: app.icon || "/pinokio-black.png",
254
+ appRoot,
255
+ launchScript,
256
+ configuredLaunchScript,
257
+ launchPath,
258
+ dependencies,
259
+ startup_root: !!override.startup_root
260
+ }
261
+ }
262
+ shouldPrepareLaunchPath(config, requestedScript) {
263
+ if (!config || !requestedScript || !Array.isArray(config.dependencies) || config.dependencies.length === 0) {
264
+ return false
265
+ }
266
+ return !!(config.configuredLaunchScript && requestedScript === config.configuredLaunchScript)
267
+ }
268
+ isReady(config) {
269
+ return !!(config && config.launchPath && this.kernel && typeof this.kernel.isScriptReady === "function" && this.kernel.isScriptReady(config.launchPath))
270
+ }
271
+ isRunning(config) {
272
+ return !!(config && config.launchPath && this.kernel && this.kernel.api && this.kernel.api.running && this.kernel.api.running[config.launchPath])
273
+ }
274
+ inflightKey(config) {
275
+ return config && config.launchPath ? path.resolve(config.launchPath) : ""
276
+ }
277
+ targetForLaunchPath(launchPath) {
278
+ if (!launchPath || !this.kernel || typeof this.kernel.getAppIdForLaunchPath !== "function") {
279
+ return null
280
+ }
281
+ const appId = this.kernel.getAppIdForLaunchPath(launchPath)
282
+ if (!appId) {
283
+ return null
284
+ }
285
+ return {
286
+ appId,
287
+ script: typeof this.kernel.getAppRelativeLaunchScript === "function"
288
+ ? this.kernel.getAppRelativeLaunchScript(appId, launchPath)
289
+ : "",
290
+ launchPath
291
+ }
292
+ }
293
+ scriptProgress(config) {
294
+ if (!config || (!this.isRunning(config) && !this.hasActiveLaunch(config) && !this.inflightStarts.has(this.inflightKey(config)))) {
295
+ return null
296
+ }
297
+ return config && config.launchPath && this.kernel && typeof this.kernel.getScriptProgress === "function"
298
+ ? this.kernel.getScriptProgress(config.launchPath)
299
+ : null
300
+ }
301
+ stripProgress(row = {}) {
302
+ const next = { ...row }
303
+ delete next.step_current
304
+ delete next.step_total
305
+ return next
306
+ }
307
+ ownerIds(row = {}) {
308
+ const ids = []
309
+ const add = (value) => {
310
+ const id = this.normalizeAppId(value)
311
+ if (id && !ids.includes(id)) {
312
+ ids.push(id)
313
+ }
314
+ }
315
+ if (Array.isArray(row.owner_app_ids)) {
316
+ row.owner_app_ids.forEach(add)
317
+ }
318
+ add(row.owner_app_id)
319
+ return ids
320
+ }
321
+ mergeOwnerIds(current = {}, extra = {}) {
322
+ const ids = this.ownerIds(current)
323
+ const add = (value) => {
324
+ const id = this.normalizeAppId(value)
325
+ if (id && !ids.includes(id)) {
326
+ ids.push(id)
327
+ }
328
+ }
329
+ if (Array.isArray(extra.owner_app_ids)) {
330
+ extra.owner_app_ids.forEach(add)
331
+ }
332
+ add(extra.owner_app_id)
333
+ return ids
334
+ }
335
+ rowIsReady(row = {}) {
336
+ return !!(
337
+ row.launch_path &&
338
+ this.kernel &&
339
+ typeof this.kernel.isScriptReady === "function" &&
340
+ this.kernel.isScriptReady(row.launch_path)
341
+ )
342
+ }
343
+ rowIsRunning(row = {}) {
344
+ if (!row.launch_path || !this.kernel || !this.kernel.api || !this.kernel.api.running) {
345
+ return false
346
+ }
347
+ return !!this.kernel.api.running[path.resolve(row.launch_path)]
348
+ }
349
+ rowHasRealLifecycle(row = {}) {
350
+ return !!(this.rowIsRunning(row) || this.rowIsReady(row))
351
+ }
352
+ lifecycleRow(row = {}) {
353
+ const next = this.stripProgress(row)
354
+ delete next.owner_app_id
355
+ delete next.owner_app_ids
356
+ next.waiting_for = []
357
+ if (this.rowIsReady(row)) {
358
+ next.state = "ready"
359
+ next.ready_at = next.ready_at || Date.now()
360
+ next.process_started = true
361
+ } else {
362
+ next.state = "starting"
363
+ next.process_started = true
364
+ }
365
+ return next
366
+ }
367
+ clearAttemptStartupStatus(ownerAppId) {
368
+ const ownerId = this.normalizeAppId(ownerAppId)
369
+ const startupStatus = this.startupHomeStatus()
370
+ const apps = startupStatus && startupStatus.apps ? startupStatus.apps : null
371
+ if (!ownerId || !apps) {
372
+ return
373
+ }
374
+ for (const appId of Object.keys(apps)) {
375
+ const row = apps[appId]
376
+ const owners = this.ownerIds(row)
377
+ if (appId === ownerId) {
378
+ delete apps[appId]
379
+ continue
380
+ }
381
+ if (!owners.includes(ownerId)) {
382
+ continue
383
+ }
384
+ const remainingOwners = owners.filter((id) => id !== ownerId)
385
+ if (this.rowHasRealLifecycle(row)) {
386
+ const next = this.lifecycleRow(row)
387
+ if (remainingOwners.length > 0) {
388
+ next.owner_app_ids = remainingOwners
389
+ }
390
+ apps[appId] = next
391
+ continue
392
+ }
393
+ if (remainingOwners.length > 0) {
394
+ apps[appId] = {
395
+ ...row,
396
+ owner_app_ids: remainingOwners
397
+ }
398
+ delete apps[appId].owner_app_id
399
+ continue
400
+ }
401
+ delete apps[appId]
402
+ }
403
+ }
404
+ makeRow(config, state, extra = {}) {
405
+ return {
406
+ id: config.id,
407
+ title: config.title,
408
+ icon: config.icon,
409
+ script: config.launchScript,
410
+ launch_path: config.launchPath,
411
+ state,
412
+ ...(this.scriptProgress(config) || {}),
413
+ ...extra
414
+ }
415
+ }
416
+ setRequirementRow(targetId, config, state, extra = {}) {
417
+ const target = this.status.targets[targetId]
418
+ if (!target || !config || config.id === targetId) {
419
+ return
420
+ }
421
+ if (!target.requirement_order.includes(config.id)) {
422
+ target.requirement_order.push(config.id)
423
+ }
424
+ target.requirements[config.id] = {
425
+ ...this.stripProgress(target.requirements[config.id] || {}),
426
+ ...this.makeRow(config, state, extra)
427
+ }
428
+ this.emitStatus(targetId)
429
+ }
430
+ setTargetStatus(config, state, extra = {}) {
431
+ if (!config || !config.id) {
432
+ return null
433
+ }
434
+ const current = this.status.targets[config.id] || {}
435
+ const target = {
436
+ ...this.stripProgress(current),
437
+ app_id: config.id,
438
+ title: config.title,
439
+ script: config.launchScript,
440
+ launch_path: config.launchPath,
441
+ state,
442
+ started_at: current.started_at || Date.now(),
443
+ requirements: current.requirements || {},
444
+ requirement_order: Array.isArray(current.requirement_order) ? current.requirement_order : [],
445
+ waiting_for: Array.isArray(current.waiting_for) ? current.waiting_for : [],
446
+ ...extra
447
+ }
448
+ this.status.targets[config.id] = target
449
+ this.emitStatus(config.id)
450
+ return target
451
+ }
452
+ seedTargetRequirements(targetConfig, requirementIds, configs) {
453
+ const target = this.status.targets[targetConfig.id]
454
+ if (!target) {
455
+ return
456
+ }
457
+ const ids = Array.isArray(requirementIds) ? requirementIds.filter((id) => id && id !== targetConfig.id) : []
458
+ target.requirements = {}
459
+ target.requirement_order = ids
460
+ target.waiting_for = ids
461
+ for (const id of ids) {
462
+ const config = configs && typeof configs.get === "function" ? configs.get(id) : null
463
+ if (config) {
464
+ this.setRequirementRow(targetConfig.id, config, this.isReady(config) ? "ready" : "waiting")
465
+ }
466
+ }
467
+ this.emitStatus(targetConfig.id)
468
+ }
469
+ async markBlockedTarget(targetConfig, error, apps, knownConfigs = []) {
470
+ const blocked_reason = error && error.blocked_reason ? error.blocked_reason : String(error || "Launch requirements blocked")
471
+ const target = this.setTargetStatus(targetConfig, "blocked", {
472
+ blocked_reason,
473
+ startup: !!targetConfig.startup_root
474
+ })
475
+ const blockedId = this.normalizeAppId(error && error.app_id)
476
+ if (!target || !blockedId || blockedId === targetConfig.id) {
477
+ return blocked_reason
478
+ }
479
+ const partialConfigs = error && error.configs && typeof error.configs.get === "function" ? error.configs : null
480
+ const known = new Map((Array.isArray(knownConfigs) ? knownConfigs : [])
481
+ .filter((config) => config && config.id)
482
+ .map((config) => [config.id, config]))
483
+ const chain = Array.isArray(error && error.chain)
484
+ ? error.chain.filter((id) => id && id !== targetConfig.id)
485
+ : []
486
+ const orderedChain = chain.slice().reverse()
487
+ if (chain.length > 0) {
488
+ target.requirement_order = orderedChain
489
+ }
490
+ for (const chainId of orderedChain) {
491
+ if (chainId === blockedId) {
492
+ continue
493
+ }
494
+ const chainConfig = known.get(chainId) || (partialConfigs ? partialConfigs.get(chainId) : null)
495
+ if (chainConfig) {
496
+ this.setRequirementRow(targetConfig.id, chainConfig, "waiting", {
497
+ waiting_for: [blockedId]
498
+ })
499
+ }
500
+ }
501
+ let blockedConfig = known.get(blockedId) || (partialConfigs ? partialConfigs.get(blockedId) : null)
502
+ if (!blockedConfig) {
503
+ blockedConfig = await this.configFor(blockedId, apps).catch(() => null)
504
+ }
505
+ if (blockedConfig) {
506
+ this.setRequirementRow(targetConfig.id, blockedConfig, "blocked", { blocked_reason })
507
+ target.waiting_for = [blockedConfig.id]
508
+ }
509
+ return blocked_reason
510
+ }
511
+ setStartupRow(status, config, state, extra = {}) {
512
+ if (!status || !status.apps || !config) {
513
+ return
514
+ }
515
+ const current = status.apps[config.id] || {}
516
+ const row = {
517
+ ...this.stripProgress(current),
518
+ ...this.makeRow(config, state, extra),
519
+ dependencies: config.dependencies,
520
+ startup_root: !!(current.startup_root || config.startup_root)
521
+ }
522
+ const ownerIds = this.mergeOwnerIds(current, extra)
523
+ delete row.owner_app_id
524
+ if (ownerIds.length > 0) {
525
+ row.owner_app_ids = ownerIds
526
+ } else {
527
+ delete row.owner_app_ids
528
+ }
529
+ status.apps[config.id] = row
530
+ }
531
+ async seedStartupHomeStatus(candidates, startedAt = Date.now()) {
532
+ const status = this.replaceStartupHomeStatus({
533
+ running: true,
534
+ started_at: startedAt,
535
+ apps: {}
536
+ })
537
+ const apps = await this.appMap()
538
+ const configs = new Map()
539
+ const visiting = new Set()
540
+ const visited = new Set()
541
+
542
+ const configForCandidate = async (candidate) => {
543
+ const config = await this.configFor(candidate.id, apps, {
544
+ launchScript: candidate.script,
545
+ launchPath: candidate.autolaunchPath,
546
+ startup_root: true
547
+ })
548
+ if (config) {
549
+ configs.set(config.id, config)
550
+ }
551
+ return config
552
+ }
553
+
554
+ const dependencyConfig = async (dependencyId) => {
555
+ const id = this.normalizeAppId(dependencyId)
556
+ if (!id) {
557
+ return null
558
+ }
559
+ if (configs.has(id)) {
560
+ return configs.get(id)
561
+ }
562
+ const config = await this.configFor(id, apps)
563
+ if (config) {
564
+ configs.set(id, config)
565
+ }
566
+ return config
567
+ }
568
+
569
+ const setMissingDependency = (id, reason, ownerId) => {
570
+ if (!id || status.apps[id]) {
571
+ return
572
+ }
573
+ status.apps[id] = {
574
+ id,
575
+ title: id,
576
+ icon: "/pinokio-black.png",
577
+ script: "",
578
+ launch_path: "",
579
+ state: "blocked",
580
+ blocked_reason: reason,
581
+ dependencies: [],
582
+ waiting_for: [],
583
+ startup_root: false,
584
+ owner_app_ids: ownerId ? [ownerId] : []
585
+ }
586
+ }
587
+
588
+ const seedConfig = async (config, ownerId) => {
589
+ if (!config || visited.has(config.id)) {
590
+ return
591
+ }
592
+ if (visiting.has(config.id)) {
593
+ this.setStartupRow(status, config, "blocked", {
594
+ blocked_reason: `Requirement cycle detected at ${config.title || config.id}`,
595
+ waiting_for: [],
596
+ owner_app_id: ownerId
597
+ })
598
+ return
599
+ }
600
+ visiting.add(config.id)
601
+ for (const dependencyId of config.dependencies) {
602
+ const dependency = await dependencyConfig(dependencyId)
603
+ if (dependency) {
604
+ await seedConfig(dependency, ownerId)
605
+ } else {
606
+ setMissingDependency(dependencyId, `Required app is not installed: ${dependencyId}`, ownerId)
607
+ }
608
+ }
609
+ visiting.delete(config.id)
610
+ visited.add(config.id)
611
+
612
+ const waitingFor = config.dependencies.filter((dependencyId) => {
613
+ const row = status.apps[dependencyId]
614
+ return !row || row.state !== "ready"
615
+ })
616
+ const missingLaunchScript = !config.launchScript || !config.launchPath
617
+ const state = missingLaunchScript
618
+ ? "blocked"
619
+ : (this.isReady(config) ? "ready" : (waitingFor.length > 0 ? "waiting" : "pending"))
620
+ this.setStartupRow(status, config, state, {
621
+ waiting_for: state === "blocked" ? [] : waitingFor,
622
+ owner_app_id: ownerId,
623
+ process_started: this.isRunning(config) || this.isReady(config),
624
+ ...(missingLaunchScript ? { blocked_reason: `${config.title || config.id} has no launch script selected` } : {})
625
+ })
626
+ }
627
+
628
+ for (const candidate of Array.isArray(candidates) ? candidates : []) {
629
+ const config = await configForCandidate(candidate)
630
+ if (!config) {
631
+ status.apps[candidate.id] = {
632
+ id: candidate.id,
633
+ title: candidate.title || candidate.id,
634
+ script: candidate.script || "",
635
+ launch_path: candidate.autolaunchPath || "",
636
+ state: "blocked",
637
+ blocked_reason: "Startup app is not installed",
638
+ dependencies: [],
639
+ waiting_for: [],
640
+ startup_root: true
641
+ }
642
+ continue
643
+ }
644
+ await seedConfig(config, config.id)
645
+ const row = status.apps[config.id]
646
+ if (!candidate.exists && row) {
647
+ status.apps[config.id] = {
648
+ ...row,
649
+ state: "blocked",
650
+ blocked_reason: config.launchScript ? "Launch script does not exist" : "No launch script selected",
651
+ waiting_for: []
652
+ }
653
+ }
654
+ }
655
+
656
+ return status
657
+ }
658
+ checkCancelled(targetId) {
659
+ if (targetId && this.cancelled.has(targetId)) {
660
+ throw this.cancelledError(targetId)
661
+ }
662
+ }
663
+ isCancelled(appId) {
664
+ const id = this.normalizeAppId(appId)
665
+ return !!(id && this.cancelled.has(id))
666
+ }
667
+ cancel(launchPath, options = {}) {
668
+ const targetInfo = this.targetForLaunchPath(launchPath)
669
+ if (!targetInfo) {
670
+ return false
671
+ }
672
+ const target = this.status.targets[targetInfo.appId]
673
+ if (!target) {
674
+ if (options && options.force) {
675
+ this.cancelled.add(targetInfo.appId)
676
+ this.clearAttemptStartupStatus(targetInfo.appId)
677
+ this.emitStatus(targetInfo.appId)
678
+ return true
679
+ }
680
+ return false
681
+ }
682
+ if (target.state === "blocked" && !(options && options.force)) {
683
+ return false
684
+ }
685
+ this.cancelled.add(targetInfo.appId)
686
+ delete this.status.targets[targetInfo.appId]
687
+ this.clearAttemptStartupStatus(targetInfo.appId)
688
+ this.emitStatus(targetInfo.appId)
689
+ return true
690
+ }
691
+ referencesApp(status, appId) {
692
+ if (!status || !appId) {
693
+ return false
694
+ }
695
+ const lists = [status.waiting_for, status.dependencies, status.requirement_order]
696
+ if (lists.some((list) => Array.isArray(list) && list.includes(appId))) {
697
+ return true
698
+ }
699
+ return !!(status.requirements && status.requirements[appId])
700
+ }
701
+ clearRelated(appId) {
702
+ const id = this.normalizeAppId(appId)
703
+ if (!id) {
704
+ return
705
+ }
706
+ this.cancelled.delete(id)
707
+ const changedTargets = new Set()
708
+ if (this.status.targets[id]) {
709
+ changedTargets.add(id)
710
+ }
711
+ delete this.status.targets[id]
712
+ for (const targetId of Object.keys(this.status.targets)) {
713
+ if (this.referencesApp(this.status.targets[targetId], id)) {
714
+ changedTargets.add(targetId)
715
+ delete this.status.targets[targetId]
716
+ }
717
+ }
718
+ changedTargets.forEach((targetId) => this.cancelled.add(targetId))
719
+ const startupStatus = this.startupHomeStatus()
720
+ const apps = startupStatus && startupStatus.apps ? startupStatus.apps : null
721
+ if (apps) {
722
+ delete apps[id]
723
+ for (const statusId of Object.keys(apps)) {
724
+ if (this.referencesApp(apps[statusId], id)) {
725
+ delete apps[statusId]
726
+ }
727
+ }
728
+ }
729
+ changedTargets.forEach((targetId) => this.emitStatus(targetId))
730
+ }
731
+ markStarted(launchPath) {
732
+ const targetInfo = this.targetForLaunchPath(launchPath)
733
+ if (!targetInfo) {
734
+ return
735
+ }
736
+ const hadTarget = !!this.status.targets[targetInfo.appId]
737
+ this.cancelled.delete(targetInfo.appId)
738
+ delete this.status.targets[targetInfo.appId]
739
+ if (hadTarget) {
740
+ this.emitStatus(targetInfo.appId)
741
+ }
742
+ }
743
+ markProgress(launchPath, progress) {
744
+ const targetInfo = this.targetForLaunchPath(launchPath)
745
+ if (!targetInfo || !progress) {
746
+ return
747
+ }
748
+ const target = this.status.targets[targetInfo.appId]
749
+ if (target) {
750
+ target.step_current = progress.step_current
751
+ target.step_total = progress.step_total
752
+ this.emitStatus(targetInfo.appId)
753
+ }
754
+ }
755
+ markDone(launchPath) {
756
+ const targetInfo = this.targetForLaunchPath(launchPath)
757
+ if (!targetInfo) {
758
+ return
759
+ }
760
+ const hadTarget = !!this.status.targets[targetInfo.appId]
761
+ delete this.status.targets[targetInfo.appId]
762
+ this.cancelled.delete(targetInfo.appId)
763
+ if (hadTarget) {
764
+ this.emitStatus(targetInfo.appId)
765
+ }
766
+ }
767
+ async resolveGraph(rootConfigs, apps) {
768
+ const configs = new Map()
769
+ const order = []
770
+ const visited = new Set()
771
+ const visiting = []
772
+ const overrides = new Map(rootConfigs.map((config) => [config.id, config]))
773
+ const visit = async (appId) => {
774
+ const id = this.normalizeAppId(appId)
775
+ if (!id) {
776
+ return
777
+ }
778
+ const cycleIndex = visiting.indexOf(id)
779
+ if (cycleIndex >= 0) {
780
+ const cycle = visiting.slice(cycleIndex).concat(id)
781
+ throw this.blocked(`Requirement cycle detected: ${cycle.join(" -> ")}`, {
782
+ app_id: id,
783
+ cycle
784
+ })
785
+ }
786
+ if (visited.has(id)) {
787
+ return
788
+ }
789
+ visiting.push(id)
790
+ let config = configs.get(id)
791
+ if (!config) {
792
+ config = await this.configFor(id, apps, overrides.get(id) || {})
793
+ if (!config) {
794
+ throw this.blocked(`Required app is not installed: ${id}`, {
795
+ app_id: id
796
+ })
797
+ }
798
+ configs.set(id, config)
799
+ }
800
+ if (!config.launchScript || !config.launchPath) {
801
+ throw this.blocked(`${config.title || config.id} has no launch script selected`, {
802
+ app_id: config.id
803
+ })
804
+ }
805
+ for (const dependencyId of config.dependencies) {
806
+ await visit(dependencyId)
807
+ }
808
+ visiting.pop()
809
+ visited.add(id)
810
+ order.push(id)
811
+ }
812
+ try {
813
+ for (const config of rootConfigs) {
814
+ await visit(config.id)
815
+ }
816
+ } catch (error) {
817
+ if (error && typeof error === "object") {
818
+ error.configs = configs
819
+ error.order = order.slice()
820
+ error.chain = visiting.slice()
821
+ }
822
+ throw error
823
+ }
824
+ return {
825
+ configs,
826
+ order
827
+ }
828
+ }
829
+ async waitUntilReady(config, context) {
830
+ while (true) {
831
+ this.checkCancelled(context.targetId)
832
+ if (this.isReady(config)) {
833
+ this.setRequirementRow(context.targetId, config, "ready")
834
+ this.setStartupRow(context.startupStatus, config, "ready", {
835
+ ready_at: Date.now(),
836
+ waiting_for: [],
837
+ owner_app_id: context.targetId,
838
+ process_started: true
839
+ })
840
+ return
841
+ }
842
+ this.setRequirementRow(context.targetId, config, "starting")
843
+ this.setStartupRow(context.startupStatus, config, "starting", {
844
+ waiting_for: [],
845
+ owner_app_id: context.targetId,
846
+ process_started: this.isRunning(config) || this.isReady(config)
847
+ })
848
+ await new Promise((resolve) => setTimeout(resolve, context.pollMs))
849
+ }
850
+ }
851
+ async startConfigIfNeeded(config) {
852
+ const key = this.inflightKey(config)
853
+ if (!key || this.isReady(config) || this.isRunning(config)) {
854
+ return
855
+ }
856
+ if (this.hasActiveLaunch(config)) {
857
+ return
858
+ }
859
+ const existing = this.inflightStarts.get(key)
860
+ if (existing) {
861
+ await existing
862
+ return
863
+ }
864
+ let promise
865
+ promise = Promise.resolve().then(async () => {
866
+ try {
867
+ if (!this.isReady(config) && !this.isRunning(config)) {
868
+ await this.kernel.api.process({
869
+ uri: config.launchPath,
870
+ input: {},
871
+ skip_requirements: true
872
+ })
873
+ }
874
+ } finally {
875
+ if (this.inflightStarts.get(key) === promise) {
876
+ this.inflightStarts.delete(key)
877
+ }
878
+ }
879
+ })
880
+ this.inflightStarts.set(key, promise)
881
+ await promise
882
+ }
883
+ async ensureConfigReady(config, context) {
884
+ this.checkCancelled(context.targetId)
885
+ if (this.isReady(config)) {
886
+ this.setRequirementRow(context.targetId, config, "ready")
887
+ this.setStartupRow(context.startupStatus, config, "ready", {
888
+ ready_at: Date.now(),
889
+ waiting_for: [],
890
+ owner_app_id: context.targetId,
891
+ process_started: true
892
+ })
893
+ return
894
+ }
895
+ if (!this.isRunning(config)) {
896
+ this.setRequirementRow(context.targetId, config, "starting")
897
+ this.setStartupRow(context.startupStatus, config, "starting", {
898
+ started_at: Date.now(),
899
+ waiting_for: [],
900
+ owner_app_id: context.targetId,
901
+ process_started: false
902
+ })
903
+ await this.startConfigIfNeeded(config)
904
+ } else {
905
+ this.setRequirementRow(context.targetId, config, "starting")
906
+ this.setStartupRow(context.startupStatus, config, "starting", {
907
+ waiting_for: [],
908
+ owner_app_id: context.targetId,
909
+ process_started: true
910
+ })
911
+ }
912
+ await this.waitUntilReady(config, context)
913
+ }
914
+ ensureNode(config, context) {
915
+ if (context.promises.has(config.id)) {
916
+ return context.promises.get(config.id)
917
+ }
918
+ const promise = (async () => {
919
+ this.checkCancelled(context.targetId)
920
+ const dependencyConfigs = config.dependencies
921
+ .map((dependencyId) => context.configs.get(dependencyId))
922
+ .filter(Boolean)
923
+ const waitingFor = dependencyConfigs
924
+ .filter((dependency) => !this.isReady(dependency))
925
+ .map((dependency) => dependency.id)
926
+ if (waitingFor.length > 0) {
927
+ this.setRequirementRow(context.targetId, config, "waiting", {
928
+ waiting_for: waitingFor
929
+ })
930
+ this.setStartupRow(context.startupStatus, config, "waiting", {
931
+ waiting_for: waitingFor,
932
+ owner_app_id: context.targetId
933
+ })
934
+ }
935
+ await Promise.all(dependencyConfigs.map((dependency) => this.ensureNode(dependency, context)))
936
+ await this.ensureConfigReady(config, context)
937
+ })()
938
+ context.promises.set(config.id, promise)
939
+ return promise
940
+ }
941
+ async ensureForLaunchPath(launchPath, options = {}) {
942
+ if (!launchPath || options.skip === true) {
943
+ return this.controlResult("continue")
944
+ }
945
+ const targetId = this.kernel.getAppIdForLaunchPath(launchPath)
946
+ if (!targetId) {
947
+ return this.controlResult("continue")
948
+ }
949
+ const startupRequest = !!(options.request && options.request.startup)
950
+ const owner = options.owner || null
951
+ const targetWasCancelled = this.cancelled.has(targetId)
952
+ const apps = await this.appMap()
953
+ const requestedScript = this.kernel.getAppRelativeLaunchScript(targetId, launchPath)
954
+ const targetConfig = await this.configFor(targetId, apps, {
955
+ launchScript: requestedScript,
956
+ launchPath,
957
+ startup_root: startupRequest
958
+ })
959
+ if (!targetConfig || !this.shouldPrepareLaunchPath(targetConfig, requestedScript)) {
960
+ if (this.status.targets[targetId]) {
961
+ delete this.status.targets[targetId]
962
+ this.emitStatus(targetId)
963
+ }
964
+ return this.controlResult("continue")
965
+ }
966
+ if (targetWasCancelled) {
967
+ if (startupRequest || this.hasActiveLaunch(targetConfig, owner)) {
968
+ return this.cancelledResult(targetId)
969
+ }
970
+ this.cancelled.delete(targetId)
971
+ }
972
+ const activeTarget = this.activeTargetStatus(targetId, launchPath)
973
+ if (activeTarget) {
974
+ if (activeTarget.state === "blocked") {
975
+ delete this.status.targets[targetId]
976
+ this.emitStatus(targetId)
977
+ } else {
978
+ return this.handledResult(targetId, activeTarget)
979
+ }
980
+ }
981
+ if (this.hasActiveLaunch(targetConfig, owner)) {
982
+ return this.handledResult(targetId, {
983
+ state: "starting"
984
+ })
985
+ }
986
+ let graph
987
+ const dependencyRoots = []
988
+ try {
989
+ for (const dependencyId of targetConfig.dependencies) {
990
+ const config = await this.configFor(dependencyId, apps)
991
+ if (!config) {
992
+ throw this.blocked(`Required app is not installed: ${dependencyId}`, {
993
+ app_id: dependencyId
994
+ })
995
+ }
996
+ dependencyRoots.push(config)
997
+ }
998
+ const initialRequirementIds = dependencyRoots.map((config) => config.id)
999
+ this.setTargetStatus(targetConfig, "waiting", {
1000
+ startup: startupRequest,
1001
+ requirements: {},
1002
+ requirement_order: initialRequirementIds,
1003
+ waiting_for: initialRequirementIds
1004
+ })
1005
+ for (const config of dependencyRoots) {
1006
+ this.setRequirementRow(targetId, config, this.isReady(config) ? "ready" : "waiting")
1007
+ }
1008
+ graph = await this.resolveGraph(dependencyRoots, apps)
1009
+ } catch (error) {
1010
+ await this.markBlockedTarget(targetConfig, error, apps, dependencyRoots)
1011
+ const startupStatus = this.startupHomeStatus()
1012
+ if (startupRequest && startupStatus) {
1013
+ const target = this.status.targets[targetId]
1014
+ this.setStartupRow(startupStatus, targetConfig, "blocked", {
1015
+ blocked_reason: target && target.blocked_reason ? target.blocked_reason : (error && error.blocked_reason ? error.blocked_reason : String(error || "Launch requirements blocked")),
1016
+ requirement_order: target && Array.isArray(target.requirement_order) ? target.requirement_order : [],
1017
+ waiting_for: target && Array.isArray(target.waiting_for) ? target.waiting_for : []
1018
+ })
1019
+ }
1020
+ const result = this.resultFromError(error, targetId)
1021
+ if (result) {
1022
+ return result
1023
+ }
1024
+ throw error
1025
+ }
1026
+ this.checkCancelled(targetId)
1027
+ const orderedRequirementIds = graph.order.filter((id) => id !== targetId)
1028
+ const target = this.status.targets[targetId]
1029
+ if (!target) {
1030
+ return this.cancelledResult(targetId)
1031
+ }
1032
+ this.seedTargetRequirements(targetConfig, orderedRequirementIds, graph.configs)
1033
+ const startupStatus = this.startupHomeStatus()
1034
+ const context = {
1035
+ targetId,
1036
+ configs: graph.configs,
1037
+ promises: new Map(),
1038
+ startupStatus,
1039
+ pollMs: options.pollMs || 1000
1040
+ }
1041
+ try {
1042
+ await Promise.all(targetConfig.dependencies.map((dependencyId) => {
1043
+ const config = graph.configs.get(dependencyId)
1044
+ return config ? this.ensureNode(config, context) : null
1045
+ }))
1046
+ this.checkCancelled(targetId)
1047
+ delete this.status.targets[targetId]
1048
+ this.emitStatus(targetId)
1049
+ return this.controlResult("continue")
1050
+ } catch (error) {
1051
+ if (error && error.cancelled) {
1052
+ delete this.status.targets[targetId]
1053
+ this.emitStatus(targetId)
1054
+ return this.cancelledResult(targetId)
1055
+ }
1056
+ const target = this.status.targets[targetId]
1057
+ if (target) {
1058
+ target.state = "blocked"
1059
+ target.blocked_reason = error && error.blocked_reason ? error.blocked_reason : String(error || "Launch requirements blocked")
1060
+ this.emitStatus(targetId)
1061
+ }
1062
+ const result = this.resultFromError(error, targetId)
1063
+ if (result) {
1064
+ return result
1065
+ }
1066
+ throw error
1067
+ }
1068
+ }
1069
+ getStatus(appId) {
1070
+ const id = this.normalizeAppId(appId)
1071
+ if (!id || !this.status.targets[id]) {
1072
+ return null
1073
+ }
1074
+ const target = this.status.targets[id]
1075
+ const orderedIds = Array.isArray(target.requirement_order) ? target.requirement_order : Object.keys(target.requirements || {})
1076
+ const requirements = orderedIds
1077
+ .map((rowId) => target.requirements[rowId])
1078
+ .filter(Boolean)
1079
+ .map((row) => {
1080
+ const progress = this.scriptProgress({
1081
+ id: row.id,
1082
+ launchPath: row.launch_path
1083
+ })
1084
+ return {
1085
+ ...row,
1086
+ ...(progress || {})
1087
+ }
1088
+ })
1089
+ return this.formatStatus(id, target, requirements, {
1090
+ visible_states: ["blocked"]
1091
+ })
1092
+ }
1093
+ formatStatus(appId, target, requirements, options = {}) {
1094
+ const waitingFor = requirements
1095
+ .filter((row) => row && row.state !== "ready")
1096
+ .map((row) => row.id)
1097
+ const visibleStates = new Set(Array.isArray(options.visible_states) ? options.visible_states : ["blocked"])
1098
+ if (!visibleStates.has(target.state) && waitingFor.length === 0) {
1099
+ return null
1100
+ }
1101
+ return {
1102
+ ...target,
1103
+ app_id: appId,
1104
+ title: target.title || appId,
1105
+ script: target.script || "",
1106
+ state: target.state,
1107
+ blocked_reason: target.blocked_reason || "",
1108
+ startup: Object.prototype.hasOwnProperty.call(options, "startup") ? !!options.startup : !!target.startup,
1109
+ requirements,
1110
+ waiting_for: waitingFor
1111
+ }
1112
+ }
1113
+ }
1114
+
1115
+ module.exports = LaunchRequirements