pinokiod 8.0.8 → 8.0.9

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.
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs')
2
2
  const { glob } = require('glob')
3
3
  const semver = require('semver')
4
+ const { isPathRemovalBlockedError, removePath } = require('../path_removal')
4
5
  const { buildCondaListFromMeta, managedCondaRuns } = require('./conda-meta')
5
6
  const {
6
7
  CONDA_PIN_VERSION,
@@ -171,11 +172,7 @@ report_errors: false`)
171
172
  await this.kernel.bin.download(installer_url, installer, ondata)
172
173
 
173
174
  const caddy = this.kernel.bin.mod && this.kernel.bin.mod.caddy
174
- let caddyWasRunning = false
175
- if (caddy && typeof caddy.running === "function") {
176
- caddyWasRunning = await caddy.running()
177
- }
178
- if (caddyWasRunning) {
175
+ if (caddy && typeof caddy.running === "function" && await caddy.running()) {
179
176
  ondata({ raw: `stopping Home Server before replacing Miniforge...\r\n` })
180
177
  await caddy.stop()
181
178
  }
@@ -183,12 +180,12 @@ report_errors: false`)
183
180
  legacy_path_exists = await this.kernel.exists(`bin/${LEGACY_CONDA_ROOT_DIR}`)
184
181
  if (legacy_path_exists) {
185
182
  console.log("Removing legacy install path...", legacy_path)
186
- await this.removeInstallPath(legacy_path)
183
+ await this.removeInstallPath(legacy_path, ondata)
187
184
  }
188
185
  install_path_exists = await this.kernel.exists(`bin/${CONDA_ROOT_DIR}`)
189
186
  if (install_path_exists) {
190
187
  console.log("Removing existing install path...", install_path)
191
- await this.removeInstallPath(install_path)
188
+ await this.removeInstallPath(install_path, ondata)
192
189
  }
193
190
 
194
191
  // 2. run the script
@@ -213,9 +210,6 @@ report_errors: false`)
213
210
  }
214
211
 
215
212
  const dependencies = new Set(Array.isArray(req.dependencies) ? req.dependencies : [])
216
- if (caddyWasRunning) {
217
- dependencies.add("caddy")
218
- }
219
213
  let mods = this.kernel.bin.mods.filter((m) => {
220
214
  return dependencies.has(m.name)
221
215
  }).map((m) => {
@@ -255,12 +249,8 @@ report_errors: false`)
255
249
  await this.ensureCompatibilityAlias()
256
250
  ondata({ raw: `Install finished\r\n` })
257
251
  await this.kernel.bin.rm(installer, ondata)
258
- if (caddyWasRunning) {
259
- ondata({ raw: `restarting Home Server after replacing Miniforge...\r\n` })
260
- await caddy.start()
261
- }
262
252
  }
263
- async removeInstallPath(target) {
253
+ async removeInstallPath(target, ondata) {
264
254
  try {
265
255
  const stat = await fs.promises.lstat(target).catch((error) => {
266
256
  if (error && error.code === "ENOENT") {
@@ -272,16 +262,26 @@ report_errors: false`)
272
262
  return
273
263
  }
274
264
  if (stat.isSymbolicLink()) {
275
- await fs.promises.rm(target, { force: true })
265
+ await removePath(target, {
266
+ force: true,
267
+ operation: "Updating Miniforge",
268
+ })
276
269
  return
277
270
  }
278
- await fs.promises.rm(target, {
271
+ await removePath(target, {
279
272
  recursive: true,
280
273
  force: true,
281
274
  maxRetries: 5,
282
275
  retryDelay: 250,
276
+ operation: "Updating Miniforge",
283
277
  })
284
278
  } catch (error) {
279
+ if (isPathRemovalBlockedError(error)) {
280
+ if (typeof ondata === "function") {
281
+ ondata(error.toJSON(), "path.remove.blocked")
282
+ }
283
+ throw new Error(`__PINOKIO_BLOCKER__ ${error.message}`)
284
+ }
285
285
  const reason = error && (error.code || error.message) ? error.code || error.message : String(error)
286
286
  throw new Error([
287
287
  `Pinokio needs to replace the Conda runtime at ${target}, but it could not delete that folder.`,
@@ -0,0 +1,133 @@
1
+ const fs = require("fs")
2
+ const path = require("path")
3
+ const { inspectWindowsFileLocks } = require("./windows_restart_manager")
4
+
5
+ const PATH_REMOVAL_BLOCKED_CODE = "PINOKIO_PATH_REMOVE_BLOCKED"
6
+ const REMAINING_PREVIEW_LIMIT = 20
7
+
8
+ class PathRemovalBlockedError extends Error {
9
+ constructor(details, cause) {
10
+ const blockers = Array.isArray(details.blockers) ? details.blockers : []
11
+ const blockerReason = `${blockers.length === 1 ? "1 application" : `${blockers.length} applications`} may still be using files inside it`
12
+ super(blockers.length > 0
13
+ ? `Windows could not remove ${details.target} because ${blockerReason}.`
14
+ : `Windows could not remove ${details.target}.`)
15
+ this.name = "PathRemovalBlockedError"
16
+ this.code = PATH_REMOVAL_BLOCKED_CODE
17
+ this.target = details.target
18
+ this.operation = details.operation || "Removing files"
19
+ this.causeCode = cause && cause.code ? cause.code : ""
20
+ const remaining = Array.isArray(details.remaining) ? details.remaining : []
21
+ this.remaining = remaining.slice(0, REMAINING_PREVIEW_LIMIT)
22
+ this.remainingCount = remaining.length
23
+ this.blockers = blockers
24
+ this.cause = cause
25
+ }
26
+
27
+ toJSON() {
28
+ return {
29
+ code: this.code,
30
+ message: this.message,
31
+ target: this.target,
32
+ operation: this.operation,
33
+ causeCode: this.causeCode,
34
+ remaining: this.remaining,
35
+ remainingCount: this.remainingCount,
36
+ blockers: this.blockers,
37
+ }
38
+ }
39
+ }
40
+
41
+ const isPathRemovalBlockedError = (error) => !!(error && error.code === PATH_REMOVAL_BLOCKED_CODE)
42
+
43
+ const collectRemaining = async (target, fsPromises = fs.promises) => {
44
+ const files = []
45
+ const paths = []
46
+ const pending = [path.resolve(target)]
47
+
48
+ while (pending.length > 0) {
49
+ const current = pending.pop()
50
+ let stat
51
+ try {
52
+ stat = await fsPromises.lstat(current)
53
+ } catch (error) {
54
+ if (error && error.code === "ENOENT") {
55
+ continue
56
+ }
57
+ paths.push(current)
58
+ continue
59
+ }
60
+
61
+ paths.push(current)
62
+ if (!stat.isDirectory() || stat.isSymbolicLink()) {
63
+ files.push(current)
64
+ continue
65
+ }
66
+
67
+ let entries
68
+ try {
69
+ entries = await fsPromises.readdir(current, { withFileTypes: true })
70
+ } catch (_) {
71
+ continue
72
+ }
73
+ for (const entry of entries) {
74
+ pending.push(path.resolve(current, entry.name))
75
+ }
76
+ }
77
+
78
+ return { files, paths }
79
+ }
80
+
81
+ const createPathRemover = (dependencies = {}) => {
82
+ const fsPromises = dependencies.fsPromises || fs.promises
83
+ const platform = dependencies.platform || process.platform
84
+ const inspectLocks = dependencies.inspectLocks || inspectWindowsFileLocks
85
+
86
+ return async (target, options = {}) => {
87
+ const resolvedTarget = path.resolve(target)
88
+ const {
89
+ operation = "Removing files",
90
+ ...rmOptions
91
+ } = options
92
+
93
+ try {
94
+ await fsPromises.rm(resolvedTarget, rmOptions)
95
+ return { removed: true, target: resolvedTarget }
96
+ } catch (error) {
97
+ if (platform !== "win32") {
98
+ throw error
99
+ }
100
+
101
+ const remaining = await collectRemaining(resolvedTarget, fsPromises)
102
+ if (remaining.paths.length === 0) {
103
+ throw error
104
+ }
105
+
106
+ let inspection = { blockers: [] }
107
+ try {
108
+ inspection = await inspectLocks(remaining.files)
109
+ } catch (inspectionError) {
110
+ console.warn("[path removal] Restart Manager inspection failed", inspectionError)
111
+ }
112
+
113
+ const remainingItems = remaining.files.length > 0 ? remaining.files : remaining.paths
114
+
115
+ throw new PathRemovalBlockedError({
116
+ target: resolvedTarget,
117
+ operation,
118
+ remaining: remainingItems,
119
+ blockers: inspection.blockers,
120
+ }, error)
121
+ }
122
+ }
123
+ }
124
+
125
+ const removePath = createPathRemover()
126
+
127
+ module.exports = {
128
+ PATH_REMOVAL_BLOCKED_CODE,
129
+ collectRemaining,
130
+ createPathRemover,
131
+ isPathRemovalBlockedError,
132
+ removePath,
133
+ }
@@ -0,0 +1,153 @@
1
+ const path = require("path")
2
+
3
+ const ERROR_SUCCESS = 0
4
+ const ERROR_MORE_DATA = 234
5
+ const CCH_RM_SESSION_KEY = 32
6
+
7
+ let bindings = null
8
+
9
+ const initialize = () => {
10
+ if (bindings) {
11
+ return bindings
12
+ }
13
+ if (process.platform !== "win32") {
14
+ return null
15
+ }
16
+
17
+ const koffi = require("koffi")
18
+ const library = koffi.load("rstrtmgr.dll")
19
+ const kernel32 = koffi.load("kernel32.dll")
20
+ const fileTime = koffi.struct("PINOKIO_RM_FILETIME", {
21
+ dwLowDateTime: "uint32_t",
22
+ dwHighDateTime: "uint32_t",
23
+ })
24
+ const uniqueProcess = koffi.struct("PINOKIO_RM_UNIQUE_PROCESS", {
25
+ dwProcessId: "uint32_t",
26
+ ProcessStartTime: fileTime,
27
+ })
28
+ const processInfo = koffi.struct("PINOKIO_RM_PROCESS_INFO", {
29
+ Process: uniqueProcess,
30
+ strAppName: koffi.array("char16_t", 256, "String"),
31
+ strServiceShortName: koffi.array("char16_t", 64, "String"),
32
+ ApplicationType: "int32_t",
33
+ AppStatus: "uint32_t",
34
+ TSSessionId: "uint32_t",
35
+ bRestartable: "int32_t",
36
+ })
37
+
38
+ bindings = {
39
+ koffi,
40
+ processInfo,
41
+ startSession: library.func("uint32_t __stdcall RmStartSession(_Out_ uint32_t *session, uint32_t flags, _Out_ void *sessionKey)"),
42
+ registerResources: library.func("uint32_t __stdcall RmRegisterResources(uint32_t session, uint32_t fileCount, const str16 *files, uint32_t applicationCount, const void *applications, uint32_t serviceCount, const str16 *services)"),
43
+ getList: library.func("uint32_t __stdcall RmGetList(uint32_t session, _Out_ uint32_t *needed, _Inout_ uint32_t *count, _Out_ void *processes, _Out_ uint32_t *rebootReasons)"),
44
+ endSession: library.func("uint32_t __stdcall RmEndSession(uint32_t session)"),
45
+ openProcess: kernel32.func("void * __stdcall OpenProcess(uint32_t access, int32_t inheritHandle, uint32_t processId)"),
46
+ queryFullProcessImageName: kernel32.func("int32_t __stdcall QueryFullProcessImageNameW(void *process, uint32_t flags, _Out_ void *filename, _Inout_ uint32_t *size)"),
47
+ closeHandle: kernel32.func("int32_t __stdcall CloseHandle(void *handle)"),
48
+ }
49
+ return bindings
50
+ }
51
+
52
+ const queryProcessImage = (api, pid) => {
53
+ const PROCESS_QUERY_LIMITED_INFORMATION = 0x1000
54
+ const handle = api.openProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid)
55
+ if (!handle) {
56
+ return ""
57
+ }
58
+ try {
59
+ const capacity = 32768
60
+ const buffer = Buffer.alloc(capacity * 2)
61
+ const size = [capacity]
62
+ if (!api.queryFullProcessImageName(handle, 0, buffer, size)) {
63
+ return ""
64
+ }
65
+ return String(api.koffi.decode.string16(buffer) || "")
66
+ } catch (_) {
67
+ return ""
68
+ } finally {
69
+ api.closeHandle(handle)
70
+ }
71
+ }
72
+
73
+ const normalizeProcessInfo = (entry) => {
74
+ const uniqueProcess = entry && entry.Process ? entry.Process : {}
75
+ const pid = Number(uniqueProcess.dwProcessId) || 0
76
+ return {
77
+ pid,
78
+ name: String(entry && entry.strAppName ? entry.strAppName : "").trim(),
79
+ serviceName: String(entry && entry.strServiceShortName ? entry.strServiceShortName : "").trim(),
80
+ }
81
+ }
82
+
83
+ const inspectWindowsFileLocks = async (files) => {
84
+ if (process.platform !== "win32") {
85
+ return { blockers: [] }
86
+ }
87
+
88
+ const resources = Array.from(new Set((Array.isArray(files) ? files : [])
89
+ .filter((file) => typeof file === "string" && file.length > 0)))
90
+ if (resources.length === 0) {
91
+ return { blockers: [] }
92
+ }
93
+
94
+ const api = initialize()
95
+ const session = [0]
96
+ const sessionKey = Buffer.alloc((CCH_RM_SESSION_KEY + 1) * 2)
97
+ let status = api.startSession(session, 0, sessionKey)
98
+ if (status !== ERROR_SUCCESS) {
99
+ throw new Error(`RmStartSession failed with Windows error ${status}`)
100
+ }
101
+
102
+ try {
103
+ status = api.registerResources(session[0], resources.length, resources, 0, null, 0, null)
104
+ if (status !== ERROR_SUCCESS) {
105
+ throw new Error(`RmRegisterResources failed with Windows error ${status}`)
106
+ }
107
+
108
+ let needed = [0]
109
+ let count = [0]
110
+ let rebootReasons = [0]
111
+ status = api.getList(session[0], needed, count, null, rebootReasons)
112
+ if (status === ERROR_SUCCESS && needed[0] === 0) {
113
+ return { blockers: [] }
114
+ }
115
+ if (status !== ERROR_MORE_DATA && status !== ERROR_SUCCESS) {
116
+ throw new Error(`RmGetList failed with Windows error ${status}`)
117
+ }
118
+
119
+ let capacity = Math.max(needed[0], 1)
120
+ for (let attempt = 0; attempt < 3; attempt += 1) {
121
+ count = [capacity]
122
+ needed = [capacity]
123
+ rebootReasons = [0]
124
+ const buffer = Buffer.alloc(api.koffi.sizeof(api.processInfo) * capacity)
125
+ status = api.getList(session[0], needed, count, buffer, rebootReasons)
126
+ if (status === ERROR_SUCCESS) {
127
+ const entries = api.koffi.decode(buffer, api.processInfo, Math.min(count[0], capacity))
128
+ const blockers = entries
129
+ .map(normalizeProcessInfo)
130
+ .filter((entry) => entry.pid > 0)
131
+ .map((entry) => {
132
+ const imagePath = queryProcessImage(api, entry.pid)
133
+ return {
134
+ ...entry,
135
+ executable: imagePath ? path.win32.basename(imagePath) : "",
136
+ }
137
+ })
138
+ return { blockers }
139
+ }
140
+ if (status !== ERROR_MORE_DATA) {
141
+ throw new Error(`RmGetList failed with Windows error ${status}`)
142
+ }
143
+ capacity = Math.max(needed[0], capacity + 1)
144
+ }
145
+ throw new Error("RmGetList changed repeatedly while enumerating blocking processes")
146
+ } finally {
147
+ api.endSession(session[0])
148
+ }
149
+ }
150
+
151
+ module.exports = {
152
+ inspectWindowsFileLocks,
153
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pinokiod",
3
- "version": "8.0.8",
3
+ "version": "8.0.9",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
package/server/index.js CHANGED
@@ -34,6 +34,7 @@ const Git = require("../kernel/git")
34
34
  const TerminalApi = require('../kernel/api/terminal')
35
35
  const PluginSources = require("../kernel/plugin_sources")
36
36
  const ManagedSkills = require("../kernel/managed_skills")
37
+ const { isPathRemovalBlockedError, removePath } = require("../kernel/path_removal")
37
38
 
38
39
  const git = require('isomorphic-git')
39
40
  const http = require('isomorphic-git/http/node')
@@ -16263,14 +16264,38 @@ class Server {
16263
16264
  }))
16264
16265
  this.app.post("/pinokio/delete", ex(async (req, res) => {
16265
16266
  try {
16266
- if (req.body.type === 'bin') {
16267
+ if (req.body.type === 'conda-runtime') {
16268
+ for (const runtimeName of ["miniconda", "miniforge"]) {
16269
+ const runtimePath = this.kernel.path("bin", runtimeName)
16270
+ await removePath(runtimePath, {
16271
+ recursive: true,
16272
+ force: true,
16273
+ maxRetries: 5,
16274
+ retryDelay: 250,
16275
+ operation: "Updating Miniforge",
16276
+ })
16277
+ }
16278
+ res.json({ success: true })
16279
+ } else if (req.body.type === 'bin') {
16267
16280
  let folderPath = this.kernel.path("bin")
16268
- await fse.remove(folderPath)
16281
+ await removePath(folderPath, {
16282
+ recursive: true,
16283
+ force: true,
16284
+ maxRetries: 5,
16285
+ retryDelay: 250,
16286
+ operation: "Resetting managed tools",
16287
+ })
16269
16288
  await fs.promises.mkdir(folderPath, { recursive: true }).catch((e) => { })
16270
16289
  res.json({ success: true })
16271
16290
  } else if (req.body.type === 'cache') {
16272
16291
  let folderPath = this.kernel.path("cache")
16273
- await fse.remove(folderPath)
16292
+ await removePath(folderPath, {
16293
+ recursive: true,
16294
+ force: true,
16295
+ maxRetries: 5,
16296
+ retryDelay: 250,
16297
+ operation: "Clearing the Pinokio cache",
16298
+ })
16274
16299
  await fs.promises.mkdir(folderPath, { recursive: true }).catch((e) => { })
16275
16300
  await Environment.ensurePinokioCacheDirs(this.kernel, {
16276
16301
  throwOnFailure: true
@@ -16288,7 +16313,13 @@ class Server {
16288
16313
  res.json({ success: true })
16289
16314
  } else if (req.body.name) {
16290
16315
  let folderPath = this.kernel.path("api", req.body.name)
16291
- await fse.remove(folderPath)
16316
+ await removePath(folderPath, {
16317
+ recursive: true,
16318
+ force: true,
16319
+ maxRetries: 5,
16320
+ retryDelay: 250,
16321
+ operation: `Deleting ${req.body.name}`,
16322
+ })
16292
16323
  // await fs.promises.mkdir(folderPath, { recursive: true }).catch((e) => { })
16293
16324
  await new Promise((resolve, reject) => {
16294
16325
  setTimeout(() => {
@@ -16299,6 +16330,10 @@ class Server {
16299
16330
  res.json({ success: true })
16300
16331
  }
16301
16332
  } catch(err) {
16333
+ if (isPathRemovalBlockedError(err)) {
16334
+ res.status(423).json({ success: false, error: err.toJSON() })
16335
+ return
16336
+ }
16302
16337
  res.json({ error: err.stack })
16303
16338
  }
16304
16339
  }))
@@ -1,4 +1,194 @@
1
1
  const CAPTURE_MIN_SIZE = 32;
2
+
3
+ const PinokioPathRemoval = (() => {
4
+ const BLOCKED_CODE = "PINOKIO_PATH_REMOVE_BLOCKED"
5
+ const RETRY_INTERVAL = 2000
6
+
7
+ const escapeHtml = (value) => String(value == null ? "" : value)
8
+ .replace(/&/g, "&amp;")
9
+ .replace(/</g, "&lt;")
10
+ .replace(/>/g, "&gt;")
11
+ .replace(/"/g, "&quot;")
12
+ .replace(/'/g, "&#39;")
13
+
14
+ const blockedDetails = (payload) => {
15
+ if (!payload || typeof payload !== "object") {
16
+ return null
17
+ }
18
+ if (payload.code === BLOCKED_CODE) {
19
+ return payload
20
+ }
21
+ if (payload.error && payload.error.code === BLOCKED_CODE) {
22
+ return payload.error
23
+ }
24
+ return null
25
+ }
26
+
27
+ const renderBlocker = (blocker) => {
28
+ const name = blocker.name || blocker.serviceName || `Process ${blocker.pid || ""}`
29
+ const detail = [
30
+ blocker.executable && blocker.executable !== name ? blocker.executable : "",
31
+ blocker.serviceName && blocker.serviceName !== name ? blocker.serviceName : "",
32
+ blocker.pid ? `PID ${blocker.pid}` : "",
33
+ ].filter(Boolean).join(" · ")
34
+ return `<li class="pinokio-path-blocker-item">
35
+ <span class="pinokio-path-blocker-name">${escapeHtml(name)}</span>
36
+ ${detail ? `<span class="pinokio-path-blocker-detail">${escapeHtml(detail)}</span>` : ""}
37
+ </li>`
38
+ }
39
+
40
+ const render = (details) => {
41
+ const blockers = Array.isArray(details.blockers) ? details.blockers : []
42
+ const remaining = Array.isArray(details.remaining) ? details.remaining : []
43
+ const itemCount = Number(details.remainingCount) || remaining.length
44
+ const itemLabel = itemCount === 1 ? "one remaining item" : `${itemCount || "some"} remaining items`
45
+ const causeCode = details.causeCode ? ` (${escapeHtml(details.causeCode)})` : ""
46
+ const explanation = blockers.length > 0
47
+ ? `Windows couldn't remove ${itemLabel} because ${itemCount === 1 ? "it is" : "they are"} still in use.`
48
+ : `Windows couldn't remove ${itemLabel}${causeCode}. No application was identified. Check permissions or security software.`
49
+ const blockerHtml = blockers.length > 0
50
+ ? `<ul class="pinokio-path-blocker-list">${blockers.map(renderBlocker).join("")}</ul>`
51
+ : ""
52
+ const filesHtml = remaining.length > 0
53
+ ? `<details class="pinokio-path-blocker-files">
54
+ <summary>${itemCount > remaining.length ? `Show first ${remaining.length} of ${itemCount} remaining items` : `Show remaining ${remaining.length === 1 ? "item" : "items"}`}</summary>
55
+ <div>${remaining.map((file) => `<code>${escapeHtml(file)}</code>`).join("")}</div>
56
+ </details>`
57
+ : ""
58
+ return `<div class="pinokio-path-blocker">
59
+ <p>${explanation}</p>
60
+ ${blockerHtml}
61
+ ${filesHtml}
62
+ <div class="pinokio-path-blocker-wait">
63
+ <i class="fa-solid fa-circle-notch fa-spin" aria-hidden="true"></i>
64
+ <span>${blockers.length > 0
65
+ ? `Close the ${blockers.length === 1 ? "app" : "apps"} above. Pinokio will retry automatically.`
66
+ : "Resolve the access issue. Pinokio will retry automatically."}</span>
67
+ </div>
68
+ </div>`
69
+ }
70
+
71
+ const parseResponse = async (response) => {
72
+ if (!response || typeof response.json !== "function") {
73
+ return response
74
+ }
75
+ const fallbackResponse = typeof response.clone === "function" ? response.clone() : null
76
+ let payload
77
+ try {
78
+ payload = await response.json()
79
+ } catch (_) {
80
+ const text = fallbackResponse && typeof fallbackResponse.text === "function"
81
+ ? await fallbackResponse.text()
82
+ : ""
83
+ throw new Error(text || `Request failed (${response.status || "unknown"})`)
84
+ }
85
+ if (!response.ok && !blockedDetails(payload)) {
86
+ const message = payload && payload.error
87
+ throw new Error(typeof message === "string" ? message : `Request failed (${response.status})`)
88
+ }
89
+ return payload
90
+ }
91
+
92
+ const show = async ({ details, retry }) => {
93
+ if (typeof Swal === "undefined" || !Swal || typeof Swal.fire !== "function") {
94
+ return { cancelled: true }
95
+ }
96
+
97
+ let current = details
98
+ let retrying = false
99
+ let settled = false
100
+ let timer = null
101
+
102
+ return await new Promise((resolve) => {
103
+ const finish = (result) => {
104
+ if (settled) return
105
+ settled = true
106
+ if (timer) clearInterval(timer)
107
+ if (Swal.isVisible()) Swal.close()
108
+ resolve(result)
109
+ }
110
+
111
+ const update = () => {
112
+ const container = Swal.getHtmlContainer()
113
+ if (container) container.innerHTML = render(current)
114
+ }
115
+
116
+ const attempt = async (manual = false) => {
117
+ if (retrying || settled || typeof retry !== "function") return false
118
+ retrying = true
119
+ if (typeof Swal.disableButtons === "function") Swal.disableButtons()
120
+ try {
121
+ const result = await retry()
122
+ const nextBlocked = blockedDetails(result)
123
+ if (nextBlocked) {
124
+ current = nextBlocked
125
+ update()
126
+ if (manual) Swal.showValidationMessage("Windows still cannot remove the remaining items.")
127
+ return false
128
+ }
129
+ finish(result)
130
+ return true
131
+ } catch (error) {
132
+ if (manual) Swal.showValidationMessage(escapeHtml(error && error.message ? error.message : "Retry failed."))
133
+ return false
134
+ } finally {
135
+ retrying = false
136
+ if (!settled && typeof Swal.enableButtons === "function") Swal.enableButtons()
137
+ }
138
+ }
139
+
140
+ Swal.fire({
141
+ title: escapeHtml(current.operation || "File removal paused"),
142
+ html: render(current),
143
+ showCancelButton: true,
144
+ confirmButtonText: "Try again now",
145
+ cancelButtonText: "Stop for now",
146
+ allowOutsideClick: false,
147
+ allowEscapeKey: false,
148
+ didOpen: () => {
149
+ timer = setInterval(() => attempt(false), RETRY_INTERVAL)
150
+ },
151
+ preConfirm: async () => {
152
+ const done = await attempt(true)
153
+ return done ? true : false
154
+ },
155
+ willClose: () => {
156
+ if (timer) clearInterval(timer)
157
+ },
158
+ }).then((result) => {
159
+ if (!settled && !result.isConfirmed) {
160
+ finish({ cancelled: true })
161
+ }
162
+ })
163
+ })
164
+ }
165
+
166
+ const request = async (requestFactory) => {
167
+ const execute = async () => parseResponse(await requestFactory())
168
+ const initial = await execute()
169
+ const details = blockedDetails(initial)
170
+ if (!details) {
171
+ return initial
172
+ }
173
+ if (typeof Swal !== "undefined" && Swal && Swal.isVisible()) {
174
+ Swal.close()
175
+ }
176
+ return show({
177
+ details,
178
+ retry: execute,
179
+ })
180
+ }
181
+
182
+ return {
183
+ blockedDetails,
184
+ request,
185
+ show,
186
+ }
187
+ })()
188
+
189
+ if (typeof window !== "undefined") {
190
+ window.PinokioPathRemoval = PinokioPathRemoval
191
+ }
2
192
  let pinokioDevGuardSatisfied = false;
3
193
  const createLauncherDebugLog = (...args) => {
4
194
  try {