pinokiod 8.0.8 → 8.0.10
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 +17 -17
- package/kernel/path_removal.js +150 -0
- package/kernel/windows_restart_manager.js +153 -0
- package/package.json +1 -1
- package/server/index.js +39 -4
- package/server/public/common.js +190 -0
- package/server/public/style.css +65 -0
- package/server/views/app.ejs +2 -4
- package/server/views/download.ejs +5 -5
- package/server/views/editor.ejs +5 -5
- package/server/views/env_editor.ejs +4 -2
- package/server/views/file_explorer.ejs +2 -4
- package/server/views/index.ejs +2 -4
- package/server/views/install.ejs +25 -0
- package/server/views/net.ejs +2 -4
- package/server/views/settings.ejs +10 -10
- package/server/views/setup.ejs +5 -5
- package/server/views/terminal.ejs +5 -5
- package/test/conda-bin.test.js +11 -11
- package/test/fixtures/hold-windows-dll.js +21 -0
- package/test/path-removal-ui.test.js +316 -0
- package/test/path-removal.test.js +189 -0
- package/test/windows-path-removal.integration.test.js +78 -0
package/kernel/bin/conda.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
265
|
+
await removePath(target, {
|
|
266
|
+
force: true,
|
|
267
|
+
operation: "Updating Miniforge",
|
|
268
|
+
})
|
|
276
269
|
return
|
|
277
270
|
}
|
|
278
|
-
await
|
|
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,150 @@
|
|
|
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
|
+
const trace = (stage, details = "") => {
|
|
93
|
+
if (platform === "win32") {
|
|
94
|
+
console.log(`[path removal] ${stage} target=${resolvedTarget}${details ? ` ${details}` : ""}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const removeStartedAt = Date.now()
|
|
99
|
+
trace("rm:start")
|
|
100
|
+
try {
|
|
101
|
+
await fsPromises.rm(resolvedTarget, rmOptions)
|
|
102
|
+
trace("rm:done", `elapsedMs=${Date.now() - removeStartedAt}`)
|
|
103
|
+
return { removed: true, target: resolvedTarget }
|
|
104
|
+
} catch (error) {
|
|
105
|
+
if (platform !== "win32") {
|
|
106
|
+
throw error
|
|
107
|
+
}
|
|
108
|
+
trace("rm:failed", `elapsedMs=${Date.now() - removeStartedAt} code=${error && error.code ? error.code : "unknown"}`)
|
|
109
|
+
|
|
110
|
+
const scanStartedAt = Date.now()
|
|
111
|
+
trace("scan:start")
|
|
112
|
+
const remaining = await collectRemaining(resolvedTarget, fsPromises)
|
|
113
|
+
trace("scan:done", `elapsedMs=${Date.now() - scanStartedAt} paths=${remaining.paths.length} files=${remaining.files.length}`)
|
|
114
|
+
if (remaining.paths.length === 0) {
|
|
115
|
+
throw error
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let inspection = { blockers: [] }
|
|
119
|
+
const inspectionStartedAt = Date.now()
|
|
120
|
+
trace("restart-manager:start", `files=${remaining.files.length}`)
|
|
121
|
+
try {
|
|
122
|
+
inspection = await inspectLocks(remaining.files)
|
|
123
|
+
const blockerCount = Array.isArray(inspection && inspection.blockers) ? inspection.blockers.length : 0
|
|
124
|
+
trace("restart-manager:done", `elapsedMs=${Date.now() - inspectionStartedAt} blockers=${blockerCount}`)
|
|
125
|
+
} catch (inspectionError) {
|
|
126
|
+
trace("restart-manager:failed", `elapsedMs=${Date.now() - inspectionStartedAt} message=${inspectionError && inspectionError.message ? inspectionError.message : String(inspectionError)}`)
|
|
127
|
+
console.warn("[path removal] Restart Manager inspection failed", inspectionError)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const remainingItems = remaining.files.length > 0 ? remaining.files : remaining.paths
|
|
131
|
+
|
|
132
|
+
throw new PathRemovalBlockedError({
|
|
133
|
+
target: resolvedTarget,
|
|
134
|
+
operation,
|
|
135
|
+
remaining: remainingItems,
|
|
136
|
+
blockers: inspection.blockers,
|
|
137
|
+
}, error)
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const removePath = createPathRemover()
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
PATH_REMOVAL_BLOCKED_CODE,
|
|
146
|
+
collectRemaining,
|
|
147
|
+
createPathRemover,
|
|
148
|
+
isPathRemovalBlockedError,
|
|
149
|
+
removePath,
|
|
150
|
+
}
|
|
@@ -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
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 === '
|
|
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
|
|
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
|
|
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
|
|
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
|
}))
|
package/server/public/common.js
CHANGED
|
@@ -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, "&")
|
|
9
|
+
.replace(/</g, "<")
|
|
10
|
+
.replace(/>/g, ">")
|
|
11
|
+
.replace(/"/g, """)
|
|
12
|
+
.replace(/'/g, "'")
|
|
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 {
|