pinokiod 8.0.15 → 8.0.17
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/bin/conda.js +2 -0
- package/kernel/bin/index.js +15 -0
- package/package.json +1 -1
- package/server/index.js +25 -16
- package/server/views/install.ejs +23 -0
- package/server/views/setup.ejs +1 -1
- package/server/views/tools.ejs +1 -1
- package/test/conda-bin.test.js +78 -0
- package/test/path-removal-ui.test.js +59 -0
- package/test/tools-reinstall-ui.test.js +4 -4
package/kernel/bin/conda.js
CHANGED
|
@@ -185,7 +185,9 @@ report_errors: false`)
|
|
|
185
185
|
install_path_exists = await this.kernel.exists(`bin/${CONDA_ROOT_DIR}`)
|
|
186
186
|
if (install_path_exists) {
|
|
187
187
|
console.log("Removing existing install path...", install_path)
|
|
188
|
+
ondata({ raw: `removing existing Miniforge installation...\r\n` })
|
|
188
189
|
await this.removeInstallPath(install_path, ondata)
|
|
190
|
+
ondata({ raw: `existing Miniforge installation removed.\r\n` })
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
// 2. run the script
|
package/kernel/bin/index.js
CHANGED
|
@@ -715,7 +715,22 @@ class Bin {
|
|
|
715
715
|
let res = await Util.filepicker(req, ondata, this.kernel)
|
|
716
716
|
return res
|
|
717
717
|
}
|
|
718
|
+
async runInstallExclusive(task) {
|
|
719
|
+
const previous = this.installQueue || Promise.resolve()
|
|
720
|
+
let release
|
|
721
|
+
const current = new Promise((resolve) => { release = resolve })
|
|
722
|
+
this.installQueue = previous.then(() => current)
|
|
723
|
+
await previous
|
|
724
|
+
try {
|
|
725
|
+
return await task()
|
|
726
|
+
} finally {
|
|
727
|
+
release()
|
|
728
|
+
}
|
|
729
|
+
}
|
|
718
730
|
async install(req, ondata) {
|
|
731
|
+
return await this.runInstallExclusive(() => this.installRequirements(req, ondata))
|
|
732
|
+
}
|
|
733
|
+
async installRequirements(req, ondata) {
|
|
719
734
|
/*
|
|
720
735
|
req.params := {
|
|
721
736
|
client: {
|
package/package.json
CHANGED
package/server/index.js
CHANGED
|
@@ -16303,26 +16303,35 @@ class Server {
|
|
|
16303
16303
|
}
|
|
16304
16304
|
try {
|
|
16305
16305
|
if (req.body.type === 'conda-runtime') {
|
|
16306
|
-
|
|
16307
|
-
const
|
|
16308
|
-
|
|
16306
|
+
await this.kernel.bin.runInstallExclusive(async () => {
|
|
16307
|
+
for (const runtimeName of ["miniconda", "miniforge"]) {
|
|
16308
|
+
const runtimePath = this.kernel.path("bin", runtimeName)
|
|
16309
|
+
await removePath(runtimePath, {
|
|
16310
|
+
recursive: true,
|
|
16311
|
+
force: true,
|
|
16312
|
+
diagnose: req.body.background !== true,
|
|
16313
|
+
operation: "Updating Miniforge",
|
|
16314
|
+
})
|
|
16315
|
+
}
|
|
16316
|
+
})
|
|
16317
|
+
res.json({ success: true })
|
|
16318
|
+
} else if (req.body.type === 'bin') {
|
|
16319
|
+
let folderPath = this.kernel.path("bin")
|
|
16320
|
+
await this.kernel.bin.runInstallExclusive(async () => {
|
|
16321
|
+
const removalStartedAt = Date.now()
|
|
16322
|
+
console.log(`[fresh install] removing bin: start ${folderPath}`)
|
|
16323
|
+
await removePath(folderPath, {
|
|
16309
16324
|
recursive: true,
|
|
16310
16325
|
force: true,
|
|
16311
16326
|
diagnose: req.body.background !== true,
|
|
16312
|
-
operation: "
|
|
16327
|
+
operation: "Resetting managed tools",
|
|
16313
16328
|
})
|
|
16314
|
-
|
|
16315
|
-
|
|
16316
|
-
|
|
16317
|
-
|
|
16318
|
-
|
|
16319
|
-
recursive: true,
|
|
16320
|
-
force: true,
|
|
16321
|
-
diagnose: req.body.background !== true,
|
|
16322
|
-
operation: "Resetting managed tools",
|
|
16329
|
+
console.log(`[fresh install] removing bin: complete (${((Date.now() - removalStartedAt) / 1000).toFixed(1)}s)`)
|
|
16330
|
+
await fs.promises.mkdir(folderPath, { recursive: true }).catch((e) => { })
|
|
16331
|
+
const refreshStartedAt = Date.now()
|
|
16332
|
+
await this.kernel.bin.refreshInstalled()
|
|
16333
|
+
console.log(`[fresh install] refreshing installed state: complete (${((Date.now() - refreshStartedAt) / 1000).toFixed(1)}s)`)
|
|
16323
16334
|
})
|
|
16324
|
-
await fs.promises.mkdir(folderPath, { recursive: true }).catch((e) => { })
|
|
16325
|
-
await this.kernel.bin.refreshInstalled()
|
|
16326
16335
|
res.json({ success: true })
|
|
16327
16336
|
} else if (req.body.type === 'cache') {
|
|
16328
16337
|
let folderPath = this.kernel.path("cache")
|
|
@@ -16619,7 +16628,7 @@ class Server {
|
|
|
16619
16628
|
this.app.post("/pinokio/install", ex((req, res) => {
|
|
16620
16629
|
req.session.requirements = req.body.requirements
|
|
16621
16630
|
req.session.callback = req.body.callback
|
|
16622
|
-
res.redirect("/pinokio/install")
|
|
16631
|
+
res.redirect(req.query.fresh === "1" ? "/pinokio/install?fresh=1" : "/pinokio/install")
|
|
16623
16632
|
}))
|
|
16624
16633
|
this.app.post("/pinokio/install/validate", ex(async (req, res) => {
|
|
16625
16634
|
res.json({
|
package/server/views/install.ejs
CHANGED
|
@@ -819,6 +819,29 @@ document.addEventListener("DOMContentLoaded", async () => {
|
|
|
819
819
|
options[key] = value;
|
|
820
820
|
}
|
|
821
821
|
}
|
|
822
|
+
if (options && options.fresh === "1") {
|
|
823
|
+
setInstallStatus("<b>Removing existing tools...</b><br>This can take a minute on Windows.")
|
|
824
|
+
try {
|
|
825
|
+
const result = await PinokioPathRemoval.request((background) => fetch("/pinokio/delete", {
|
|
826
|
+
method: "post",
|
|
827
|
+
headers: { "Content-Type": "application/json" },
|
|
828
|
+
body: JSON.stringify({ type: "bin", background })
|
|
829
|
+
}))
|
|
830
|
+
if (result.cancelled) {
|
|
831
|
+
navigateToCallback()
|
|
832
|
+
return
|
|
833
|
+
}
|
|
834
|
+
if (result.error) throw new Error(result.error)
|
|
835
|
+
} catch (error) {
|
|
836
|
+
ModalInput({
|
|
837
|
+
title: "Error",
|
|
838
|
+
type: "modal",
|
|
839
|
+
description: error && error.message ? error.message : "Unable to reset managed tools."
|
|
840
|
+
})
|
|
841
|
+
return
|
|
842
|
+
}
|
|
843
|
+
setInstallStatus("<b>Installing tools...</b><br>Starting the fresh installation.")
|
|
844
|
+
}
|
|
822
845
|
term.onData((data) => {
|
|
823
846
|
if (shell_id) {
|
|
824
847
|
socket.emit({
|
package/server/views/setup.ejs
CHANGED
|
@@ -179,7 +179,7 @@
|
|
|
179
179
|
|
|
180
180
|
<div class="setup-secondary-actions">
|
|
181
181
|
<span class="task-inline-help">Not working?</span>
|
|
182
|
-
<button class="task-button" type="submit" form="install-form">
|
|
182
|
+
<button class="task-button" type="submit" form="install-form" formaction="/pinokio/install?fresh=1">
|
|
183
183
|
<i class="fa-solid fa-trash" aria-hidden="true"></i>
|
|
184
184
|
<span>Try a fresh install</span>
|
|
185
185
|
</button>
|
package/server/views/tools.ejs
CHANGED
|
@@ -495,7 +495,7 @@
|
|
|
495
495
|
<span>Install</span>
|
|
496
496
|
</a>
|
|
497
497
|
<% } else { %>
|
|
498
|
-
<form method="post" action="/pinokio/install">
|
|
498
|
+
<form method="post" action="/pinokio/install?fresh=1">
|
|
499
499
|
<input type="hidden" name="requirements" value="<%= JSON.stringify(bundle.requirements) %>">
|
|
500
500
|
<input type="hidden" name="callback" value="/tools">
|
|
501
501
|
<button class="task-link-button" type="submit">
|
package/test/conda-bin.test.js
CHANGED
|
@@ -425,6 +425,84 @@ test('Conda install keeps the old runtime when the replacement installer downloa
|
|
|
425
425
|
assert.equal(await pathExists(oldRuntimeFile), true)
|
|
426
426
|
})
|
|
427
427
|
|
|
428
|
+
test('Conda waits for runtime removal before starting the installer', async () => {
|
|
429
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-removal-order-'))
|
|
430
|
+
await createMiniforge(root)
|
|
431
|
+
const kernel = createKernel(root, 'win32')
|
|
432
|
+
const events = []
|
|
433
|
+
const output = []
|
|
434
|
+
let releaseRemoval
|
|
435
|
+
let removalStarted
|
|
436
|
+
const removalGate = new Promise((resolve) => { releaseRemoval = resolve })
|
|
437
|
+
const removalStartedPromise = new Promise((resolve) => { removalStarted = resolve })
|
|
438
|
+
|
|
439
|
+
kernel.bin.mods = []
|
|
440
|
+
kernel.bin.download = async () => events.push('download')
|
|
441
|
+
kernel.bin.rm = async () => events.push('installer-cleanup')
|
|
442
|
+
kernel.bin.exec = async (payload) => {
|
|
443
|
+
events.push(payload && payload.conda && payload.conda.skip ? 'installer' : 'conda-install')
|
|
444
|
+
if (payload && payload.conda && payload.conda.skip) {
|
|
445
|
+
await fs.mkdir(path.join(root, 'bin', 'miniforge', 'conda-meta'), { recursive: true })
|
|
446
|
+
await fs.writeFile(path.join(root, 'bin', 'miniforge', 'python.exe'), 'fake python\n')
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const conda = createConda(kernel)
|
|
451
|
+
const removeInstallPath = conda.removeInstallPath.bind(conda)
|
|
452
|
+
conda.removeInstallPath = async (...args) => {
|
|
453
|
+
events.push('remove-start')
|
|
454
|
+
removalStarted()
|
|
455
|
+
await removalGate
|
|
456
|
+
await removeInstallPath(...args)
|
|
457
|
+
events.push('remove-complete')
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const installation = conda._install({ dependencies: [] }, (data) => {
|
|
461
|
+
if (data && data.raw) output.push(data.raw)
|
|
462
|
+
})
|
|
463
|
+
await removalStartedPromise
|
|
464
|
+
|
|
465
|
+
assert.deepEqual(events, ['download', 'remove-start'])
|
|
466
|
+
assert.match(output.join(''), /removing existing Miniforge installation/)
|
|
467
|
+
|
|
468
|
+
releaseRemoval()
|
|
469
|
+
await installation
|
|
470
|
+
|
|
471
|
+
assert.ok(events.indexOf('remove-complete') < events.indexOf('installer'))
|
|
472
|
+
assert.match(output.join(''), /existing Miniforge installation removed/)
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
test('Bin serializes concurrent installs without dropping either request', async () => {
|
|
476
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-bin-install-single-flight-'))
|
|
477
|
+
const bin = createBin(root)
|
|
478
|
+
const events = []
|
|
479
|
+
let release
|
|
480
|
+
const gate = new Promise((resolve) => { release = resolve })
|
|
481
|
+
|
|
482
|
+
bin.installRequirements = async (req) => {
|
|
483
|
+
events.push(`start:${req.id}`)
|
|
484
|
+
if (req.id === 'first') await gate
|
|
485
|
+
events.push(`finish:${req.id}`)
|
|
486
|
+
return req.id
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const first = bin.install({ id: 'first' }, () => {})
|
|
490
|
+
const second = bin.install({ id: 'second' }, () => {})
|
|
491
|
+
await Promise.resolve()
|
|
492
|
+
assert.deepEqual(events, ['start:first'])
|
|
493
|
+
|
|
494
|
+
release()
|
|
495
|
+
assert.deepEqual(await Promise.all([first, second]), ['first', 'second'])
|
|
496
|
+
assert.deepEqual(events, ['start:first', 'finish:first', 'start:second', 'finish:second'])
|
|
497
|
+
|
|
498
|
+
bin.installRequirements = async (req) => {
|
|
499
|
+
if (req.id === 'failure') throw new Error('install failed')
|
|
500
|
+
return req.id
|
|
501
|
+
}
|
|
502
|
+
await assert.rejects(bin.install({ id: 'failure' }, () => {}), /install failed/)
|
|
503
|
+
assert.equal(await bin.install({ id: 'after-failure' }, () => {}), 'after-failure')
|
|
504
|
+
})
|
|
505
|
+
|
|
428
506
|
test('Conda install propagates bootstrap failure after replacing the runtime', async () => {
|
|
429
507
|
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-bootstrap-fails-'))
|
|
430
508
|
await createMiniforge(root)
|
|
@@ -457,3 +457,62 @@ test("successful runtime cleanup resubmits the interrupted install", async (t) =
|
|
|
457
457
|
fields: { requirements, callback: "/tools" },
|
|
458
458
|
})
|
|
459
459
|
})
|
|
460
|
+
|
|
461
|
+
test("fresh install shows removal status and waits for cleanup before starting", async (t) => {
|
|
462
|
+
const source = await fs.readFile(installViewPath, "utf8")
|
|
463
|
+
const html = ejs.render(source, {
|
|
464
|
+
agent: "web",
|
|
465
|
+
callback: "/tools",
|
|
466
|
+
requirements: '[{"name":"conda"}]',
|
|
467
|
+
theme: "dark",
|
|
468
|
+
}, { filename: installViewPath })
|
|
469
|
+
|
|
470
|
+
let cleanupStarted = false
|
|
471
|
+
let resolveCleanup
|
|
472
|
+
let socketStarted = false
|
|
473
|
+
const cleanup = new Promise((resolve) => { resolveCleanup = resolve })
|
|
474
|
+
const dom = new JSDOM(html, {
|
|
475
|
+
runScripts: "dangerously",
|
|
476
|
+
url: "http://localhost/pinokio/install?fresh=1",
|
|
477
|
+
beforeParse(window) {
|
|
478
|
+
window.fetch = async () => ({ json: async () => ({ config: {} }) })
|
|
479
|
+
window.matchMedia = () => ({ matches: false, addEventListener() {}, removeEventListener() {} })
|
|
480
|
+
window.ResizeObserver = class { observe() {} }
|
|
481
|
+
window.N = class { Noty() {} }
|
|
482
|
+
window.Terminal = class {
|
|
483
|
+
constructor() { this.cols = 80; this.rows = 24 }
|
|
484
|
+
attachCustomKeyEventHandler() {}
|
|
485
|
+
focus() {}
|
|
486
|
+
hasSelection() { return false }
|
|
487
|
+
loadAddon() {}
|
|
488
|
+
onData() {}
|
|
489
|
+
open() {}
|
|
490
|
+
write() {}
|
|
491
|
+
}
|
|
492
|
+
window.FitAddon = { FitAddon: class { fit() {} } }
|
|
493
|
+
window.WebLinksAddon = { WebLinksAddon: class {} }
|
|
494
|
+
window.xtermTheme = { FrontEndDelight: {} }
|
|
495
|
+
window.PinokioTouch = { bindTerminalFocus() {} }
|
|
496
|
+
window.PinokioPathRemoval = {
|
|
497
|
+
request: async () => {
|
|
498
|
+
cleanupStarted = true
|
|
499
|
+
return await cleanup
|
|
500
|
+
},
|
|
501
|
+
}
|
|
502
|
+
window.Socket = class {
|
|
503
|
+
emit() {}
|
|
504
|
+
run() { socketStarted = true }
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
})
|
|
508
|
+
t.after(() => dom.window.close())
|
|
509
|
+
|
|
510
|
+
await waitFor(() => cleanupStarted, "fresh cleanup request")
|
|
511
|
+
assert.match(dom.window.document.querySelector("#status-screen").textContent, /Removing existing tools/)
|
|
512
|
+
assert.match(dom.window.document.querySelector("#status-screen").textContent, /take a minute on Windows/)
|
|
513
|
+
assert.equal(socketStarted, false)
|
|
514
|
+
|
|
515
|
+
resolveCleanup({ success: true })
|
|
516
|
+
await waitFor(() => socketStarted, "installer socket")
|
|
517
|
+
assert.match(dom.window.document.querySelector("#status-screen").textContent, /Installing tools/)
|
|
518
|
+
})
|
|
@@ -36,11 +36,11 @@ test("Tools reinstall immediately submits a fresh install request", async (t) =>
|
|
|
36
36
|
})
|
|
37
37
|
t.after(() => dom.window.close())
|
|
38
38
|
|
|
39
|
-
const form = dom.window.document.querySelector('.tools-bundle-actions form[action="/pinokio/install"]')
|
|
39
|
+
const form = dom.window.document.querySelector('.tools-bundle-actions form[action="/pinokio/install?fresh=1"]')
|
|
40
40
|
form.addEventListener("submit", (event) => {
|
|
41
41
|
event.preventDefault()
|
|
42
42
|
submission = {
|
|
43
|
-
action: new URL(form.action).pathname
|
|
43
|
+
action: `${new URL(form.action).pathname}${new URL(form.action).search}`,
|
|
44
44
|
fields: Object.fromEntries(new dom.window.FormData(form)),
|
|
45
45
|
}
|
|
46
46
|
})
|
|
@@ -48,7 +48,7 @@ test("Tools reinstall immediately submits a fresh install request", async (t) =>
|
|
|
48
48
|
button.click()
|
|
49
49
|
|
|
50
50
|
assert.deepEqual(submission, {
|
|
51
|
-
action: "/pinokio/install",
|
|
51
|
+
action: "/pinokio/install?fresh=1",
|
|
52
52
|
fields: {
|
|
53
53
|
requirements: '[{"name":"conda","type":"bin","installed":true}]',
|
|
54
54
|
callback: "/tools",
|
|
@@ -58,6 +58,6 @@ test("Tools reinstall immediately submits a fresh install request", async (t) =>
|
|
|
58
58
|
|
|
59
59
|
test("setup fresh install immediately submits the existing install form", async () => {
|
|
60
60
|
const source = await fs.readFile(setupViewPath, "utf8")
|
|
61
|
-
assert.match(source, /<button class="task-button" type="submit" form="install-form">/)
|
|
61
|
+
assert.match(source, /<button class="task-button" type="submit" form="install-form" formaction="\/pinokio\/install\?fresh=1">/)
|
|
62
62
|
assert.doesNotMatch(source, /id="del-bin"|setup-reset-loading/)
|
|
63
63
|
})
|