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.
@@ -11,7 +11,7 @@
11
11
  to be free on the machine itself.
12
12
  */
13
13
  const fs = require('fs')
14
- const path = require('path')
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
- return map && typeof map === 'object' && !Array.isArray(map) ? map : {}
23
- } catch (_) {
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 }
@@ -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 JSON.stringify(a || null) === JSON.stringify(b || null)
94
+ return isDeepStrictEqual(a || null, b || null)
93
95
  }
94
96
 
95
- module.exports = { buildServeConfig, describeServeConfig, sameServeConfig, DEFAULT_HTTPS_PORT }
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
+ }
@@ -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
  }
@@ -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 = '26.2.2'
13
- const PIP_INSTALL_COMMAND = 'python -I -m pip --isolated install --no-index "pip @ https://files.pythonhosted.org/packages/3f/06/3e4420302363fbb71be3c7f35ce042f54bfca330e3e66b3a2e3c4cca579f/pip-26.2.2-py3-none-any.whl#sha256=fd7f4dfa8190e014013b9f08f3e973f4e4c26ffd179a204895533a7254813a70"'
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
- PIP_INSTALL_COMMAND,
52
+ PIP_WHEEL,
53
+ PIP_SEED_SCRIPT,
50
54
  TLS_PACKAGE_SPECS,
51
55
  hasSecureTlsPackages,
52
56
  PYTHON_INSTALL_SPEC,
@@ -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
- PIP_INSTALL_COMMAND,
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) && await managedCondaRuns(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 result = await this.kernel.bin.exec({
183
- message: [
184
- `conda install -y --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(" ")}`,
185
- PIP_INSTALL_COMMAND,
186
- ],
187
- }, ondata)
188
- if (result?.error?.length || !(await this.check())) throw new Error("Conda certificate update failed; existing environments were preserved.")
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
- PIP_INSTALL_COMMAND,
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(
@@ -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) && await managedCondaRuns(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
@@ -271,31 +271,17 @@ module.exports = {
271
271
  }
272
272
  },
273
273
  anywhere: (kernel) => {
274
- let requirements = [
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
- { name: "py" },
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
- zip_cmd,
296
- "uv",
297
- "git",
298
- "tailscaled"
284
+ "tailscaled", "tailscale"
299
285
  ]
300
286
  }
301
287
  },
@@ -1,15 +1,15 @@
1
1
  const path = require('path')
2
- const semver = require('semver')
2
+ const fs = require('fs')
3
+ const { execFile } = require('child_process')
4
+ const { promisify } = require('util')
5
+ const run = promisify(execFile)
3
6
 
4
- // conda-forge ships only the daemon binary (no `tailscale` CLI); Pinokio
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 TAILSCALED_SPEC
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("tailscaled")) {
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 coerced = semver.coerce(versions.tailscaled)
38
- if (!coerced) {
39
- return false
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 semver.satisfies(coerced, TAILSCALED_REQUIREMENT)
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().catch(() => {})
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
- try {
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, PIP_INSTALL_COMMAND } = require('./bin/conda-pins')
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
- // Escape the quotes and variable names for interactive cmd.exe so
1284
- // CALL expands the current values only after activation has finished.
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
@@ -1247,6 +1247,7 @@ const rewrite_localhost= (kernel, obj, source) => {
1247
1247
 
1248
1248
  module.exports = {
1249
1249
  NestedLayout: require('./nested_layout'),
1250
+ resolveAsarPath,
1250
1251
  parse_env,
1251
1252
  log_path,
1252
1253
  api_path,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.2.10",
3
+ "version": "8.2.12",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {