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
|
@@ -7,7 +7,7 @@ const { spawnSync } = require('node:child_process')
|
|
|
7
7
|
|
|
8
8
|
const ShellRunTemplate = require('../kernel/api/shell_run_template')
|
|
9
9
|
const Shell = require('../kernel/shell')
|
|
10
|
-
const {
|
|
10
|
+
const { PIP_SEED_SCRIPT, PIP_WHEEL } = require('../kernel/bin/conda-pins')
|
|
11
11
|
|
|
12
12
|
function createKernel(root = process.cwd()) {
|
|
13
13
|
return {
|
|
@@ -399,11 +399,11 @@ test('redactEnvArgs summarizes protected argv env values', () => {
|
|
|
399
399
|
})
|
|
400
400
|
|
|
401
401
|
|
|
402
|
-
test('
|
|
402
|
+
test('venv selection preserves legacy activation without a per-venv pip install', async t => {
|
|
403
403
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-venv-selection-'))
|
|
404
404
|
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
405
405
|
const cases = [
|
|
406
|
-
{ venv:
|
|
406
|
+
{ venv: 'default-env' },
|
|
407
407
|
{ venv: { path: 'object-default-env' } },
|
|
408
408
|
{ venv: 'explicit-env', venv_python: '3.10', version: '3.10' },
|
|
409
409
|
{ venv: { path: 'object-env', python: '3.11' }, version: '3.11' },
|
|
@@ -413,26 +413,20 @@ test('only new default venvs bootstrap pip across supported shells', async t =>
|
|
|
413
413
|
]
|
|
414
414
|
await fs.mkdir(path.join(root, 'existing-env'))
|
|
415
415
|
for (const [platform, shellName] of [
|
|
416
|
-
['linux', 'bash'],
|
|
417
|
-
['
|
|
418
|
-
['win32', 'cmd.exe'],
|
|
419
|
-
['win32', 'powershell'],
|
|
420
|
-
['win32', 'bash'],
|
|
416
|
+
['linux', 'bash'], ['darwin', 'bash'], ['win32', 'cmd.exe'],
|
|
417
|
+
['win32', 'powershell'], ['win32', 'bash'],
|
|
421
418
|
]) {
|
|
422
419
|
for (const scenario of cases) {
|
|
423
420
|
const shell = createShell(createKernel(root))
|
|
424
421
|
shell.platform = platform
|
|
425
422
|
shell.shell = shellName
|
|
426
423
|
const params = await shell.activate({
|
|
427
|
-
path: root,
|
|
428
|
-
|
|
429
|
-
venv: scenario.venv,
|
|
430
|
-
venv_python: scenario.venv_python,
|
|
431
|
-
message: 'echo PINOKIO_REQUIREMENTS_RAN',
|
|
424
|
+
path: root, conda: { skip: true }, venv: scenario.venv,
|
|
425
|
+
venv_python: scenario.venv_python, message: 'echo PINOKIO_REQUIREMENTS_RAN',
|
|
432
426
|
})
|
|
433
427
|
const command = shell.build(params)
|
|
434
|
-
assert.
|
|
435
|
-
assert.
|
|
428
|
+
assert.match(command, /echo PINOKIO_REQUIREMENTS_RAN/)
|
|
429
|
+
assert.doesNotMatch(command, /--seed|--no-config|--python-preference|uv pip install|-m pip|https:|sha256|repair_pip_seed|\bCALL\s|\bexit\s/)
|
|
436
430
|
if (!scenario.venv || scenario.existing) {
|
|
437
431
|
assert.doesNotMatch(command, /python -m venv|uv venv/)
|
|
438
432
|
} else if (scenario.version) {
|
|
@@ -441,28 +435,17 @@ test('only new default venvs bootstrap pip across supported shells', async t =>
|
|
|
441
435
|
} else {
|
|
442
436
|
assert.match(command, /python -m venv /)
|
|
443
437
|
}
|
|
444
|
-
if (scenario.venv && !scenario.version && !scenario.existing) {
|
|
445
|
-
assert.equal(command.split(PIP_INSTALL_COMMAND).length - 1, 1)
|
|
446
|
-
assert.ok(command.indexOf(PIP_INSTALL_COMMAND) > command.lastIndexOf('activate'))
|
|
447
|
-
const envPath = path.resolve(root, typeof scenario.venv === 'string' ? scenario.venv : scenario.venv.path)
|
|
448
|
-
const shellPath = shell.isCmdShell() ? `"${envPath}"` : shell.quoteArgForShell(platform === 'win32' && /bash/i.test(shellName) ? require('../kernel/util').p2u(envPath) : envPath)
|
|
449
|
-
for (const step of [`python -m venv ${shellPath}`, PIP_INSTALL_COMMAND]) {
|
|
450
|
-
assert.ok(command.includes(shell.isPowerShell()
|
|
451
|
-
? `${step}; if ($LASTEXITCODE -ne 0) { exit 1 }`
|
|
452
|
-
: `${step} || exit 1`))
|
|
453
|
-
}
|
|
454
|
-
} else {
|
|
455
|
-
assert.doesNotMatch(command, /-m pip/)
|
|
456
|
-
}
|
|
457
438
|
if (scenario.venv) {
|
|
458
439
|
const envPath = path.resolve(root, typeof scenario.venv === 'string' ? scenario.venv : scenario.venv.path)
|
|
459
|
-
const script = path.resolve(envPath, platform === 'win32' ? 'Scripts' : 'bin',
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
assert.ok(command.includes(
|
|
465
|
-
|
|
440
|
+
const script = path.resolve(envPath, platform === 'win32' ? 'Scripts' : 'bin', 'activate')
|
|
441
|
+
const windowsBash = platform === 'win32' && /bash/i.test(shellName)
|
|
442
|
+
const scriptPath = windowsBash ? `"${require('../kernel/util').p2u(script)}"` : script
|
|
443
|
+
const target = windowsBash ? `"${require('../kernel/util').p2u(envPath)}"` : envPath
|
|
444
|
+
const activation = `${platform !== 'win32' || windowsBash ? 'source ' : ''}${scriptPath} ${target}`
|
|
445
|
+
assert.ok(command.includes(activation), command)
|
|
446
|
+
assert.equal(command.split(scriptPath).length - 1, scenario.version && !scenario.existing ? 2 : 1, command)
|
|
447
|
+
if (!scenario.version) assert.doesNotMatch(command, /\bdeactivate\b/i)
|
|
448
|
+
assert.equal(params.message[1], 'echo PINOKIO_REQUIREMENTS_RAN')
|
|
466
449
|
}
|
|
467
450
|
assert.equal(shell.env.UV_PYTHON_PREFERENCE, 'only-managed')
|
|
468
451
|
}
|
|
@@ -470,128 +453,152 @@ test('only new default venvs bootstrap pip across supported shells', async t =>
|
|
|
470
453
|
})
|
|
471
454
|
|
|
472
455
|
|
|
473
|
-
test('failed venv creation or pip bootstrap stops an unconditional command chain', async t => {
|
|
474
|
-
if (process.platform === 'win32') return t.skip('executes the POSIX shell failure guard')
|
|
475
|
-
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-failure-'))
|
|
476
|
-
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
477
|
-
const binDir = path.join(root, 'bin')
|
|
478
|
-
await fs.mkdir(binDir)
|
|
479
|
-
await fs.writeFile(path.join(binDir, 'python'), `#!/bin/sh
|
|
480
|
-
if [ "$1" = "-m" ] && [ "$2" = "venv" ]; then
|
|
481
|
-
[ "$PINOKIO_TEST_VENV_EXIT_CODE" = "0" ] || exit "$PINOKIO_TEST_VENV_EXIT_CODE"
|
|
482
|
-
mkdir -p "$3/bin"
|
|
483
|
-
echo 'deactivate() { :; }' > "$3/bin/activate"
|
|
484
|
-
else
|
|
485
|
-
echo PINOKIO_PIP_BOOTSTRAP_RAN
|
|
486
|
-
exit "$PINOKIO_TEST_PIP_EXIT_CODE"
|
|
487
|
-
fi
|
|
488
|
-
`, { mode: 0o755 })
|
|
489
|
-
const shell = createShell(createKernel(root))
|
|
490
|
-
shell.shell = 'bash'
|
|
491
|
-
const params = await shell.activate({
|
|
492
|
-
path: root, conda: { skip: true }, venv: 'env',
|
|
493
|
-
chain: '*', message: 'echo PINOKIO_APPLICATION_RAN',
|
|
494
|
-
})
|
|
495
|
-
const command = shell.build(params)
|
|
496
|
-
for (const [venvExit, pipExit] of [[9, 0], [0, 7], [0, 0]]) {
|
|
497
|
-
const result = spawnSync('/bin/bash', ['-c', command], {
|
|
498
|
-
encoding: 'utf8',
|
|
499
|
-
env: { ...process.env, PATH: `${binDir}${path.delimiter}${process.env.PATH}`, PINOKIO_TEST_VENV_EXIT_CODE: String(venvExit), PINOKIO_TEST_PIP_EXIT_CODE: String(pipExit) },
|
|
500
|
-
})
|
|
501
|
-
assert.equal(result.status, venvExit || pipExit ? 1 : 0, result.stderr)
|
|
502
|
-
assert.equal(result.stdout.includes('PINOKIO_APPLICATION_RAN'), venvExit === 0 && pipExit === 0)
|
|
503
|
-
assert.equal(result.stdout.includes('PINOKIO_PIP_BOOTSTRAP_RAN'), venvExit === 0)
|
|
504
|
-
}
|
|
505
|
-
})
|
|
506
456
|
|
|
507
457
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
test('only new stdlib venvs under custom Conda prefixes run the local seed repair', async t => {
|
|
462
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-custom-seed-'))
|
|
511
463
|
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
512
|
-
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
const created = path.join(root, 'created')
|
|
539
|
-
for (const existing of [false, true]) {
|
|
540
|
-
const shell = createShell(createKernel(root))
|
|
541
|
-
shell.shell = 'bash'
|
|
542
|
-
const params = await shell.activate({
|
|
543
|
-
path: project, conda: { skip: true }, venv,
|
|
544
|
-
...(explicit ? { venv_python: '3.10' } : {}),
|
|
545
|
-
chain: '*', message: 'printf "PINOKIO_APP_TARGET=%s\\n" "$VIRTUAL_ENV"',
|
|
546
|
-
})
|
|
547
|
-
const result = spawnSync('/bin/bash', ['-c', shell.build(params)], {
|
|
548
|
-
encoding: 'utf8', env: {
|
|
549
|
-
...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH}`,
|
|
550
|
-
VIRTUAL_ENV: '', NAME: 'must-not-expand',
|
|
551
|
-
PINOKIO_TEST_ENV: envPath, PINOKIO_TEST_CREATED: created, PINOKIO_TEST_ACTIVATE: fixture,
|
|
552
|
-
},
|
|
553
|
-
})
|
|
554
|
-
assert.equal(result.status, 0, result.stdout + result.stderr)
|
|
555
|
-
assert.equal(await fs.readFile(created, 'utf8'), envPath)
|
|
556
|
-
assert.ok(result.stdout.includes(`PINOKIO_APP_TARGET=${envPath}\n`), result.stdout)
|
|
557
|
-
assert.equal(result.stdout.includes(`PINOKIO_BOOTSTRAP_TARGET=${envPath}\n`), !explicit && !existing)
|
|
464
|
+
await fs.mkdir(path.join(root, 'existing-env'))
|
|
465
|
+
for (const [platform, shellName] of [['darwin', 'bash'], ['linux', 'bash'], ['win32', 'cmd.exe'], ['win32', 'powershell'], ['win32', 'bash']]) {
|
|
466
|
+
for (const [conda, expected] of [[undefined, false], [{ name: 'base' }, false],
|
|
467
|
+
['bin/miniforge', false], [{ skip: true }, false], [{ name: 'custom' }, true],
|
|
468
|
+
[{ path: "custom O'Brien & (test)" }, true], ['custom', true],
|
|
469
|
+
[{ name: 'custom', activate: 'minimal' }, false], [{ name: 'custom', activate: ['custom activation'] }, false]]) {
|
|
470
|
+
for (const kind of ['new', 'existing', 'uv']) {
|
|
471
|
+
const kernel = createKernel(root)
|
|
472
|
+
kernel.bin = { path: (...parts) => path.join(root, 'bin', ...parts) }
|
|
473
|
+
const shell = createShell(kernel)
|
|
474
|
+
shell.platform = platform
|
|
475
|
+
shell.shell = shellName
|
|
476
|
+
const params = await shell.activate({ path: root, conda,
|
|
477
|
+
venv: kind === 'existing' ? 'existing-env' : 'env',
|
|
478
|
+
...(kind === 'uv' ? { venv_python: '3.11' } : {}), message: 'echo PINOKIO_APPLICATION_RAN' })
|
|
479
|
+
const command = shell.build(params)
|
|
480
|
+
const repair = expected && kind === 'new'
|
|
481
|
+
assert.equal(command.includes('repair_pip_seed.py'), repair, command)
|
|
482
|
+
assert.doesNotMatch(command, /https:|sha256|-m pip/)
|
|
483
|
+
if (repair) {
|
|
484
|
+
assert.ok(command.indexOf('repair_pip_seed.py') < command.indexOf('python -m venv'), command)
|
|
485
|
+
assert.ok(command.includes(PIP_WHEEL.filename), command)
|
|
486
|
+
if (shell.isCmdShell()) assert.doesNotMatch(command.slice(command.indexOf('python -I -B')), /\^/)
|
|
487
|
+
assert.equal((command.match(/\bexit 1\b/g) || []).length, shell.isCmdShell() || shell.isPowerShell() ? 1 : 0, command)
|
|
488
|
+
}
|
|
489
|
+
}
|
|
558
490
|
}
|
|
559
491
|
}
|
|
560
492
|
})
|
|
561
493
|
|
|
562
|
-
test('
|
|
563
|
-
if (process.platform === 'win32') return t.skip('executes bash
|
|
564
|
-
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-
|
|
494
|
+
test('custom Conda repair failure is retried after the Conda prefix already exists', async t => {
|
|
495
|
+
if (process.platform === 'win32') return t.skip('executes bash command chains')
|
|
496
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-custom-seed-retry-'))
|
|
565
497
|
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
566
498
|
const bin = path.join(root, 'bin')
|
|
567
499
|
await fs.mkdir(bin)
|
|
500
|
+
const trace = path.join(root, 'trace')
|
|
501
|
+
const prefix = path.join(root, "custom O'Brien & (test)")
|
|
568
502
|
await fs.writeFile(path.join(bin, 'python'), `#!/bin/sh
|
|
569
|
-
if [ "$1" = "-
|
|
503
|
+
if [ "$1" = "-I" ]; then
|
|
504
|
+
[ "$#" = "5" ] && [ "$5" = "$PINOKIO_TEST_PREFIX" ] || exit 9
|
|
505
|
+
echo repair >> "$PINOKIO_TEST_TRACE"
|
|
506
|
+
exit "$PINOKIO_TEST_REPAIR_EXIT"
|
|
507
|
+
elif [ "$1" = "-m" ] && [ "$2" = "venv" ]; then
|
|
508
|
+
echo create >> "$PINOKIO_TEST_TRACE"
|
|
570
509
|
mkdir -p "$3/bin"
|
|
571
|
-
|
|
572
|
-
echo 'deactivate() { rm -f "$PINOKIO_TEST_ENV/bin/activate"; }' > "$3/bin/activate"
|
|
573
|
-
fi
|
|
510
|
+
echo 'deactivate() { :; }' > "$3/bin/activate"
|
|
574
511
|
else
|
|
575
|
-
|
|
512
|
+
exit 9
|
|
576
513
|
fi
|
|
577
514
|
`, { mode: 0o755 })
|
|
578
|
-
for (const failure of [
|
|
579
|
-
const
|
|
580
|
-
|
|
581
|
-
const shell = createShell(
|
|
515
|
+
for (const failure of [true, false]) {
|
|
516
|
+
const kernel = createKernel(root)
|
|
517
|
+
kernel.bin = { path: (...parts) => path.join(bin, ...parts) }
|
|
518
|
+
const shell = createShell(kernel)
|
|
582
519
|
shell.shell = 'bash'
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
chain: '*', message: 'echo PINOKIO_APPLICATION_RAN'
|
|
520
|
+
shell.conda_hook = () => ''
|
|
521
|
+
const params = await shell.activate({ path: root, conda: { path: path.basename(prefix) }, venv: 'env',
|
|
522
|
+
chain: '*', message: 'echo PINOKIO_APPLICATION_RAN' })
|
|
523
|
+
assert.equal(shell.build(params).includes('conda create'), failure)
|
|
524
|
+
const result = spawnSync('/bin/bash', ['-c', `conda() { if [ "$1" = create ]; then mkdir -p "$4"; fi; }\n${shell.build(params)}`], {
|
|
525
|
+
encoding: 'utf8', env: { ...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH}`,
|
|
526
|
+
PINOKIO_TEST_REPAIR_EXIT: failure ? '7' : '0', PINOKIO_TEST_TRACE: trace, PINOKIO_TEST_PREFIX: prefix },
|
|
586
527
|
})
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
528
|
+
assert.equal(result.status, failure ? 7 : 0, result.stdout + result.stderr)
|
|
529
|
+
assert.equal(result.stdout.includes('PINOKIO_APPLICATION_RAN'), !failure)
|
|
530
|
+
assert.equal(await fs.access(path.join(root, 'env')).then(() => true, () => false), !failure)
|
|
531
|
+
}
|
|
532
|
+
assert.equal(await fs.readFile(trace, 'utf8'), 'repair\nrepair\ncreate\n')
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
test('uv retains its existing creation and deactivation failure behavior', async t => {
|
|
540
|
+
if (process.platform === 'win32') return t.skip('executes bash commands')
|
|
541
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-setup-uv-'))
|
|
542
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
543
|
+
await fs.mkdir(path.join(root, 'bin'))
|
|
544
|
+
await fs.writeFile(path.join(root, 'bin', 'uv'), `#!/bin/sh
|
|
545
|
+
mkdir -p "$2/bin"
|
|
546
|
+
echo 'deactivate() { return 7; }' > "$2/bin/activate"
|
|
547
|
+
exit 7
|
|
548
|
+
`, { mode: 0o755 })
|
|
549
|
+
const shell = createShell(createKernel(root))
|
|
550
|
+
shell.shell = 'bash'
|
|
551
|
+
const params = await shell.activate({ path: root, conda: { skip: true }, venv: 'env', venv_python: '3.11',
|
|
552
|
+
message: 'echo APP' })
|
|
553
|
+
const result = spawnSync('/bin/bash', ['-c', shell.build(params)], {
|
|
554
|
+
encoding: 'utf8', env: { ...process.env, PATH: `${path.join(root, 'bin')}${path.delimiter}${process.env.PATH}` },
|
|
555
|
+
})
|
|
556
|
+
assert.equal(result.status, 0, result.stderr)
|
|
557
|
+
assert.equal(result.stdout, 'APP\n')
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
test('failed custom seed repair stops venv creation and all app commands', async t => {
|
|
561
|
+
const powershell = process.env.PINOKIO_TEST_PWSH || (process.platform === 'win32' ? 'powershell.exe' : null)
|
|
562
|
+
for (const [name, executable] of [
|
|
563
|
+
['bash', process.platform !== 'win32' ? '/bin/bash' : null],
|
|
564
|
+
['CMD', process.platform === 'win32' ? 'cmd.exe' : null],
|
|
565
|
+
['PowerShell', powershell],
|
|
566
|
+
]) {
|
|
567
|
+
await t.test(name, { skip: !executable && `requires ${name}` }, async t => {
|
|
568
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-seed-stop-'))
|
|
569
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }))
|
|
570
|
+
const bin = path.join(root, 'bin')
|
|
571
|
+
const trace = path.join(root, 'trace')
|
|
572
|
+
await fs.mkdir(bin)
|
|
573
|
+
await fs.mkdir(path.join(root, 'custom'))
|
|
574
|
+
await fs.writeFile(path.join(bin, process.platform === 'win32' ? 'python.bat' : 'python'),
|
|
575
|
+
process.platform === 'win32'
|
|
576
|
+
? '@echo %1>>"%PINOKIO_TEST_TRACE%"\r\n@exit /b 7\r\n'
|
|
577
|
+
: '#!/bin/sh\nprintf "%s\\n" "$1" >> "$PINOKIO_TEST_TRACE"\nexit 7\n', { mode: 0o755 })
|
|
578
|
+
if (name === 'CMD') await fs.writeFile(path.join(bin, 'conda.bat'), '@exit /b 0\r\n')
|
|
579
|
+
const chains = name === 'PowerShell' ? ['*'] : [undefined, '&', '|', '*']
|
|
580
|
+
for (const chain of chains) {
|
|
581
|
+
const kernel = createKernel(root)
|
|
582
|
+
kernel.bin = { path: (...parts) => path.join(bin, ...parts) }
|
|
583
|
+
const shell = createShell(kernel)
|
|
584
|
+
shell.shell = executable
|
|
585
|
+
shell.conda_hook = () => ''
|
|
586
|
+
const message = name === 'PowerShell' ? 'Write-Output FIRST; Write-Output SECOND'
|
|
587
|
+
: name === 'CMD' ? 'echo FIRST & echo SECOND' : 'echo FIRST; echo SECOND'
|
|
588
|
+
const params = await shell.activate({ path: root, conda: { path: 'custom' }, venv: 'env', chain, message })
|
|
589
|
+
const command = (name === 'bash' ? 'conda() { :; }\n' : name === 'PowerShell' ? 'function conda {}\n' : '') + shell.build(params)
|
|
590
|
+
const result = spawnSync(executable, name === 'CMD' ? ['/D', '/Q']
|
|
591
|
+
: name === 'PowerShell' ? ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', command] : ['-c', command], {
|
|
592
|
+
...(name === 'CMD' ? { input: `${command}\r\nexit\r\n` } : {}),
|
|
593
|
+
cwd: root, encoding: 'utf8', timeout: 15000,
|
|
594
|
+
env: { ...process.env, PATH: `${bin}${path.delimiter}${process.env.PATH}`, PINOKIO_TEST_TRACE: trace },
|
|
595
|
+
})
|
|
596
|
+
assert.ifError(result.error)
|
|
597
|
+
assert.equal(result.status, name === 'bash' ? 7 : 1, result.stdout + result.stderr)
|
|
598
|
+
assert.doesNotMatch(result.stdout, /FIRST|SECOND/)
|
|
599
|
+
assert.equal(await fs.access(path.join(root, 'env')).then(() => true, () => false), false)
|
|
600
|
+
}
|
|
601
|
+
assert.deepEqual((await fs.readFile(trace, 'utf8')).trim().split(/\r?\n/), chains.map(() => '-I'))
|
|
592
602
|
})
|
|
593
|
-
assert.equal(result.status, 1, `${failure}: ${result.stdout}${result.stderr}`)
|
|
594
|
-
assert.equal(result.stdout.includes('PINOKIO_BOOTSTRAP_RAN'), false, failure)
|
|
595
|
-
assert.equal(result.stdout.includes('PINOKIO_APPLICATION_RAN'), false, failure)
|
|
596
603
|
}
|
|
597
604
|
})
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const assert = require('node:assert/strict')
|
|
2
|
+
const test = require('node:test')
|
|
3
|
+
const http = require('node:http')
|
|
4
|
+
const fs = require('node:fs')
|
|
5
|
+
const os = require('node:os')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
const vm = require('node:vm')
|
|
8
|
+
const { EventEmitter } = require('node:events')
|
|
9
|
+
const TailscaleCLI = require('../kernel/anywhere/cli')
|
|
10
|
+
const Anywhere = require('../kernel/anywhere')
|
|
11
|
+
const Tailscaled = require('../kernel/bin/tailscaled')
|
|
12
|
+
|
|
13
|
+
test('CLI login delivers each streamed update once and rejects output-handler failures', async () => {
|
|
14
|
+
const source = fs.readFileSync(require.resolve('../kernel/anywhere/cli'), 'utf8')
|
|
15
|
+
const cliRequire = require('node:module').createRequire(require.resolve('../kernel/anywhere/cli'))
|
|
16
|
+
for (const failure of [null, 'json', 'callback']) {
|
|
17
|
+
let finish, killed = false
|
|
18
|
+
const child = { stdout: new EventEmitter(), kill() { killed = true } }
|
|
19
|
+
const module = { exports: {} }
|
|
20
|
+
vm.runInNewContext(source, { module, require(name) {
|
|
21
|
+
if (name !== 'child_process') return cliRequire(name)
|
|
22
|
+
return { execFile(file, args, options, callback) { finish = callback; return child } }
|
|
23
|
+
} })
|
|
24
|
+
const cli = new module.exports({ binPath: 'fixture', socketPath: 'fixture' })
|
|
25
|
+
const seen = []
|
|
26
|
+
let updates = 0
|
|
27
|
+
const result = cli.up(value => {
|
|
28
|
+
updates++
|
|
29
|
+
if (failure === 'callback') throw new Error('handler failed')
|
|
30
|
+
seen.push(value.AuthURL)
|
|
31
|
+
})
|
|
32
|
+
const checked = failure ? assert.rejects(result, failure === 'json' ? { name: 'SyntaxError' } : /handler failed/) : result
|
|
33
|
+
const first = '{\n "AuthURL": "first"\n}\n'
|
|
34
|
+
const second = '{\n "AuthURL": "second"\n}\n'
|
|
35
|
+
assert.doesNotThrow(() => {
|
|
36
|
+
child.stdout.emit('data', failure === 'json' ? '{\nINVALID JSON\n}\n' : first)
|
|
37
|
+
if (!failure) {
|
|
38
|
+
child.stdout.emit('data', second.slice(0, 10))
|
|
39
|
+
child.stdout.emit('data', second.slice(10))
|
|
40
|
+
} else {
|
|
41
|
+
child.stdout.emit('data', second)
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
finish(null, '', '')
|
|
45
|
+
await checked
|
|
46
|
+
assert.equal(killed, !!failure)
|
|
47
|
+
assert.equal(cli.children.size, 0)
|
|
48
|
+
assert.equal(updates, failure === 'json' ? 0 : failure === 'callback' ? 1 : 2)
|
|
49
|
+
if (!failure) assert.deepEqual(seen, ['first', 'second'])
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
// Run against the actual pinned binary on each native platform. No system
|
|
54
|
+
// Tailscale endpoint, account or network settings are used by these tests.
|
|
55
|
+
const binPath = process.env.PINOKIO_TEST_TAILSCALE
|
|
56
|
+
test('pinned CLI: login, approvals, conditional Serve writes, failures and cancellation', { skip: !binPath, timeout: 30000 }, async () => {
|
|
57
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-cli-'))
|
|
58
|
+
const socketPath = process.platform === 'win32' ? `\\\\.\\pipe\\pinokio-test-${process.pid}` : path.join(tmp, 'api.sock')
|
|
59
|
+
const seen = []
|
|
60
|
+
let config = null
|
|
61
|
+
let revision = 1
|
|
62
|
+
let conflict = false
|
|
63
|
+
let failure = 0
|
|
64
|
+
let bus = null
|
|
65
|
+
let loginNumber = 0
|
|
66
|
+
let status = {
|
|
67
|
+
BackendState: 'Running', Version: '1.104.1', TailscaleIPs: ['100.100.100.100'],
|
|
68
|
+
Self: { DNSName: 'pinokio.tail1.ts.net.', TailscaleIPs: ['100.100.100.100'] },
|
|
69
|
+
CertDomains: ['pinokio.tail1.ts.net'], CurrentTailnet: { MagicDNSSuffix: 'tail1.ts.net' },
|
|
70
|
+
}
|
|
71
|
+
const server = http.createServer((req, res) => {
|
|
72
|
+
let body = ''
|
|
73
|
+
req.on('data', (chunk) => { body += chunk })
|
|
74
|
+
req.on('end', () => {
|
|
75
|
+
seen.push({ method: req.method, url: req.url, body, etag: req.headers['if-match'] })
|
|
76
|
+
res.setHeader('Content-Type', 'application/json')
|
|
77
|
+
res.setHeader('Tailscale-Version', '1.104.1')
|
|
78
|
+
if (req.url.startsWith('/localapi/v0/status')) {
|
|
79
|
+
if (failure) { res.statusCode = failure; return res.end('control failure') }
|
|
80
|
+
return res.end(JSON.stringify(status))
|
|
81
|
+
}
|
|
82
|
+
if (req.url === '/localapi/v0/serve-config') {
|
|
83
|
+
if (req.method === 'POST') {
|
|
84
|
+
if (conflict || req.headers['if-match'] !== `v${revision}`) { res.statusCode = 412; return res.end('changed concurrently') }
|
|
85
|
+
config = JSON.parse(body); revision += 1
|
|
86
|
+
return res.end('{}')
|
|
87
|
+
}
|
|
88
|
+
res.setHeader('Etag', `v${revision}`)
|
|
89
|
+
return res.end(JSON.stringify(config))
|
|
90
|
+
}
|
|
91
|
+
if (req.url === '/localapi/v0/query-feature?feature=serve') return res.end(JSON.stringify({ Complete: !!status.CertDomains, URL: 'https://login.tailscale.com/f/serve?node=1', Text: 'Approve HTTPS' }))
|
|
92
|
+
if (req.url === '/localapi/v0/prefs' || req.url === '/localapi/v0/check-prefs') return res.end('{}')
|
|
93
|
+
if (req.url.startsWith('/localapi/v0/watch-ipn-bus')) { bus = res; res.flushHeaders(); return }
|
|
94
|
+
if (req.url === '/localapi/v0/start' || req.url === '/localapi/v0/login-interactive') {
|
|
95
|
+
if (bus && req.url.endsWith('/login-interactive')) bus.write(JSON.stringify({ BrowseToURL: `https://login.tailscale.com/a/fresh${++loginNumber}` }) + '\n')
|
|
96
|
+
res.statusCode = 204; return res.end()
|
|
97
|
+
}
|
|
98
|
+
if (req.url === '/localapi/v0/logout') { status.BackendState = 'NeedsLogin'; res.statusCode = 204; return res.end() }
|
|
99
|
+
res.statusCode = 404; res.end(`unexpected ${req.url}`)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
const connections = new Set()
|
|
103
|
+
server.on('connection', (socket) => { connections.add(socket); socket.on('close', () => connections.delete(socket)) })
|
|
104
|
+
const cli = new TailscaleCLI({ binPath, socketPath })
|
|
105
|
+
try {
|
|
106
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(socketPath, resolve) })
|
|
107
|
+
assert.equal((await cli.status()).Version, '1.104.1')
|
|
108
|
+
await cli.setPrefs('pinokio')
|
|
109
|
+
const prefs = JSON.parse(seen.find((r) => r.method === 'PATCH' && r.url.endsWith('/prefs')).body)
|
|
110
|
+
assert.equal(prefs.CorpDNS, false)
|
|
111
|
+
assert.equal(prefs.RouteAll, false)
|
|
112
|
+
assert.equal(prefs.Hostname, 'pinokio')
|
|
113
|
+
status.CertDomains = null
|
|
114
|
+
assert.equal((await cli.queryFeature()).URL, 'https://login.tailscale.com/f/serve?node=1')
|
|
115
|
+
status.CertDomains = ['pinokio.tail1.ts.net']
|
|
116
|
+
|
|
117
|
+
const kernel = {
|
|
118
|
+
platform: process.platform, homedir: tmp, server_port: 42000,
|
|
119
|
+
path: (...parts) => path.join(tmp, ...parts),
|
|
120
|
+
memory: { local: { [path.join(tmp, 'api/comfy/start.js')]: { url: 'http://127.0.0.1:7860' } } },
|
|
121
|
+
bin: { mod: { tailscaled: { installed: async () => true } } },
|
|
122
|
+
}
|
|
123
|
+
const anywhere = new Anywhere(kernel)
|
|
124
|
+
anywhere.readPid = () => 123 // The mock endpoint represents a verified owned daemon.
|
|
125
|
+
anywhere.api = cli
|
|
126
|
+
await anywhere.saveConfig({ enabled: true })
|
|
127
|
+
assert.equal((await anywhere.snapshot()).phase, 'ready', JSON.stringify({ config, seen }))
|
|
128
|
+
assert.equal(config.Web['pinokio.tail1.ts.net:443'].Handlers['/'].Proxy, 'http://127.0.0.1:42000')
|
|
129
|
+
assert.equal(config.Web['pinokio.tail1.ts.net:43000'].Handlers['/'].Proxy, 'http://127.0.0.1:7860')
|
|
130
|
+
assert.equal(config.AllowFunnel, undefined)
|
|
131
|
+
assert.equal(await anywhere.sync(), false, 'unchanged routes do not write')
|
|
132
|
+
const portMap = fs.readFileSync(anywhere.portMapPath(), 'utf8')
|
|
133
|
+
kernel.memory.local[path.join(tmp, 'api/comfy/start.js')].url = 'http://127.0.0.1:7861'
|
|
134
|
+
conflict = true
|
|
135
|
+
assert.equal((await anywhere.snapshot()).phase, 'error')
|
|
136
|
+
assert.equal(anywhere.rewriteHref('pinokio.tail1.ts.net', 'http://127.0.0.1:7861/'), 'http://127.0.0.1:7861/')
|
|
137
|
+
assert.equal(config.Web['pinokio.tail1.ts.net:43000'].Handlers['/'].Proxy, 'http://127.0.0.1:7860')
|
|
138
|
+
conflict = false
|
|
139
|
+
assert.equal((await anywhere.snapshot()).phase, 'ready')
|
|
140
|
+
assert.equal(fs.readFileSync(anywhere.portMapPath(), 'utf8'), portMap)
|
|
141
|
+
assert.equal(config.Web['pinokio.tail1.ts.net:43000'].Handlers['/'].Proxy, 'http://127.0.0.1:7861')
|
|
142
|
+
for (const [origin, expected] of [
|
|
143
|
+
['https://127.0.0.1', 'https://127.0.0.1'],
|
|
144
|
+
['https://localhost', 'https://localhost'],
|
|
145
|
+
['http://[::1]', 'http://[::1]'],
|
|
146
|
+
['http://0.0.0.0', 'http://127.0.0.1'],
|
|
147
|
+
]) {
|
|
148
|
+
kernel.memory.local[path.join(tmp, 'api/comfy/start.js')].url = `${origin}:7861/ui/`
|
|
149
|
+
assert.equal((await anywhere.snapshot()).phase, 'ready')
|
|
150
|
+
assert.equal(config.Web['pinokio.tail1.ts.net:43000'].Handlers['/'].Proxy, `${expected}:7861`)
|
|
151
|
+
}
|
|
152
|
+
kernel.memory.local = {}
|
|
153
|
+
await anywhere.sync()
|
|
154
|
+
assert.deepEqual(Object.keys(config.TCP), ['443'])
|
|
155
|
+
assert.ok(seen.filter((r) => r.method === 'POST' && r.url.endsWith('/serve-config')).every((r) => /^v\d+$/.test(r.etag)))
|
|
156
|
+
|
|
157
|
+
for (const code of [401, 403, 503]) {
|
|
158
|
+
failure = code
|
|
159
|
+
await assert.rejects(() => cli.ping(), /control failure/, `HTTP ${code} is not absence`)
|
|
160
|
+
}
|
|
161
|
+
failure = 0
|
|
162
|
+
await cli.logout()
|
|
163
|
+
let gotLink
|
|
164
|
+
const link = new Promise((resolve) => { gotLink = resolve })
|
|
165
|
+
const pending = cli.up((update) => { if (update.AuthURL) gotLink(update.AuthURL) })
|
|
166
|
+
const completed = pending.catch((error) => error)
|
|
167
|
+
assert.equal(await link, 'https://login.tailscale.com/a/fresh1')
|
|
168
|
+
cli.cancel()
|
|
169
|
+
assert.equal((await completed).cancelled, true)
|
|
170
|
+
let gotFresh
|
|
171
|
+
const fresh = new Promise((resolve) => { gotFresh = resolve })
|
|
172
|
+
const retried = cli.up((update) => { if (update.AuthURL) gotFresh(update.AuthURL) }, true)
|
|
173
|
+
const retriedResult = retried.catch((error) => error)
|
|
174
|
+
assert.equal(await fresh, 'https://login.tailscale.com/a/fresh2')
|
|
175
|
+
status.BackendState = 'Running'
|
|
176
|
+
bus.write(JSON.stringify({ State: 6, SelfChange: { Key: 'nodekey:' + '01'.repeat(32) } }) + '\n')
|
|
177
|
+
assert.equal(typeof await retriedResult, 'string')
|
|
178
|
+
} finally {
|
|
179
|
+
cli.cancel()
|
|
180
|
+
for (const socket of connections) socket.destroy()
|
|
181
|
+
await new Promise((resolve) => server.close(resolve))
|
|
182
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
test('native daemon: setup, simultaneous start, adoption, stop and saved Off', {
|
|
187
|
+
skip: !binPath || !process.env.PINOKIO_TEST_TAILSCALED, timeout: 45000,
|
|
188
|
+
}, async () => {
|
|
189
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'pk-native-'))
|
|
190
|
+
const binDir = path.join(tmp, 'managed tools')
|
|
191
|
+
fs.mkdirSync(binDir)
|
|
192
|
+
const cliBin = path.join(binDir, path.basename(binPath))
|
|
193
|
+
const daemonBin = path.join(binDir, path.basename(process.env.PINOKIO_TEST_TAILSCALED))
|
|
194
|
+
fs.copyFileSync(binPath, cliBin)
|
|
195
|
+
fs.copyFileSync(process.env.PINOKIO_TEST_TAILSCALED, daemonBin)
|
|
196
|
+
const mod = new Tailscaled()
|
|
197
|
+
const kernel = {
|
|
198
|
+
platform: process.platform, homedir: tmp, path: (...parts) => path.join(tmp, ...parts),
|
|
199
|
+
bin: { mod: { tailscaled: mod }, installed: { conda: new Set(['tailscaled', 'tailscale']),
|
|
200
|
+
conda_versions: { tailscaled: '1.104.1', tailscale: '1.104.1' } } },
|
|
201
|
+
}
|
|
202
|
+
mod.kernel = kernel
|
|
203
|
+
mod.binPath = () => daemonBin
|
|
204
|
+
mod.cliPath = () => cliBin
|
|
205
|
+
const first = new Anywhere(kernel)
|
|
206
|
+
if (process.platform === 'win32') first._socketPath = `\\\\.\\pipe\\pinokio-native-test-${process.pid}`
|
|
207
|
+
let adopted
|
|
208
|
+
try {
|
|
209
|
+
assert.equal(await mod.installed(), true, 'both actual executable versions match')
|
|
210
|
+
await first.saveConfig({ enabled: true })
|
|
211
|
+
await Promise.all([first.start(), first.start(), first.start()])
|
|
212
|
+
assert.ok(first.child && first.child.pid)
|
|
213
|
+
const pid = first.child.pid
|
|
214
|
+
const statePath = first.stateDir('tailscaled.state')
|
|
215
|
+
// A fresh daemon must show sign-in or the pending CLI flow, never ready.
|
|
216
|
+
const snap = await first.snapshot()
|
|
217
|
+
assert.ok(['starting', 'login'].includes(snap.phase), JSON.stringify(snap))
|
|
218
|
+
const deadline = Date.now() + 15000
|
|
219
|
+
while (!first.login.url && !first.error && Date.now() < deadline) await new Promise((resolve) => setTimeout(resolve, 100))
|
|
220
|
+
assert.ok(first.login.url, first.error || 'native login did not produce a URL within 15 seconds')
|
|
221
|
+
first.client().cancel()
|
|
222
|
+
if (first.login) await first.login.promise
|
|
223
|
+
adopted = new Anywhere(kernel)
|
|
224
|
+
adopted._socketPath = first.socketPath()
|
|
225
|
+
await adopted.start()
|
|
226
|
+
assert.equal(adopted.child, null, 'a second controller uses the existing endpoint')
|
|
227
|
+
assert.equal(first.child.pid, pid, 'no duplicate daemon')
|
|
228
|
+
assert.ok(fs.existsSync(statePath))
|
|
229
|
+
await adopted.disable()
|
|
230
|
+
assert.equal((await adopted.snapshot()).phase, 'off')
|
|
231
|
+
assert.equal(await adopted.autostart(), false)
|
|
232
|
+
assert.ok(fs.existsSync(statePath), 'disable retains node state')
|
|
233
|
+
assert.equal(await new TailscaleCLI({ binPath: cliBin, socketPath: first.socketPath() }).ping(), false)
|
|
234
|
+
} finally {
|
|
235
|
+
try { first.killSync() } catch (_) { }
|
|
236
|
+
try { if (adopted) adopted.killSync() } catch (_) { }
|
|
237
|
+
fs.rmSync(tmp, { recursive: true, force: true })
|
|
238
|
+
}
|
|
239
|
+
})
|