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.
- package/kernel/api/index.js +111 -15
- package/kernel/api/script/index.js +10 -0
- package/kernel/autolaunch.js +111 -0
- package/kernel/environment.js +89 -1
- package/kernel/git.js +9 -19
- package/kernel/index.js +142 -43
- package/kernel/launch_requirements.js +1115 -0
- package/kernel/ready.js +231 -0
- package/kernel/script.js +16 -0
- package/kernel/shells.js +9 -1
- package/kernel/workspace_status.js +111 -45
- package/package.json +1 -1
- package/server/autolaunch.js +725 -0
- package/server/index.js +244 -411
- package/server/views/app.ejs +256 -152
- package/server/views/autolaunch.ejs +363 -75
- package/server/views/index.ejs +550 -26
- package/server/views/partials/app_autolaunch_dependency_events.ejs +99 -0
- package/server/views/partials/app_autolaunch_dependency_save.ejs +10 -0
- package/server/views/partials/app_autolaunch_dependency_styles.ejs +357 -0
- package/server/views/partials/app_autolaunch_modal_helpers.ejs +347 -0
- package/server/views/partials/autolaunch_dependency_helpers.ejs +204 -0
- package/server/views/partials/autolaunch_dependency_save.ejs +13 -0
- package/server/views/partials/autolaunch_dependency_styles.ejs +347 -0
- package/server/views/partials/home_action_modal.ejs +4 -1
- package/server/views/partials/launch_requirements_status_client.ejs +271 -0
- package/server/views/partials/launch_requirements_status_styles.ejs +171 -0
- package/server/views/partials/launch_settings_dependency_save_factory.ejs +45 -0
- package/server/views/partials/launch_settings_dependency_script_loader_factory.ejs +25 -0
- package/server/views/terminal.ejs +196 -2
- package/test/home-autolaunch-live-ui.test.js +455 -0
- package/test/launch-requirements-browser.test.js +579 -0
- package/test/launch-requirements-contract-coverage.test.js +627 -0
- package/test/launch-requirements-real-browser.js +625 -0
- package/test/launch-requirements-status-client.test.js +132 -0
- package/test/launch-requirements.test.js +1806 -0
- package/test/launch-settings-ui.test.js +370 -0
- package/test/ready-state.test.js +49 -0
- package/test/server-autolaunch.test.js +1052 -0
- package/test/startup-git-index-benchmark.js +409 -0
- package/test/startup-git-index-browser.js +320 -0
- package/test/startup-git-index-performance.test.js +380 -0
- package/test/startup-git-index-refactor.test.js +450 -0
- package/test/startup-git-index-route.test.js +588 -0
- package/test/universal-launcher.smoke.spec.js +10 -9
- package/test/workspace-gitignore-benchmark.js +815 -0
- package/test/workspace-gitignore-path-scoped.test.js +256 -0
- 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
|