pinokiod 8.2.10 → 8.2.12
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/anywhere/cli.js +100 -0
- package/kernel/anywhere/index.js +237 -161
- package/kernel/anywhere/ports.js +6 -9
- package/kernel/anywhere/serve.js +17 -7
- package/kernel/anywhere/state.js +14 -0
- package/kernel/bin/conda-meta.js +19 -1
- package/kernel/bin/conda-pins.js +7 -3
- package/kernel/bin/conda.js +67 -15
- package/kernel/bin/index.js +5 -2
- package/kernel/bin/setup.js +3 -17
- package/kernel/bin/tailscaled.js +23 -13
- package/kernel/index.js +11 -9
- package/kernel/shell.js +14 -6
- package/kernel/util.js +1 -0
- package/package.json +1 -1
- package/server/index.js +15 -19
- package/server/public/universal-launcher.js +5 -0
- package/server/views/anywhere.ejs +13 -13
- package/server/views/partials/main_sidebar.ejs +0 -1
- package/server/views/remote.ejs +12 -4
- package/system/pip_seed.json +6 -0
- package/system/repair_pip_seed.py +100 -0
- package/test/anywhere-remote.test.js +182 -8
- package/test/anywhere.test.js +1063 -131
- package/test/conda-bin.test.js +254 -4
- package/test/pip_seed_test.py +183 -0
- package/test/shell-run-template.test.js +147 -140
- package/test/tailscale-cli.test.js +239 -0
- package/kernel/anywhere/localapi.js +0 -147
package/kernel/anywhere/ports.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
to be free on the machine itself.
|
|
12
12
|
*/
|
|
13
13
|
const fs = require('fs')
|
|
14
|
-
const
|
|
14
|
+
const writeJson = require('./state')
|
|
15
15
|
|
|
16
16
|
const FIRST_PORT = 43000
|
|
17
17
|
const LAST_PORT = 43999
|
|
@@ -19,17 +19,14 @@ const LAST_PORT = 43999
|
|
|
19
19
|
async function loadPortMap(file) {
|
|
20
20
|
try {
|
|
21
21
|
const map = JSON.parse(await fs.promises.readFile(file, 'utf8'))
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
if (!map || typeof map !== 'object' || Array.isArray(map)) throw new Error('Invalid saved Anywhere port map')
|
|
23
|
+
return map
|
|
24
|
+
} catch (e) {
|
|
25
|
+
if (e.code !== 'ENOENT') throw e
|
|
24
26
|
return {}
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
async function savePortMap(file, map) {
|
|
29
|
-
await fs.promises.mkdir(path.dirname(file), { recursive: true })
|
|
30
|
-
await fs.promises.writeFile(file, JSON.stringify(map, null, 2))
|
|
31
|
-
}
|
|
32
|
-
|
|
33
30
|
// Returns a copy of `map` with a port for every key, giving new keys the
|
|
34
31
|
// lowest port not already taken. Existing assignments are never changed.
|
|
35
32
|
function assignPorts(map, keys) {
|
|
@@ -48,4 +45,4 @@ function assignPorts(map, keys) {
|
|
|
48
45
|
return { map: out, changed }
|
|
49
46
|
}
|
|
50
47
|
|
|
51
|
-
module.exports = { loadPortMap, savePortMap, assignPorts }
|
|
48
|
+
module.exports = { loadPortMap, savePortMap: writeJson, assignPorts, FIRST_PORT, LAST_PORT }
|
package/kernel/anywhere/serve.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
const DEFAULT_HTTPS_PORT = 443
|
|
17
|
+
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '0.0.0.0', '[::1]'])
|
|
18
|
+
const { isDeepStrictEqual } = require('util')
|
|
17
19
|
|
|
18
20
|
function normalizePort(value) {
|
|
19
21
|
const port = Number(value)
|
|
@@ -27,7 +29,7 @@ function normalizePort(value) {
|
|
|
27
29
|
options:
|
|
28
30
|
dnsName "pinokio.tail1234.ts.net" (no trailing dot)
|
|
29
31
|
serverPort pinokiod's own port (mapped to :443)
|
|
30
|
-
targets [{ listen, target }]: remote port -> local port
|
|
32
|
+
targets [{ listen, target, origin? }]: remote port -> local port/origin
|
|
31
33
|
*/
|
|
32
34
|
function buildServeConfig({ dnsName, serverPort, targets = [] } = {}) {
|
|
33
35
|
const host = String(dnsName || '').replace(/\.$/, '')
|
|
@@ -35,11 +37,11 @@ function buildServeConfig({ dnsName, serverPort, targets = [] } = {}) {
|
|
|
35
37
|
throw new Error('buildServeConfig requires dnsName')
|
|
36
38
|
}
|
|
37
39
|
const config = { TCP: {}, Web: {} }
|
|
38
|
-
const add = (listenPort, targetPort) => {
|
|
40
|
+
const add = (listenPort, targetPort, origin) => {
|
|
39
41
|
config.TCP[String(listenPort)] = { HTTPS: true }
|
|
40
42
|
config.Web[`${host}:${listenPort}`] = {
|
|
41
43
|
Handlers: {
|
|
42
|
-
'/': { Proxy: `http://127.0.0.1:${targetPort}` }
|
|
44
|
+
'/': { Proxy: origin || `http://127.0.0.1:${targetPort}` }
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
}
|
|
@@ -54,11 +56,11 @@ function buildServeConfig({ dnsName, serverPort, targets = [] } = {}) {
|
|
|
54
56
|
const target = normalizePort(raw && raw.target)
|
|
55
57
|
if (!listen || !target || seen.has(listen)) continue
|
|
56
58
|
seen.add(listen)
|
|
57
|
-
entries.push({ listen, target })
|
|
59
|
+
entries.push({ listen, target, origin: raw.origin })
|
|
58
60
|
}
|
|
59
61
|
// deterministic key order so unchanged mappings compare equal
|
|
60
62
|
for (const entry of entries.sort((a, b) => a.listen - b.listen)) {
|
|
61
|
-
add(entry.listen, entry.target)
|
|
63
|
+
add(entry.listen, entry.target, entry.origin)
|
|
62
64
|
}
|
|
63
65
|
return config
|
|
64
66
|
}
|
|
@@ -89,7 +91,15 @@ function describeServeConfig(config, dnsName, names = {}) {
|
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
function sameServeConfig(a, b) {
|
|
92
|
-
return
|
|
94
|
+
return isDeepStrictEqual(a || null, b || null)
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
|
|
97
|
+
function rewriteHref(href, routes) {
|
|
98
|
+
let url
|
|
99
|
+
try { url = new URL(String(href)) } catch (_) { return href }
|
|
100
|
+
if (!/^https?:$/.test(url.protocol) || !LOOPBACK_HOSTS.has(url.hostname) || !url.port) return href
|
|
101
|
+
const route = routes.find((entry) => entry.local === Number(url.port))
|
|
102
|
+
return route ? route.url + url.pathname + url.search + url.hash : href
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { buildServeConfig, describeServeConfig, sameServeConfig, rewriteHref, LOOPBACK_HOSTS, DEFAULT_HTTPS_PORT }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { randomUUID } = require('crypto')
|
|
4
|
+
|
|
5
|
+
module.exports = async function writeJson(file, value) {
|
|
6
|
+
await fs.promises.mkdir(path.dirname(file), { recursive: true })
|
|
7
|
+
const temporary = `${file}.${randomUUID()}.tmp`
|
|
8
|
+
try {
|
|
9
|
+
await fs.promises.writeFile(temporary, JSON.stringify(value, null, 2), { flag: 'wx' })
|
|
10
|
+
await fs.promises.rename(temporary, file)
|
|
11
|
+
} finally {
|
|
12
|
+
await fs.promises.rm(temporary, { force: true }).catch(() => {})
|
|
13
|
+
}
|
|
14
|
+
}
|
package/kernel/bin/conda-meta.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require('path')
|
|
|
3
3
|
const { execFile } = require('child_process')
|
|
4
4
|
const { managedPythonEnv } = require('../python_env')
|
|
5
5
|
const semver = require('semver')
|
|
6
|
-
const { PIP_MIN_VERSION } = require('./conda-pins')
|
|
6
|
+
const { PIP_MIN_VERSION, PIP_SEED_SCRIPT } = require('./conda-pins')
|
|
7
7
|
|
|
8
8
|
async function buildCondaListFromMeta(minicondaPath, useCondaList) {
|
|
9
9
|
if (!useCondaList) {
|
|
@@ -122,8 +122,26 @@ async function hasSecureManagedPip(prefix, platform = process.platform) {
|
|
|
122
122
|
})
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
async function runPipSeedRepair(prefix, wheel, platform = process.platform, check = false) {
|
|
126
|
+
const python = path.join(prefix, platform === 'win32' ? 'python.exe' : 'bin/python')
|
|
127
|
+
return await new Promise((resolve, reject) => {
|
|
128
|
+
execFile(python, ['-I', '-B', PIP_SEED_SCRIPT, wheel, prefix, ...(check ? ['--check'] : [])], {
|
|
129
|
+
env: managedPythonEnv(), windowsHide: true, timeout: 15000,
|
|
130
|
+
}, (error, stdout, stderr) => {
|
|
131
|
+
if (error) reject(new Error(`Managed Python pip seed ${check ? 'check' : 'repair'} failed: ${stderr || error.message}`))
|
|
132
|
+
else resolve(stdout)
|
|
133
|
+
})
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function hasSecureManagedPipSeed(prefix, wheel, platform = process.platform) {
|
|
138
|
+
return await runPipSeedRepair(prefix, wheel, platform, true).then(() => true, () => false)
|
|
139
|
+
}
|
|
140
|
+
|
|
125
141
|
module.exports = {
|
|
126
142
|
buildCondaListFromMeta,
|
|
127
143
|
managedCondaRuns,
|
|
128
144
|
hasSecureManagedPip,
|
|
145
|
+
hasSecureManagedPipSeed,
|
|
146
|
+
runPipSeedRepair,
|
|
129
147
|
}
|
package/kernel/bin/conda-pins.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const semver = require('semver')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { resolveAsarPath } = require('../util')
|
|
4
|
+
const PIP_WHEEL = require('../../system/pip_seed.json')
|
|
2
5
|
|
|
3
6
|
const CONDA_PIN_VERSION = "26.5.3"
|
|
4
7
|
// Conda 26.9.4 has no Python 3.10 builds. Apply its CA-bundle requirements
|
|
@@ -9,8 +12,8 @@ const TLS_PACKAGE_MINIMUMS = {
|
|
|
9
12
|
}
|
|
10
13
|
// conda-forge's patched pip requires Python 3.11; this official wheel supports 3.10.
|
|
11
14
|
// Bootstrap URL and hash: https://discuss.python.org/t/109576
|
|
12
|
-
const PIP_MIN_VERSION =
|
|
13
|
-
const
|
|
15
|
+
const PIP_MIN_VERSION = PIP_WHEEL.version
|
|
16
|
+
const PIP_SEED_SCRIPT = resolveAsarPath(path.resolve(__dirname, '../../system/repair_pip_seed.py'))
|
|
14
17
|
const TLS_PACKAGE_SPECS = Object.entries(TLS_PACKAGE_MINIMUMS).map(([name, version]) => `${name}>=${version}`)
|
|
15
18
|
const hasSecureTlsPackages = (versions = {}) => Object.entries(TLS_PACKAGE_MINIMUMS).every(([name, minimum]) => {
|
|
16
19
|
const version = semver.coerce(versions[name])
|
|
@@ -46,7 +49,8 @@ const isExpectedPythonPinned = (platform, version, build) => {
|
|
|
46
49
|
module.exports = {
|
|
47
50
|
CONDA_PIN_VERSION,
|
|
48
51
|
PIP_MIN_VERSION,
|
|
49
|
-
|
|
52
|
+
PIP_WHEEL,
|
|
53
|
+
PIP_SEED_SCRIPT,
|
|
50
54
|
TLS_PACKAGE_SPECS,
|
|
51
55
|
hasSecureTlsPackages,
|
|
52
56
|
PYTHON_INSTALL_SPEC,
|
package/kernel/bin/conda.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
const fs = require('fs')
|
|
2
2
|
const { glob } = require('glob')
|
|
3
3
|
const semver = require('semver')
|
|
4
|
-
const { createHash } = require('crypto')
|
|
4
|
+
const { createHash, randomUUID } = require('crypto')
|
|
5
5
|
const { isPathRemovalBlockedError, removePath } = require('../path_removal')
|
|
6
|
-
const { buildCondaListFromMeta, managedCondaRuns, hasSecureManagedPip } = require('./conda-meta')
|
|
6
|
+
const { buildCondaListFromMeta, managedCondaRuns, hasSecureManagedPip, hasSecureManagedPipSeed, runPipSeedRepair } = require('./conda-meta')
|
|
7
7
|
const {
|
|
8
8
|
CONDA_PIN_VERSION,
|
|
9
|
-
|
|
9
|
+
PIP_WHEEL,
|
|
10
10
|
TLS_PACKAGE_SPECS,
|
|
11
11
|
hasSecureTlsPackages,
|
|
12
12
|
PYTHON_INSTALL_SPEC,
|
|
@@ -76,6 +76,34 @@ class Conda {
|
|
|
76
76
|
throw new Error('Miniforge installer SHA-256 verification failed. The installer was not run and existing environments were preserved.')
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
async cachePipWheel(ondata) {
|
|
80
|
+
const destination = this.kernel.bin.path(PIP_WHEEL.filename)
|
|
81
|
+
const verified = async file => {
|
|
82
|
+
const hash = createHash('sha256')
|
|
83
|
+
try {
|
|
84
|
+
for await (const chunk of fs.createReadStream(file)) hash.update(chunk)
|
|
85
|
+
return hash.digest('hex') === PIP_WHEEL.sha256
|
|
86
|
+
} catch (error) {
|
|
87
|
+
if (error.code === 'ENOENT') return false
|
|
88
|
+
throw error
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (await verified(destination)) return destination
|
|
92
|
+
const temporary = `.pip-seed-${randomUUID()}.whl`
|
|
93
|
+
const temporaryPath = this.kernel.bin.path(temporary)
|
|
94
|
+
try {
|
|
95
|
+
await this.kernel.bin.download(PIP_WHEEL.url, temporary, ondata)
|
|
96
|
+
if (!(await verified(temporaryPath))) throw new Error('Pip wheel SHA-256 verification failed; the runtime was not changed.')
|
|
97
|
+
await fs.promises.rename(temporaryPath, destination)
|
|
98
|
+
} finally {
|
|
99
|
+
await fs.promises.rm(temporaryPath, { force: true })
|
|
100
|
+
}
|
|
101
|
+
return destination
|
|
102
|
+
}
|
|
103
|
+
async preparePipSeed(ondata) {
|
|
104
|
+
const wheel = await this.cachePipWheel(ondata)
|
|
105
|
+
await runPipSeedRepair(this.kernel.bin.path(CONDA_ROOT_DIR), wheel, this.kernel.platform)
|
|
106
|
+
}
|
|
79
107
|
env() {
|
|
80
108
|
let base = {
|
|
81
109
|
CONDA_PREFIX: this.kernel.bin.path(CONDA_ROOT_DIR),
|
|
@@ -121,7 +149,7 @@ report_errors: false`)
|
|
|
121
149
|
}
|
|
122
150
|
}
|
|
123
151
|
}
|
|
124
|
-
async check() {
|
|
152
|
+
async check({ pipSeed = true } = {}) {
|
|
125
153
|
let res = await buildCondaListFromMeta(this.kernel.bin.path(CONDA_ROOT_DIR))
|
|
126
154
|
|
|
127
155
|
let lines = res.response.split(/[\r\n]+/)
|
|
@@ -169,7 +197,9 @@ report_errors: false`)
|
|
|
169
197
|
if (!(conda_check.conda && conda_check.mamba && conda_check.python && hasSecureTlsPackages(conda_versions))) {
|
|
170
198
|
return false
|
|
171
199
|
}
|
|
172
|
-
return await hasSecureManagedPip(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform) &&
|
|
200
|
+
return await hasSecureManagedPip(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform) &&
|
|
201
|
+
(!pipSeed || await hasSecureManagedPipSeed(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.bin.path(PIP_WHEEL.filename), this.kernel.platform)) &&
|
|
202
|
+
await managedCondaRuns(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform)
|
|
173
203
|
}
|
|
174
204
|
async install(req, ondata) {
|
|
175
205
|
await this.check()
|
|
@@ -177,21 +207,41 @@ report_errors: false`)
|
|
|
177
207
|
if (isExpectedCondaPinned(versions.conda) &&
|
|
178
208
|
isExpectedPythonPinned(this.kernel.platform, versions.python, builds.python) &&
|
|
179
209
|
await managedCondaRuns(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform)) {
|
|
180
|
-
// Refresh a working runtime in place, preserving named app environments.
|
|
181
210
|
await this.init()
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
211
|
+
const solverReady = semver.satisfies(semver.coerce(versions['conda-libmamba-solver']) || '0.0.0', '>=25.4.0')
|
|
212
|
+
if (solverReady && hasSecureTlsPackages(versions) && await hasSecureManagedPip(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform)) {
|
|
213
|
+
// Seed/cache damage is repaired without reinstalling Python or Conda.
|
|
214
|
+
await this.preparePipSeed(ondata)
|
|
215
|
+
if (!(await this.check())) throw new Error('Managed Python pip seed repair failed; existing environments were preserved.')
|
|
216
|
+
return
|
|
217
|
+
}
|
|
218
|
+
// Refresh a working runtime in place, preserving named app environments.
|
|
219
|
+
await this.cachePipWheel(ondata)
|
|
220
|
+
if (!solverReady || !hasSecureTlsPackages(versions)) {
|
|
221
|
+
const result = await this.kernel.bin.exec({
|
|
222
|
+
message: [
|
|
223
|
+
`conda install -y${solverReady ? '' : ' --solver classic'} --override-channels -c conda-forge "${this.kernel.platform === "win32" ? WINDOWS_PYTHON_SSL_FIX_SPEC : PYTHON_INSTALL_SPEC}" "conda-libmamba-solver>=25.4.0" ${TLS_PACKAGE_SPECS.map(spec => `"${spec}"`).join(" ")}`,
|
|
224
|
+
],
|
|
225
|
+
}, ondata)
|
|
226
|
+
if (result?.error?.length) throw new Error("Conda certificate update failed; existing environments were preserved.")
|
|
227
|
+
}
|
|
228
|
+
if (!(await hasSecureManagedPip(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.platform))) {
|
|
229
|
+
const pip = await this.kernel.bin.exec({
|
|
230
|
+
message: { _: ['python', '-I', '-m', 'pip', '--isolated', 'install', '--no-index', PIP_WHEEL.filename] },
|
|
231
|
+
}, ondata)
|
|
232
|
+
if (pip?.error?.length) throw new Error("Conda certificate update failed; existing environments were preserved.")
|
|
233
|
+
}
|
|
234
|
+
await this.preparePipSeed(ondata)
|
|
235
|
+
if (!(await this.check())) throw new Error("Conda certificate update failed; existing environments were preserved.")
|
|
189
236
|
return
|
|
190
237
|
}
|
|
191
238
|
for(let i=0; i<5; i++) {
|
|
192
239
|
await this._install(req, ondata)
|
|
193
|
-
let installed = await this.check()
|
|
240
|
+
let installed = await this.check({ pipSeed: false })
|
|
194
241
|
if (installed) {
|
|
242
|
+
if (!(await hasSecureManagedPipSeed(this.kernel.bin.path(CONDA_ROOT_DIR), this.kernel.bin.path(PIP_WHEEL.filename), this.kernel.platform))) {
|
|
243
|
+
throw new Error('Managed Python pip seed verification failed; the runtime was preserved.')
|
|
244
|
+
}
|
|
195
245
|
console.log("Conda properly installed and updated")
|
|
196
246
|
return
|
|
197
247
|
} else {
|
|
@@ -200,6 +250,7 @@ report_errors: false`)
|
|
|
200
250
|
}
|
|
201
251
|
}
|
|
202
252
|
async _install(req, ondata) {
|
|
253
|
+
await this.cachePipWheel(ondata)
|
|
203
254
|
const installer_url = this.urls[this.kernel.platform][this.kernel.arch]
|
|
204
255
|
const installer = this.installer[this.kernel.platform]
|
|
205
256
|
const install_path = this.kernel.bin.path(CONDA_ROOT_DIR)
|
|
@@ -278,7 +329,7 @@ report_errors: false`)
|
|
|
278
329
|
let cmds = [
|
|
279
330
|
"conda clean -y --all",
|
|
280
331
|
`conda install -y --override-channels -c conda-forge ${condaPackages.join(" ")} ${mods}`.trim(),
|
|
281
|
-
|
|
332
|
+
{ _: ['python', '-I', '-m', 'pip', '--isolated', 'install', '--no-index', PIP_WHEEL.filename] },
|
|
282
333
|
]
|
|
283
334
|
await this.kernel.bin.exec({
|
|
284
335
|
message: cmds,
|
|
@@ -288,6 +339,7 @@ report_errors: false`)
|
|
|
288
339
|
}, (stream) => {
|
|
289
340
|
ondata(stream)
|
|
290
341
|
})
|
|
342
|
+
await this.preparePipSeed(ondata)
|
|
291
343
|
if (this.kernel.platform === "win32") {
|
|
292
344
|
// copy python.exe to python3.exe so you can run with both python3 and python
|
|
293
345
|
await fs.promises.copyFile(
|
package/kernel/bin/index.js
CHANGED
|
@@ -21,11 +21,12 @@ const LLVM = require('./llvm')
|
|
|
21
21
|
const VS = require("./vs")
|
|
22
22
|
const Cuda = require("./cuda")
|
|
23
23
|
const Torch = require("./torch")
|
|
24
|
-
const { buildCondaListFromMeta, managedCondaRuns, hasSecureManagedPip } = require('./conda-meta')
|
|
24
|
+
const { buildCondaListFromMeta, managedCondaRuns, hasSecureManagedPip, hasSecureManagedPipSeed } = require('./conda-meta')
|
|
25
25
|
const {
|
|
26
26
|
isExpectedCondaPinned,
|
|
27
27
|
isExpectedPythonPinned,
|
|
28
28
|
hasSecureTlsPackages,
|
|
29
|
+
PIP_WHEEL,
|
|
29
30
|
} = require('./conda-pins')
|
|
30
31
|
const { glob } = require('glob')
|
|
31
32
|
const fakeUa = require('fake-useragent');
|
|
@@ -414,7 +415,9 @@ class Bin {
|
|
|
414
415
|
}
|
|
415
416
|
|
|
416
417
|
if (conda_check.conda && conda_check.mamba && conda_check.python && hasSecureTlsPackages(conda_versions)) {
|
|
417
|
-
this.correct_conda = await hasSecureManagedPip(this.kernel.bin.path("miniforge"), this.platform) &&
|
|
418
|
+
this.correct_conda = await hasSecureManagedPip(this.kernel.bin.path("miniforge"), this.platform) &&
|
|
419
|
+
await hasSecureManagedPipSeed(this.kernel.bin.path("miniforge"), this.kernel.bin.path(PIP_WHEEL.filename), this.platform) &&
|
|
420
|
+
await managedCondaRuns(this.kernel.bin.path("miniforge"), this.platform)
|
|
418
421
|
}
|
|
419
422
|
}
|
|
420
423
|
this.installed.conda = conda
|
package/kernel/bin/setup.js
CHANGED
|
@@ -271,31 +271,17 @@ module.exports = {
|
|
|
271
271
|
}
|
|
272
272
|
},
|
|
273
273
|
anywhere: (kernel) => {
|
|
274
|
-
|
|
274
|
+
const requirements = [
|
|
275
275
|
{ name: "conda", },
|
|
276
|
-
{ name: "zip", },
|
|
277
|
-
]
|
|
278
|
-
if (platform === "darwin") {
|
|
279
|
-
requirements.push({ name: "brew" })
|
|
280
|
-
}
|
|
281
|
-
requirements = requirements.concat([
|
|
282
|
-
{ name: "git", },
|
|
283
|
-
{ name: "uv", },
|
|
284
|
-
{ name: "node", },
|
|
285
276
|
{ name: "tailscaled", },
|
|
286
|
-
|
|
287
|
-
{ name: "bluefairy" },
|
|
288
|
-
])
|
|
277
|
+
]
|
|
289
278
|
return {
|
|
290
279
|
icon: "fa-solid fa-mobile-screen",
|
|
291
280
|
title: "Pinokio Anywhere",
|
|
292
281
|
description: "Reach this Pinokio and its apps from your phone or another computer over a private Tailscale network",
|
|
293
282
|
requirements,
|
|
294
283
|
conda_requirements: [
|
|
295
|
-
|
|
296
|
-
"uv",
|
|
297
|
-
"git",
|
|
298
|
-
"tailscaled"
|
|
284
|
+
"tailscaled", "tailscale"
|
|
299
285
|
]
|
|
300
286
|
}
|
|
301
287
|
},
|
package/kernel/bin/tailscaled.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
const path = require('path')
|
|
2
|
-
const
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const { execFile } = require('child_process')
|
|
4
|
+
const { promisify } = require('util')
|
|
5
|
+
const run = promisify(execFile)
|
|
3
6
|
|
|
4
|
-
|
|
5
|
-
// drives it over its LocalAPI (see kernel/anywhere).
|
|
6
|
-
const TAILSCALED_SPEC = "tailscaled=1.104.1"
|
|
7
|
-
const TAILSCALED_REQUIREMENT = ">=1.104.0" // 1.104 added --windows-mode=dev
|
|
7
|
+
const VERSION = '1.104.1'
|
|
8
8
|
|
|
9
9
|
class Tailscaled {
|
|
10
10
|
description = "Installs the Tailscale daemon for Pinokio Anywhere: reach this Pinokio and its apps from your phone or another computer over a private network."
|
|
11
11
|
cmd() {
|
|
12
|
-
return
|
|
12
|
+
return `tailscaled=${VERSION} tailscale=${VERSION}`
|
|
13
13
|
}
|
|
14
14
|
binPath() {
|
|
15
15
|
if (this.kernel.platform === "win32") {
|
|
@@ -17,6 +17,9 @@ class Tailscaled {
|
|
|
17
17
|
}
|
|
18
18
|
return this.kernel.bin.path("miniforge", "bin", "tailscaled")
|
|
19
19
|
}
|
|
20
|
+
cliPath() {
|
|
21
|
+
return path.join(path.dirname(this.binPath()), this.kernel.platform === 'win32' ? 'tailscale.exe' : 'tailscale')
|
|
22
|
+
}
|
|
20
23
|
async install(req, ondata) {
|
|
21
24
|
await this.kernel.bin.exec({
|
|
22
25
|
message: [
|
|
@@ -30,15 +33,22 @@ class Tailscaled {
|
|
|
30
33
|
async installed() {
|
|
31
34
|
try {
|
|
32
35
|
const conda = this.kernel.bin.installed && this.kernel.bin.installed.conda
|
|
33
|
-
if (!conda || !conda.has(
|
|
36
|
+
if (!conda || !['tailscaled', 'tailscale'].every((name) => conda.has(name))) {
|
|
34
37
|
return false
|
|
35
38
|
}
|
|
36
39
|
const versions = this.kernel.bin.installed.conda_versions || {}
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
for (const [name, bin] of [['tailscaled', this.binPath()], ['tailscale', this.cliPath()]]) {
|
|
41
|
+
if (versions[name] !== VERSION) return false
|
|
42
|
+
const stat = await fs.promises.stat(bin)
|
|
43
|
+
const key = `${bin}:${stat.mtimeMs}:${stat.size}`
|
|
44
|
+
if (!this.verified) this.verified = new Set()
|
|
45
|
+
if (!this.verified.has(key)) {
|
|
46
|
+
const { stdout } = await run(bin, ['--version'], { shell: false, windowsHide: true, timeout: 5000 })
|
|
47
|
+
if (stdout.trim().split(/\s/)[0] !== VERSION) return false
|
|
48
|
+
this.verified.add(key)
|
|
49
|
+
}
|
|
40
50
|
}
|
|
41
|
-
return
|
|
51
|
+
return true
|
|
42
52
|
} catch (e) {
|
|
43
53
|
console.log("tailscaled installed check error", e)
|
|
44
54
|
return false
|
|
@@ -59,11 +69,11 @@ class Tailscaled {
|
|
|
59
69
|
async uninstall(req, ondata) {
|
|
60
70
|
ondata({ raw: "stopping Pinokio Anywhere\r\n" })
|
|
61
71
|
if (this.kernel.anywhere && typeof this.kernel.anywhere.stop === "function") {
|
|
62
|
-
await this.kernel.anywhere.stop()
|
|
72
|
+
await this.kernel.anywhere.stop()
|
|
63
73
|
}
|
|
64
74
|
ondata({ raw: "cleaning up\r\n" })
|
|
65
75
|
await this.kernel.bin.exec({
|
|
66
|
-
message: `conda remove -y tailscaled`,
|
|
76
|
+
message: `conda remove -y tailscaled tailscale`,
|
|
67
77
|
}, ondata)
|
|
68
78
|
ondata({ raw: "finished cleaning up\r\n" })
|
|
69
79
|
}
|
package/kernel/index.js
CHANGED
|
@@ -714,12 +714,10 @@ class Kernel {
|
|
|
714
714
|
}
|
|
715
715
|
// 8b. Pinokio Anywhere: refresh the tailscale serve config from the apps'
|
|
716
716
|
// published URLs. No-op unless Anywhere is on and its daemon is running.
|
|
717
|
-
if (this.anywhere) {
|
|
718
|
-
|
|
719
|
-
await this.anywhere.sync()
|
|
720
|
-
} catch (err) {
|
|
717
|
+
if (this.anywhere && !this.anywhere.syncing) {
|
|
718
|
+
this.anywhere.sync().catch((err) => {
|
|
721
719
|
console.warn('[anywhere] sync failed:', err && err.message ? err.message : err)
|
|
722
|
-
}
|
|
720
|
+
})
|
|
723
721
|
}
|
|
724
722
|
// 6. tell peers to refresh
|
|
725
723
|
let changed
|
|
@@ -1095,6 +1093,14 @@ class Kernel {
|
|
|
1095
1093
|
/// })
|
|
1096
1094
|
/// }
|
|
1097
1095
|
async init(options) {
|
|
1096
|
+
if (this.anywhere && this.homedir) {
|
|
1097
|
+
// Stop against the old home before changing paths or startup readiness.
|
|
1098
|
+
try { await this.anywhere.stop() }
|
|
1099
|
+
catch (error) {
|
|
1100
|
+
if (error.code !== 'ERR_ANYWHERE_UNOWNED') throw error
|
|
1101
|
+
console.warn('[anywhere]', error.message)
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1098
1104
|
// Re-arm startup readiness on every init cycle so restart waits for the
|
|
1099
1105
|
// newly rebuilt template/sysinfo state instead of a stale resolved promise.
|
|
1100
1106
|
this.sysReady = new Promise((resolve) => {
|
|
@@ -1113,10 +1119,6 @@ class Kernel {
|
|
|
1113
1119
|
if (this.peer) {
|
|
1114
1120
|
this.peer.stop()
|
|
1115
1121
|
}
|
|
1116
|
-
if (this.anywhere) {
|
|
1117
|
-
// re-initializing the kernel (home change): drop the old daemon
|
|
1118
|
-
await this.anywhere.stop().catch(() => {})
|
|
1119
|
-
}
|
|
1120
1122
|
|
|
1121
1123
|
this.info = new Info(this)
|
|
1122
1124
|
this.pipe = new Pipe(this)
|
package/kernel/shell.js
CHANGED
|
@@ -21,7 +21,7 @@ const ShellParser = require('./shell_parser')
|
|
|
21
21
|
const AnsiStreamTracker = require('./ansi_stream_tracker')
|
|
22
22
|
const ShellStateSync = require('./shell_state_sync')
|
|
23
23
|
const ShellRunTemplate = require('./api/shell_run_template')
|
|
24
|
-
const { PYTHON_INSTALL_SPEC,
|
|
24
|
+
const { PYTHON_INSTALL_SPEC, PIP_WHEEL, PIP_SEED_SCRIPT } = require('./bin/conda-pins')
|
|
25
25
|
const CondaRuntimeGuard = require('./shell_conda_runtime_guard')
|
|
26
26
|
const home = os.homedir()
|
|
27
27
|
const WINDOWS_CMD_CONDA_COMMAND_PATTERN = /(?:^|[&|()]\s*)@?\s*(?:call\s+)?(?:"[^"]*[\\/]conda(?:\.bat|\.exe)?"|(?:[a-z]:)?[^&|()\s"]*[\\/]conda(?:\.bat|\.exe)?|conda(?:\.bat|\.exe)?)\s+/i
|
|
@@ -1280,8 +1280,8 @@ class Shell {
|
|
|
1280
1280
|
// calling vcvars, but vcvars rebuilds LIB and drops that entry.
|
|
1281
1281
|
// Restore both the active Conda environment and managed CUDA import
|
|
1282
1282
|
// libraries after all compiler activation scripts have completed.
|
|
1283
|
-
//
|
|
1284
|
-
//
|
|
1283
|
+
// CALL's second expansion is required because these commands are
|
|
1284
|
+
// joined on one cmd.exe line and must read LIB after vcvars updates it.
|
|
1285
1285
|
conda_activation.push(`CALL set ^"LIB=%^CONDA_PREFIX%\\Library\\lib;%^CUDA_HOME%\\lib;%^LIB%^"`)
|
|
1286
1286
|
} catch (e) {
|
|
1287
1287
|
console.log('vc vars setup', e)
|
|
@@ -1396,12 +1396,20 @@ class Shell {
|
|
|
1396
1396
|
`python -m venv ${shellEnvPath}`,
|
|
1397
1397
|
activate_command,
|
|
1398
1398
|
// `python -m pip install --upgrade pip setuptools wheel`,
|
|
1399
|
-
deactivate_path,
|
|
1400
1399
|
// timeout,
|
|
1401
|
-
activate_command,
|
|
1402
1400
|
// timeout,
|
|
1403
|
-
this.isPowerShell() ? `${PIP_INSTALL_COMMAND}; if ($LASTEXITCODE -ne 0) { exit 1 }` : this.isCmdShell() ? `(${PIP_INSTALL_COMMAND} || exit 1)` : `${PIP_INSTALL_COMMAND} || exit 1`,
|
|
1404
1401
|
]
|
|
1402
|
+
// Base is prepared during setup; custom Conda Python needs its own seed.
|
|
1403
|
+
if (!conda_activate && (conda_path || (conda_name && conda_name !== 'base'))) {
|
|
1404
|
+
const prefix = conda_path ? path.resolve(params.path, conda_path) : this.kernel.bin.path('miniforge/envs', conda_name)
|
|
1405
|
+
if (normalizeComparablePath(prefix, this.platform) !== normalizeComparablePath(this.kernel.bin.path('miniforge'), this.platform)) {
|
|
1406
|
+
const args = [PIP_SEED_SCRIPT, this.kernel.bin.path(PIP_WHEEL.filename), prefix]
|
|
1407
|
+
const command = this.isCmdShell() ? `python -I -B ${args.map(value => `"${value}"`).join(' ')}` : this.build({ message: { _: ['python', '-I', '-B', ...args] } })
|
|
1408
|
+
venv_activation.unshift(this.isPowerShell()
|
|
1409
|
+
? `${command}; if (!$?) { exit 1 }`
|
|
1410
|
+
: this.isCmdShell() ? `(${command} || exit 1)` : `${command} || exit $?`)
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1405
1413
|
}
|
|
1406
1414
|
}
|
|
1407
1415
|
} else {
|
package/kernel/util.js
CHANGED