pinokiod 8.0.16 → 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.
@@ -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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.16",
3
+ "version": "8.0.17",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
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
- for (const runtimeName of ["miniconda", "miniforge"]) {
16307
- const runtimePath = this.kernel.path("bin", runtimeName)
16308
- await removePath(runtimePath, {
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: "Updating Miniforge",
16327
+ operation: "Resetting managed tools",
16313
16328
  })
16314
- }
16315
- res.json({ success: true })
16316
- } else if (req.body.type === 'bin') {
16317
- let folderPath = this.kernel.path("bin")
16318
- await removePath(folderPath, {
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")
@@ -820,6 +820,7 @@ document.addEventListener("DOMContentLoaded", async () => {
820
820
  }
821
821
  }
822
822
  if (options && options.fresh === "1") {
823
+ setInstallStatus("<b>Removing existing tools...</b><br>This can take a minute on Windows.")
823
824
  try {
824
825
  const result = await PinokioPathRemoval.request((background) => fetch("/pinokio/delete", {
825
826
  method: "post",
@@ -839,6 +840,7 @@ document.addEventListener("DOMContentLoaded", async () => {
839
840
  })
840
841
  return
841
842
  }
843
+ setInstallStatus("<b>Installing tools...</b><br>Starting the fresh installation.")
842
844
  }
843
845
  term.onData((data) => {
844
846
  if (shell_id) {
@@ -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
+ })