pinokiod 8.0.16 → 8.0.18

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.
@@ -940,13 +940,7 @@ document.addEventListener("DOMContentLoaded", async () => {
940
940
  if (proceed) {
941
941
  document.querySelector(".reset-bin-loading").classList.remove("hidden")
942
942
  document.querySelector("#del-bin").classList.add("hidden")
943
- let res = await PinokioPathRemoval.request((background) => fetch("/pinokio/delete", {
944
- method: "post",
945
- headers: {
946
- "Content-Type": "application/json"
947
- },
948
- body: JSON.stringify({ type: "bin", background })
949
- }))
943
+ let res = await PinokioPathRemoval.remove({ type: "bin" })
950
944
  console.log(res)
951
945
  document.querySelector(".reset-bin-loading").classList.add("hidden")
952
946
  if (res.cancelled) {
@@ -447,13 +447,7 @@ document.addEventListener("DOMContentLoaded", async () => {
447
447
  }
448
448
  })
449
449
  try {
450
- const res = await PinokioPathRemoval.request((background) => fetch("/pinokio/delete", {
451
- method: "post",
452
- headers: {
453
- "Content-Type": "application/json"
454
- },
455
- body: JSON.stringify({ name: projectName, background })
456
- }))
450
+ const res = await PinokioPathRemoval.remove({ name: projectName })
457
451
  Swal.close()
458
452
  if (res && res.success) {
459
453
  navigateHome()
@@ -636,13 +636,7 @@ document.addEventListener("click", async (e) => {
636
636
  actions: "hidden"
637
637
  }
638
638
  });
639
- let res = await PinokioPathRemoval.request((background) => fetch("/pinokio/delete", {
640
- method: "post",
641
- headers: {
642
- "Content-Type": "application/json"
643
- },
644
- body: JSON.stringify({ name, background })
645
- }))
639
+ let res = await PinokioPathRemoval.remove({ name })
646
640
  console.log("res", res)
647
641
  Swal.close()
648
642
  if (res) {
@@ -4031,13 +4031,7 @@ document.addEventListener("click", async (e) => {
4031
4031
  actions: "hidden"
4032
4032
  }
4033
4033
  });
4034
- let res = await PinokioPathRemoval.request((background) => fetch("/pinokio/delete", {
4035
- method: "post",
4036
- headers: {
4037
- "Content-Type": "application/json"
4038
- },
4039
- body: JSON.stringify({ name, background })
4040
- }))
4034
+ let res = await PinokioPathRemoval.remove({ name })
4041
4035
  console.log("res", res)
4042
4036
  Swal.close()
4043
4037
  if (res) {
@@ -820,12 +820,9 @@ 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
- const result = await PinokioPathRemoval.request((background) => fetch("/pinokio/delete", {
825
- method: "post",
826
- headers: { "Content-Type": "application/json" },
827
- body: JSON.stringify({ type: "bin", background })
828
- }))
825
+ const result = await PinokioPathRemoval.remove({ type: "bin" })
829
826
  if (result.cancelled) {
830
827
  navigateToCallback()
831
828
  return
@@ -839,6 +836,7 @@ document.addEventListener("DOMContentLoaded", async () => {
839
836
  })
840
837
  return
841
838
  }
839
+ setInstallStatus("<b>Installing tools...</b><br>Starting the fresh installation.")
842
840
  }
843
841
  term.onData((data) => {
844
842
  if (shell_id) {
@@ -889,22 +887,7 @@ document.addEventListener("DOMContentLoaded", async () => {
889
887
  } else if (packet.type === "path.remove.blocked") {
890
888
  const result = await PinokioPathRemoval.show({
891
889
  details: packet.data,
892
- retry: async (manual) => {
893
- const response = await fetch("/pinokio/delete", {
894
- method: "post",
895
- headers: {
896
- "Content-Type": "application/json"
897
- },
898
- body: JSON.stringify({ type: "conda-runtime", background: !manual })
899
- })
900
- const payload = await response.json()
901
- const blocked = PinokioPathRemoval.blockedDetails(payload)
902
- if (!blocked && (!response.ok || (payload && payload.error))) {
903
- const message = payload && payload.error
904
- throw new Error(typeof message === "string" ? message : `Cleanup failed (${response.status})`)
905
- }
906
- return payload
907
- }
890
+ retry: () => PinokioPathRemoval.tryRemove({ type: "conda-runtime" })
908
891
  })
909
892
  if (result && result.success) {
910
893
  resumeInstall()
@@ -2361,13 +2361,7 @@ document.addEventListener("click", async (e) => {
2361
2361
  actions: "hidden"
2362
2362
  }
2363
2363
  });
2364
- let res = await PinokioPathRemoval.request((background) => fetch("/pinokio/delete", {
2365
- method: "post",
2366
- headers: {
2367
- "Content-Type": "application/json"
2368
- },
2369
- body: JSON.stringify({ name, background })
2370
- }))
2364
+ let res = await PinokioPathRemoval.remove({ name })
2371
2365
  console.log("res", res)
2372
2366
  Swal.close()
2373
2367
  if (res) {
@@ -1537,13 +1537,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1537
1537
  if (proceed) {
1538
1538
  document.querySelector(".reset-bin-loading").classList.remove("hidden")
1539
1539
  document.querySelector("#del-bin").classList.add("hidden")
1540
- let res = await PinokioPathRemoval.request((background) => fetch("/pinokio/delete", {
1541
- method: "post",
1542
- headers: {
1543
- "Content-Type": "application/json"
1544
- },
1545
- body: JSON.stringify({ type: "bin", background })
1546
- }))
1540
+ let res = await PinokioPathRemoval.remove({ type: "bin" })
1547
1541
  console.log(res)
1548
1542
  document.querySelector(".reset-bin-loading").classList.add("hidden")
1549
1543
  if (res.cancelled) {
@@ -1561,13 +1555,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1561
1555
  if (proceed) {
1562
1556
  document.querySelector(".reset-cache-loading").classList.remove("hidden")
1563
1557
  document.querySelector("#del-cache").classList.add("hidden")
1564
- let res = await PinokioPathRemoval.request((background) => fetch("/pinokio/delete", {
1565
- method: "post",
1566
- headers: {
1567
- "Content-Type": "application/json"
1568
- },
1569
- body: JSON.stringify({ type: "cache", background })
1570
- }))
1558
+ let res = await PinokioPathRemoval.remove({ type: "cache" })
1571
1559
  console.log(res)
1572
1560
  document.querySelector(".reset-cache-loading").classList.add("hidden")
1573
1561
  if (res.cancelled) {
@@ -3339,13 +3339,7 @@ document.addEventListener("DOMContentLoaded", async () => {
3339
3339
  if (proceed) {
3340
3340
  document.querySelector(".reset-bin-loading").classList.remove("hidden")
3341
3341
  document.querySelector("#del-bin").classList.add("hidden")
3342
- let res = await PinokioPathRemoval.request((background) => fetch("/pinokio/delete", {
3343
- method: "post",
3344
- headers: {
3345
- "Content-Type": "application/json"
3346
- },
3347
- body: JSON.stringify({ type: "bin", background })
3348
- }))
3342
+ let res = await PinokioPathRemoval.remove({ type: "bin" })
3349
3343
  console.log(res)
3350
3344
  document.querySelector(".reset-bin-loading").classList.add("hidden")
3351
3345
  if (res.cancelled) {
@@ -425,6 +425,53 @@ 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
+
428
475
  test('Conda install propagates bootstrap failure after replacing the runtime', async () => {
429
476
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-conda-bootstrap-fails-'))
430
477
  await createMiniforge(root)
@@ -142,10 +142,9 @@ test("path-removal modal renders identified blockers and escapes backend text",
142
142
 
143
143
  const shown = api.show({
144
144
  details: {
145
- operation: "Delete <app>",
146
145
  blockers: [{
147
146
  name: "G <HUB>",
148
- executable: "C:\\Program Files\\G & HUB\\lghub.exe",
147
+ serviceName: "lghub-service",
149
148
  pid: 4321,
150
149
  }],
151
150
  remainingCount: 25,
@@ -154,10 +153,11 @@ test("path-removal modal renders identified blockers and escapes backend text",
154
153
  retry: async () => ({ ok: true }),
155
154
  })
156
155
 
157
- assert.equal(state.fireOptions.title, "Delete &lt;app&gt;")
156
+ assert.equal(state.fireOptions.title, "File removal paused")
158
157
  assert.match(state.htmlContainer.innerHTML, /G &lt;HUB&gt;/)
159
158
  assert.doesNotMatch(state.htmlContainer.innerHTML, /G <HUB>/)
160
- assert.match(state.htmlContainer.textContent, /C:\\Program Files\\G & HUB\\lghub\.exe · PID 4321/)
159
+ assert.match(state.htmlContainer.textContent, /lghub-service · PID 4321/)
160
+ assert.match(state.htmlContainer.textContent, /because one or more files are still in use/)
161
161
  assert.match(state.htmlContainer.textContent, /Show first 20 of 25 remaining items/)
162
162
  assert.match(state.htmlContainer.textContent, /Close the app above/)
163
163
  assert.equal(state.htmlContainer.querySelector(".pinokio-path-blocker-icon"), null)
@@ -175,7 +175,6 @@ test("path-removal modal reports unattributed EACCES without naming an applicati
175
175
 
176
176
  const shown = api.show({
177
177
  details: {
178
- operation: "Delete app",
179
178
  causeCode: "EACCES",
180
179
  blockers: [],
181
180
  remainingCount: 1,
@@ -198,20 +197,19 @@ test("path-removal request retries automatically without overlapping and resolve
198
197
  t.after(() => dom.window.close())
199
198
 
200
199
  let requestCount = 0
201
- const backgroundRequests = []
200
+ let firstRequest
202
201
  let resolveRetry
203
202
  const retryResponse = new Promise((resolve) => {
204
203
  resolveRetry = resolve
205
204
  })
206
- const resultPromise = api.request(async (background) => {
205
+ dom.window.fetch = async (url, options) => {
207
206
  requestCount += 1
208
- backgroundRequests.push(background)
207
+ if (requestCount === 1) firstRequest = { url, options }
209
208
  if (requestCount === 1) {
210
209
  return response({
211
210
  success: false,
212
211
  error: {
213
212
  code: "PINOKIO_PATH_REMOVE_BLOCKED",
214
- operation: "Replace Miniforge",
215
213
  blockers: [{ name: "First blocker", pid: 10 }],
216
214
  remainingCount: 1,
217
215
  remaining: ["C:\\runtime\\first.dll"],
@@ -220,9 +218,13 @@ test("path-removal request retries automatically without overlapping and resolve
220
218
  }
221
219
  if (requestCount === 2) return await retryResponse
222
220
  return response({ ok: true }, { ok: true, status: 200 })
223
- })
221
+ }
222
+ const resultPromise = api.remove({ type: "bin" })
224
223
 
225
224
  await waitFor(() => state.fireOptions, "path-removal modal")
225
+ assert.equal(firstRequest.url, "/pinokio/delete")
226
+ assert.equal(firstRequest.options.method, "post")
227
+ assert.deepEqual(JSON.parse(firstRequest.options.body), { type: "bin" })
226
228
  const intervalId = state.getRetryIntervalId()
227
229
  assert.equal(state.intervals.get(intervalId).delay, 2000)
228
230
  state.htmlContainer.querySelector(".pinokio-path-blocker-files").open = true
@@ -237,7 +239,6 @@ test("path-removal request retries automatically without overlapping and resolve
237
239
  success: false,
238
240
  error: {
239
241
  code: "PINOKIO_PATH_REMOVE_BLOCKED",
240
- operation: "Replace Miniforge",
241
242
  blockers: [{ name: "Second blocker", pid: 20 }],
242
243
  remainingCount: 1,
243
244
  remaining: ["C:\\runtime\\second.dll"],
@@ -246,45 +247,52 @@ test("path-removal request retries automatically without overlapping and resolve
246
247
  await firstAttempt
247
248
  assert.equal(state.buttonsDisabled, false)
248
249
  assert.equal(state.htmlContainer.querySelector(".pinokio-path-blocker-files").open, true)
249
- assert.match(state.htmlContainer.textContent, /Second blocker/)
250
- assert.doesNotMatch(state.htmlContainer.textContent, /First blocker/)
250
+ assert.match(state.htmlContainer.textContent, /First blocker/)
251
+ assert.doesNotMatch(state.htmlContainer.textContent, /Second blocker/)
251
252
 
252
253
  await state.runInterval()
253
254
  const result = await resultPromise
254
255
  assert.equal(result.ok, true)
255
256
  assert.equal(requestCount, 3)
256
- assert.deepEqual(backgroundRequests, [false, true, true])
257
257
  assert.equal(state.closeCount, 1)
258
258
  assert.equal(state.intervals.has(intervalId), false)
259
259
  assert.ok(state.clearedIntervals.has(intervalId))
260
260
  })
261
261
 
262
- test("background pending response keeps the current blocker without rerendering", async (t) => {
262
+ test("background blocked response keeps the current modal without rerendering", async (t) => {
263
263
  const { api, dom, state } = await createHarness()
264
264
  t.after(() => dom.window.close())
265
265
 
266
266
  let requestCount = 0
267
- const shown = api.request(async () => {
267
+ dom.window.fetch = async () => {
268
268
  requestCount += 1
269
269
  if (requestCount === 1) {
270
270
  return response({
271
271
  error: {
272
272
  code: "PINOKIO_PATH_REMOVE_BLOCKED",
273
- operation: "Delete app",
274
273
  blockers: [{ name: "Original blocker", pid: 10 }],
275
274
  remainingCount: 1,
276
275
  remaining: ["C:\\app\\locked.dll"],
277
276
  },
278
277
  })
279
278
  }
280
- return response({ success: false, pending: true })
281
- })
279
+ return response({
280
+ error: {
281
+ code: "PINOKIO_PATH_REMOVE_BLOCKED",
282
+ blockers: [{ name: "Updated blocker", pid: 20 }],
283
+ remainingCount: 1,
284
+ remaining: ["C:\\app\\updated.dll"],
285
+ },
286
+ })
287
+ }
288
+ const shown = api.remove({ name: "app" })
282
289
 
283
290
  await waitFor(() => state.fireOptions, "path-removal modal")
284
291
  await state.runInterval()
285
292
  assert.equal(requestCount, 2)
286
293
  assert.equal(state.visible, true)
287
294
  assert.match(state.htmlContainer.textContent, /Original blocker/)
295
+ assert.doesNotMatch(state.htmlContainer.textContent, /Updated blocker/)
288
296
 
289
297
  state.dismiss()
290
298
  assert.equal((await shown).cancelled, true)
@@ -300,7 +308,6 @@ test("Stop remains available during a background retry and ignores its late resp
300
308
  })
301
309
  const shown = api.show({
302
310
  details: {
303
- operation: "Delete app",
304
311
  blockers: [{ name: "Original blocker", pid: 10 }],
305
312
  remainingCount: 1,
306
313
  remaining: ["C:\\app\\original.dll"],
@@ -313,7 +320,6 @@ test("Stop remains available during a background retry and ignores its late resp
313
320
  assert.equal(state.dismiss(), true)
314
321
  resolveRetry({
315
322
  code: "PINOKIO_PATH_REMOVE_BLOCKED",
316
- operation: "Delete app",
317
323
  blockers: [{ name: "Late blocker", pid: 20 }],
318
324
  remainingCount: 1,
319
325
  remaining: ["C:\\app\\late.dll"],
@@ -332,7 +338,6 @@ test("manual retry keeps the modal open when blocked and Stop cancels future ret
332
338
  let retryCount = 0
333
339
  const shown = api.show({
334
340
  details: {
335
- operation: "Delete app",
336
341
  blockers: [{ name: "Lock holder", pid: 99 }],
337
342
  remainingCount: 1,
338
343
  remaining: ["C:\\app\\locked.dll"],
@@ -341,7 +346,6 @@ test("manual retry keeps the modal open when blocked and Stop cancels future ret
341
346
  retryCount += 1
342
347
  return {
343
348
  code: "PINOKIO_PATH_REMOVE_BLOCKED",
344
- operation: "Delete app",
345
349
  blockers: [{ name: "Lock holder", pid: 99 }],
346
350
  remainingCount: 1,
347
351
  remaining: ["C:\\app\\locked.dll"],
@@ -369,7 +373,6 @@ test("manual retry escapes unexpected error messages", async (t) => {
369
373
 
370
374
  const shown = api.show({
371
375
  details: {
372
- operation: "Delete app",
373
376
  blockers: [],
374
377
  remainingCount: 1,
375
378
  remaining: ["C:\\app\\locked.dll"],
@@ -387,6 +390,27 @@ test("manual retry escapes unexpected error messages", async (t) => {
387
390
  assert.equal((await shown).cancelled, true)
388
391
  })
389
392
 
393
+ test("ordinary retry errors keep the modal open", async (t) => {
394
+ const { api, dom, state } = await createHarness()
395
+ t.after(() => dom.window.close())
396
+
397
+ const shown = api.show({
398
+ details: {
399
+ blockers: [],
400
+ remainingCount: 1,
401
+ remaining: ["C:\\app\\locked.dll"],
402
+ },
403
+ retry: async () => ({ error: "Cleanup failed" }),
404
+ })
405
+
406
+ assert.equal(await state.fireOptions.preConfirm(), false)
407
+ assert.equal(state.visible, true)
408
+ assert.equal(state.validationMessage, "Cleanup failed")
409
+
410
+ state.dismiss()
411
+ assert.equal((await shown).cancelled, true)
412
+ })
413
+
390
414
  test("successful runtime cleanup resubmits the interrupted install", async (t) => {
391
415
  const source = await fs.readFile(installViewPath, "utf8")
392
416
  const requirements = '[{"name":"conda"}]'
@@ -398,6 +422,7 @@ test("successful runtime cleanup resubmits the interrupted install", async (t) =
398
422
  }, { filename: installViewPath })
399
423
 
400
424
  let rpc
425
+ let cleanupPayload
401
426
  let submission
402
427
  let submitted
403
428
  const submittedPromise = new Promise((resolve) => { submitted = resolve })
@@ -424,8 +449,11 @@ test("successful runtime cleanup resubmits the interrupted install", async (t) =
424
449
  window.xtermTheme = { FrontEndDelight: {} }
425
450
  window.PinokioTouch = { bindTerminalFocus() {} }
426
451
  window.PinokioPathRemoval = {
427
- blockedDetails: () => null,
428
- show: async () => ({ success: true }),
452
+ show: async ({ retry }) => await retry(),
453
+ tryRemove: async (payload) => {
454
+ cleanupPayload = payload
455
+ return { success: true }
456
+ },
429
457
  }
430
458
  window.Socket = class {
431
459
  emit() {}
@@ -451,9 +479,69 @@ test("successful runtime cleanup resubmits the interrupted install", async (t) =
451
479
 
452
480
  await submittedPromise
453
481
  assert.equal(rpc.params, requirements)
482
+ assert.equal(cleanupPayload.type, "conda-runtime")
454
483
  assert.deepEqual(submission, {
455
484
  method: "post",
456
485
  pathname: "/pinokio/install",
457
486
  fields: { requirements, callback: "/tools" },
458
487
  })
459
488
  })
489
+
490
+ test("fresh install shows removal status and waits for cleanup before starting", async (t) => {
491
+ const source = await fs.readFile(installViewPath, "utf8")
492
+ const html = ejs.render(source, {
493
+ agent: "web",
494
+ callback: "/tools",
495
+ requirements: '[{"name":"conda"}]',
496
+ theme: "dark",
497
+ }, { filename: installViewPath })
498
+
499
+ let cleanupStarted = false
500
+ let resolveCleanup
501
+ let socketStarted = false
502
+ const cleanup = new Promise((resolve) => { resolveCleanup = resolve })
503
+ const dom = new JSDOM(html, {
504
+ runScripts: "dangerously",
505
+ url: "http://localhost/pinokio/install?fresh=1",
506
+ beforeParse(window) {
507
+ window.fetch = async () => ({ json: async () => ({ config: {} }) })
508
+ window.matchMedia = () => ({ matches: false, addEventListener() {}, removeEventListener() {} })
509
+ window.ResizeObserver = class { observe() {} }
510
+ window.N = class { Noty() {} }
511
+ window.Terminal = class {
512
+ constructor() { this.cols = 80; this.rows = 24 }
513
+ attachCustomKeyEventHandler() {}
514
+ focus() {}
515
+ hasSelection() { return false }
516
+ loadAddon() {}
517
+ onData() {}
518
+ open() {}
519
+ write() {}
520
+ }
521
+ window.FitAddon = { FitAddon: class { fit() {} } }
522
+ window.WebLinksAddon = { WebLinksAddon: class {} }
523
+ window.xtermTheme = { FrontEndDelight: {} }
524
+ window.PinokioTouch = { bindTerminalFocus() {} }
525
+ window.PinokioPathRemoval = {
526
+ remove: async () => {
527
+ cleanupStarted = true
528
+ return await cleanup
529
+ },
530
+ }
531
+ window.Socket = class {
532
+ emit() {}
533
+ run() { socketStarted = true }
534
+ }
535
+ },
536
+ })
537
+ t.after(() => dom.window.close())
538
+
539
+ await waitFor(() => cleanupStarted, "fresh cleanup request")
540
+ assert.match(dom.window.document.querySelector("#status-screen").textContent, /Removing existing tools/)
541
+ assert.match(dom.window.document.querySelector("#status-screen").textContent, /take a minute on Windows/)
542
+ assert.equal(socketStarted, false)
543
+
544
+ resolveCleanup({ success: true })
545
+ await waitFor(() => socketStarted, "installer socket")
546
+ assert.match(dom.window.document.querySelector("#status-screen").textContent, /Installing tools/)
547
+ })