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.
- package/kernel/bin/conda.js +3 -21
- package/kernel/path_removal.js +51 -50
- package/kernel/windows_restart_manager.js +4 -38
- package/package.json +1 -1
- package/server/index.js +9 -73
- package/server/public/common.js +27 -39
- package/server/public/style.css +3 -26
- package/server/views/app.ejs +1 -7
- package/server/views/download.ejs +1 -7
- package/server/views/editor.ejs +1 -7
- package/server/views/env_editor.ejs +1 -7
- package/server/views/file_explorer.ejs +1 -7
- package/server/views/index.ejs +1 -7
- package/server/views/install.ejs +4 -21
- package/server/views/net.ejs +1 -7
- package/server/views/settings.ejs +2 -14
- package/server/views/terminal.ejs +1 -7
- package/test/conda-bin.test.js +47 -0
- package/test/path-removal-ui.test.js +114 -26
- package/test/path-removal.test.js +203 -121
- package/test/shell-conda-runtime-guard.test.js +8 -2
- package/test/startup-git-index-route.test.js +0 -52
- package/test/windows-path-removal.integration.test.js +2 -12
package/server/views/editor.ejs
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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) {
|
package/server/views/index.ejs
CHANGED
|
@@ -4031,13 +4031,7 @@ document.addEventListener("click", async (e) => {
|
|
|
4031
4031
|
actions: "hidden"
|
|
4032
4032
|
}
|
|
4033
4033
|
});
|
|
4034
|
-
let res = await PinokioPathRemoval.
|
|
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) {
|
package/server/views/install.ejs
CHANGED
|
@@ -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.
|
|
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:
|
|
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()
|
package/server/views/net.ejs
CHANGED
|
@@ -2361,13 +2361,7 @@ document.addEventListener("click", async (e) => {
|
|
|
2361
2361
|
actions: "hidden"
|
|
2362
2362
|
}
|
|
2363
2363
|
});
|
|
2364
|
-
let res = await PinokioPathRemoval.
|
|
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.
|
|
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.
|
|
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.
|
|
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) {
|
package/test/conda-bin.test.js
CHANGED
|
@@ -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
|
-
|
|
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, "
|
|
156
|
+
assert.equal(state.fireOptions.title, "File removal paused")
|
|
158
157
|
assert.match(state.htmlContainer.innerHTML, /G <HUB>/)
|
|
159
158
|
assert.doesNotMatch(state.htmlContainer.innerHTML, /G <HUB>/)
|
|
160
|
-
assert.match(state.htmlContainer.textContent, /
|
|
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
|
-
|
|
200
|
+
let firstRequest
|
|
202
201
|
let resolveRetry
|
|
203
202
|
const retryResponse = new Promise((resolve) => {
|
|
204
203
|
resolveRetry = resolve
|
|
205
204
|
})
|
|
206
|
-
|
|
205
|
+
dom.window.fetch = async (url, options) => {
|
|
207
206
|
requestCount += 1
|
|
208
|
-
|
|
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, /
|
|
250
|
-
assert.doesNotMatch(state.htmlContainer.textContent, /
|
|
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
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
428
|
-
|
|
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
|
+
})
|