pinokiod 8.2.17 → 8.2.19
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/package.json +1 -1
- package/server/index.js +32 -8
- package/test/anywhere-api.test.js +33 -5
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -1037,7 +1037,7 @@ class Server {
|
|
|
1037
1037
|
const entries = await this.kernel.api.listApps()
|
|
1038
1038
|
return { apps: entries.map((entry) => ({ id: entry.id, name: entry.title || entry.name || entry.id, icon: entry.icon })) }
|
|
1039
1039
|
}
|
|
1040
|
-
// launch
|
|
1040
|
+
// launch and stop are side effects: only pinokio.co and this computer may call them
|
|
1041
1041
|
anywhereOriginAllowed(origin) {
|
|
1042
1042
|
if (!origin) return true
|
|
1043
1043
|
let parsed
|
|
@@ -1067,10 +1067,15 @@ class Server {
|
|
|
1067
1067
|
}
|
|
1068
1068
|
return items
|
|
1069
1069
|
}
|
|
1070
|
+
// the folder of an installed app, or "" for anything that is not exactly one folder name under api/
|
|
1071
|
+
async anywhereAppDir(id) {
|
|
1072
|
+
const dir = typeof id === "string" ? this.kernel.path("api", id) : ""
|
|
1073
|
+
if (!dir || path.dirname(dir) !== this.kernel.path("api") || path.basename(dir) !== id) return ""
|
|
1074
|
+
return (await this.kernel.api.exists(dir)) ? dir : ""
|
|
1075
|
+
}
|
|
1070
1076
|
async composeAnywhereLaunch({ id, href } = {}) {
|
|
1071
1077
|
const api = this.kernel.api
|
|
1072
|
-
|
|
1073
|
-
if (!dir || path.dirname(dir) !== this.kernel.path("api") || path.basename(dir) !== id || !(await api.exists(dir))) {
|
|
1078
|
+
if (!(await this.anywhereAppDir(id))) {
|
|
1074
1079
|
return { status: 400, body: { error: `Unknown app: ${id}` } }
|
|
1075
1080
|
}
|
|
1076
1081
|
const menu = await this.anywhereMenu(api, await api.launcher_path(id))
|
|
@@ -1104,6 +1109,21 @@ class Server {
|
|
|
1104
1109
|
}
|
|
1105
1110
|
return { status: 200, body: { action: "started" } }
|
|
1106
1111
|
}
|
|
1112
|
+
// stops every run under the app's folder the way the desktop stops one (kernel.api.stop by its running key)
|
|
1113
|
+
async composeAnywhereStop({ id } = {}) {
|
|
1114
|
+
const api = this.kernel.api
|
|
1115
|
+
const dir = await this.anywhereAppDir(id)
|
|
1116
|
+
if (!dir) {
|
|
1117
|
+
return { status: 400, body: { error: `Unknown app: ${id}` } }
|
|
1118
|
+
}
|
|
1119
|
+
for (const [key, value] of Object.entries(api.running || {})) {
|
|
1120
|
+
const rel = path.relative(dir, key.split("?")[0])
|
|
1121
|
+
if (value && rel && !rel.startsWith("..") && !path.isAbsolute(rel)) {
|
|
1122
|
+
await api.stop({ params: { id: key } })
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return { status: 200, body: { action: "stopped" } }
|
|
1126
|
+
}
|
|
1107
1127
|
async composeAnywhereStatus({ light } = {}) {
|
|
1108
1128
|
const anywhere = this.kernel ? this.kernel.anywhere : null
|
|
1109
1129
|
if (!anywhere) {
|
|
@@ -1140,7 +1160,8 @@ class Server {
|
|
|
1140
1160
|
id: app.id,
|
|
1141
1161
|
name: app.name,
|
|
1142
1162
|
icon: app.icon,
|
|
1143
|
-
|
|
1163
|
+
// Anywhere's own states: ready once a Serve route carries the app, publishing while it is up locally without one
|
|
1164
|
+
state: entry ? "ready" : (app.ready_url || app.url ? "publishing" : "starting"),
|
|
1144
1165
|
remote: entry ? rewriteAnywhereHref(app.ready_url || app.url, snap.serve) : null,
|
|
1145
1166
|
exposed: !!entry,
|
|
1146
1167
|
})
|
|
@@ -14348,19 +14369,22 @@ class Server {
|
|
|
14348
14369
|
res.set("Cache-Control", "no-store")
|
|
14349
14370
|
res.json(await this.composeAnywhereApps())
|
|
14350
14371
|
}))
|
|
14351
|
-
this.
|
|
14372
|
+
// app actions change what runs on this computer: pinokio.co and this computer only
|
|
14373
|
+
const anywhereAppAction = (fn) => ex(async (req, res) => {
|
|
14352
14374
|
res.set("Cache-Control", "no-store")
|
|
14353
14375
|
if (!this.anywhereOriginAllowed(req.get("Origin"))) {
|
|
14354
|
-
res.status(403).json({ error: "
|
|
14376
|
+
res.status(403).json({ error: "Allowed only from pinokio.co or this computer" })
|
|
14355
14377
|
return
|
|
14356
14378
|
}
|
|
14357
14379
|
try {
|
|
14358
|
-
const { status, body } = await
|
|
14380
|
+
const { status, body } = await fn(req.body || {})
|
|
14359
14381
|
res.status(status).json(body)
|
|
14360
14382
|
} catch (error) {
|
|
14361
14383
|
res.status(500).json({ error: error && error.message ? error.message : String(error) })
|
|
14362
14384
|
}
|
|
14363
|
-
})
|
|
14385
|
+
})
|
|
14386
|
+
this.app.post("/anywhere/apps/launch", anywhereAppAction((body) => this.composeAnywhereLaunch(body)))
|
|
14387
|
+
this.app.post("/anywhere/apps/stop", anywhereAppAction((body) => this.composeAnywhereStop(body)))
|
|
14364
14388
|
const anywhereAction = (label, fn) => ex(async (req, res) => {
|
|
14365
14389
|
const anywhere = this.kernel.anywhere
|
|
14366
14390
|
if (!anywhere) {
|
|
@@ -22,9 +22,11 @@ test('status carries app ids and complete remote addresses, and no installed cou
|
|
|
22
22
|
let readyUrl
|
|
23
23
|
const fake = {
|
|
24
24
|
kernel: { anywhere, api: { listApps: async () => [{ id: 'comfy' }, { id: 'forge' }, { id: 'idle' }] } },
|
|
25
|
+
// Home Server states (pending: up locally, no external address) never leak into Anywhere's
|
|
25
26
|
collectHomeServerApps: async () => [
|
|
26
|
-
{ id: 'comfy', name: 'ComfyUI', icon: '/c.png', ready_url: readyUrl, state: '
|
|
27
|
+
{ id: 'comfy', name: 'ComfyUI', icon: '/c.png', ready_url: readyUrl, state: 'pending' },
|
|
27
28
|
{ id: 'forge', name: 'Forge', icon: '/f.png', ready_url: '', state: 'starting' },
|
|
29
|
+
{ id: 'unrouted', name: 'Unrouted', icon: '/u.png', ready_url: 'http://127.0.0.1:9999', state: 'pending' },
|
|
28
30
|
],
|
|
29
31
|
portFromUrl: Server.prototype.portFromUrl,
|
|
30
32
|
}
|
|
@@ -35,7 +37,7 @@ test('status carries app ids and complete remote addresses, and no installed cou
|
|
|
35
37
|
readyUrl = url
|
|
36
38
|
const status = await Server.prototype.composeAnywhereStatus.call(fake, {})
|
|
37
39
|
assert.equal(status.installed, undefined)
|
|
38
|
-
assert.deepEqual(status.apps.map((a) => [a.id, a.remote]), [['comfy', expected], ['forge', null]])
|
|
40
|
+
assert.deepEqual(status.apps.map((a) => [a.id, a.state, a.remote]), [['comfy', 'ready', expected], ['forge', 'starting', null], ['unrouted', 'publishing', null]])
|
|
39
41
|
}
|
|
40
42
|
const light = await Server.prototype.composeAnywhereStatus.call(fake, { light: true })
|
|
41
43
|
assert.equal(light.apps, undefined, 'the light status is unchanged')
|
|
@@ -108,7 +110,7 @@ test('the catalog maps installed apps to id, name and icon', async () => {
|
|
|
108
110
|
// a launch harness: one app "comfy" at /managed/api/comfy whose menu is set per case;
|
|
109
111
|
// kernel.status mirrors the kernel's session-aware running check
|
|
110
112
|
const launchHarness = ({ menu = [], running = {}, anywhere = readyAnywhere() } = {}) => {
|
|
111
|
-
const calls = { process: [], cleared: [], reserved: [], counted: [] }
|
|
113
|
+
const calls = { process: [], cleared: [], reserved: [], counted: [], stopped: [] }
|
|
112
114
|
const api = {
|
|
113
115
|
exists: async (p) => p === '/managed/api/comfy',
|
|
114
116
|
launcher_path: async (name) => `/managed/api/${name}`,
|
|
@@ -116,6 +118,7 @@ const launchHarness = ({ menu = [], running = {}, anywhere = readyAnywhere() } =
|
|
|
116
118
|
running,
|
|
117
119
|
logSessions: { reserveLaunch: async (uri) => { calls.reserved.push(uri) } },
|
|
118
120
|
process: (req) => { calls.process.push(req); return Promise.resolve() },
|
|
121
|
+
stop: async (req) => { calls.stopped.push(req.params.id) },
|
|
119
122
|
}
|
|
120
123
|
const server = Object.assign(Object.create(Server.prototype), {
|
|
121
124
|
kernel: {
|
|
@@ -126,7 +129,7 @@ const launchHarness = ({ menu = [], running = {}, anywhere = readyAnywhere() } =
|
|
|
126
129
|
},
|
|
127
130
|
appPreferences: { recordLaunchByPath: async (p, options) => { calls.counted.push([p, options.source]) } },
|
|
128
131
|
})
|
|
129
|
-
return { server, calls, launch: (body) => server.composeAnywhereLaunch(body) }
|
|
132
|
+
return { server, calls, launch: (body) => server.composeAnywhereLaunch(body), stop: (body) => server.composeAnywhereStop(body) }
|
|
130
133
|
}
|
|
131
134
|
|
|
132
135
|
test('launch opens a loopback URL default at its Anywhere address, null until Serve carries it, other URLs as they are', async () => {
|
|
@@ -205,7 +208,32 @@ test('a drive-relative app id is refused on Windows', { skip: process.platform !
|
|
|
205
208
|
}
|
|
206
209
|
})
|
|
207
210
|
|
|
208
|
-
test('
|
|
211
|
+
test('stop ends every run under the app folder by its running key, and nothing else', async () => {
|
|
212
|
+
const running = {
|
|
213
|
+
'/managed/api/comfy/start.js': true,
|
|
214
|
+
'/managed/api/comfy/start.js&session=abc': true,
|
|
215
|
+
'/managed/api/comfy/sub/worker.js?cwd=/managed/api/comfy': true,
|
|
216
|
+
'/managed/api/comfy/old.js': false,
|
|
217
|
+
'/managed/api/comfyui/start.js': true,
|
|
218
|
+
'/managed/api/other/start.js': true,
|
|
219
|
+
}
|
|
220
|
+
const h = launchHarness({ running })
|
|
221
|
+
assert.deepEqual(await h.stop({ id: 'comfy' }), { status: 200, body: { action: 'stopped' } })
|
|
222
|
+
assert.deepEqual(h.calls.stopped, [
|
|
223
|
+
'/managed/api/comfy/start.js',
|
|
224
|
+
'/managed/api/comfy/start.js&session=abc',
|
|
225
|
+
'/managed/api/comfy/sub/worker.js?cwd=/managed/api/comfy',
|
|
226
|
+
])
|
|
227
|
+
const idle = launchHarness()
|
|
228
|
+
assert.deepEqual(await idle.stop({ id: 'comfy' }), { status: 200, body: { action: 'stopped' } })
|
|
229
|
+
assert.deepEqual(idle.calls.stopped, [])
|
|
230
|
+
for (const id of ['nope', '../comfy', 'comfy/start.js', '', undefined]) {
|
|
231
|
+
assert.equal((await h.stop({ id })).status, 400, String(id))
|
|
232
|
+
}
|
|
233
|
+
assert.equal(h.calls.stopped.length, 3)
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test('launch and stop are allowed from pinokio.co and this computer only', () => {
|
|
209
237
|
const server = Object.assign(Object.create(Server.prototype), { kernel: { anywhere: readyAnywhere() } })
|
|
210
238
|
for (const origin of [undefined, 'https://pinokio.co', 'http://localhost:42000', 'http://127.0.0.1:42000', 'https://pinokio.tail1.ts.net']) {
|
|
211
239
|
assert.equal(server.anywhereOriginAllowed(origin), true, String(origin))
|