dsh-clean-desktop-shell 0.1.8 → 0.1.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/README.en.md +32 -0
- package/README.md +20 -0
- package/electron/config.js +40 -10
- package/electron/crashGuard.js +60 -0
- package/electron/main.js +15 -7
- package/electron/service.js +91 -48
- package/electron/shortcut.js +47 -55
- package/electron/update.js +237 -34
- package/electron/window.js +7 -5
- package/lib/common.js +21 -0
- package/lib/icon.js +1 -13
- package/lib/runtime.js +82 -33
- package/package.json +4 -3
- package/scripts/check-syntax.mjs +54 -0
- package/src/host/common.js +21 -0
- package/src/host/icon.js +1 -13
- package/src/host/runtime.js +82 -33
- package/version.txt +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Syntax gate: `node --check` every JS file that ships in the package
|
|
3
|
+
* (electron/ main-process modules + lib/ host/client bundle). Previously
|
|
4
|
+
* only lib/index.js was checked — a syntax slip in any other file shipped
|
|
5
|
+
* unnoticed.
|
|
6
|
+
*
|
|
7
|
+
* The two sandboxed preloads are CommonJS (require/module.exports) while
|
|
8
|
+
* package.json declares "type": "module", so plain --check on them fails
|
|
9
|
+
* spuriously; they are validated through a temporary .cjs copy instead.
|
|
10
|
+
*/
|
|
11
|
+
import { spawnSync } from 'node:child_process'
|
|
12
|
+
import { readdirSync, readFileSync, statSync, rmSync, writeFileSync, mkdtempSync } from 'node:fs'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
import { tmpdir } from 'node:os'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
|
|
17
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
18
|
+
// CJS files relative to electron/ — parsed as CommonJS despite the ESM package.
|
|
19
|
+
const CJS_FILES = new Set(['preload.js', 'progress-preload.js'])
|
|
20
|
+
|
|
21
|
+
function listJs(dir) {
|
|
22
|
+
return readdirSync(join(root, dir)).filter((f) => f.endsWith('.js'))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const scratch = mkdtempSync(join(tmpdir(), 'dsh-shell-check-'))
|
|
26
|
+
let bad = 0
|
|
27
|
+
let checked = 0
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
for (const dir of ['electron', 'lib']) {
|
|
31
|
+
for (const f of listJs(dir)) {
|
|
32
|
+
const full = join(root, dir, f)
|
|
33
|
+
if (!statSync(full).isFile()) continue
|
|
34
|
+
const cjs = CJS_FILES.has(f)
|
|
35
|
+
const target = cjs ? join(scratch, `${dir}-${f}.cjs`) : full
|
|
36
|
+
if (cjs) writeFileSync(target, readFileSync(full))
|
|
37
|
+
const r = spawnSync(process.execPath, ['--check', target], { stdio: 'pipe' })
|
|
38
|
+
const ok = r.status === 0
|
|
39
|
+
checked++
|
|
40
|
+
if (!ok) {
|
|
41
|
+
bad++
|
|
42
|
+
console.log(` FAIL ${dir}/${f}${cjs ? ' (checked as CJS)' : ''}`)
|
|
43
|
+
if (r.stderr) console.error(r.stderr.toString().trim())
|
|
44
|
+
} else {
|
|
45
|
+
console.log(` ok ${dir}/${f}${cjs ? ' (checked as CJS)' : ''}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
} finally {
|
|
50
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(bad === 0 ? `PASS (${checked} files)` : `FAIL (${bad}/${checked} file(s))`)
|
|
54
|
+
process.exit(bad === 0 ? 0 : 1)
|
package/src/host/common.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* location electron/ and build/ live in the published package.
|
|
7
7
|
*/
|
|
8
8
|
import { homedir } from 'node:os'
|
|
9
|
+
import { spawn } from 'node:child_process'
|
|
9
10
|
import { dirname, join } from 'node:path'
|
|
10
11
|
import { fileURLToPath } from 'node:url'
|
|
11
12
|
|
|
@@ -51,3 +52,23 @@ export function runtimeRoot() {
|
|
|
51
52
|
export function launchLogPath() {
|
|
52
53
|
return join(dshHome(), 'desktop-shell-launch.log')
|
|
53
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Download a file to disk via curl — shared by runtime provisioning and
|
|
58
|
+
* icon patching. Chosen over native fetch because undici (Node's fetch)
|
|
59
|
+
* ignores HTTP(S)_PROXY env vars unless a proxy dispatcher is wired in,
|
|
60
|
+
* while curl honors them out of the box (users behind Clash/v2ray rely on
|
|
61
|
+
* that). --max-time keeps a stalled proxy from hanging forever; --retry
|
|
62
|
+
* rides out transient failures.
|
|
63
|
+
*/
|
|
64
|
+
export function fetchFile(url, dest, timeoutSec = 600) {
|
|
65
|
+
return new Promise((resolve) => {
|
|
66
|
+
const child = spawn(
|
|
67
|
+
'curl',
|
|
68
|
+
['-L', '--fail', '--silent', '--show-error', '--retry', '2', '--max-time', String(timeoutSec), '-o', dest, url],
|
|
69
|
+
{ windowsHide: true, stdio: 'ignore' },
|
|
70
|
+
)
|
|
71
|
+
child.on('error', () => resolve(false))
|
|
72
|
+
child.on('exit', (code) => resolve(code === 0))
|
|
73
|
+
})
|
|
74
|
+
}
|
package/src/host/icon.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { spawn } from 'node:child_process'
|
|
15
15
|
import { existsSync, writeFileSync } from 'node:fs'
|
|
16
16
|
import { join } from 'node:path'
|
|
17
|
-
import { PKG_ROOT, isWin, runtimeRoot } from './common.js'
|
|
17
|
+
import { PKG_ROOT, isWin, runtimeRoot, fetchFile } from './common.js'
|
|
18
18
|
|
|
19
19
|
export async function patchExeIcon(ctx, exe) {
|
|
20
20
|
if (!isWin) return
|
|
@@ -49,15 +49,3 @@ export async function patchExeIcon(ctx, exe) {
|
|
|
49
49
|
ctx.logger.warn('[clean-desktop-shell] rcedit patch failed — taskbar icon stays default')
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
function fetchFile(url, dest) {
|
|
54
|
-
return new Promise((resolve) => {
|
|
55
|
-
const child = spawn(
|
|
56
|
-
'curl',
|
|
57
|
-
['-L', '--fail', '--silent', '--show-error', '--retry', '2', '--max-time', '600', '-o', dest, url],
|
|
58
|
-
{ windowsHide: true, stdio: 'ignore' },
|
|
59
|
-
)
|
|
60
|
-
child.on('error', () => resolve(false))
|
|
61
|
-
child.on('exit', (code) => resolve(code === 0))
|
|
62
|
-
})
|
|
63
|
-
}
|
package/src/host/runtime.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* 4. drop stale electron-v* dirs (no unbounded disk growth)
|
|
14
14
|
*/
|
|
15
15
|
import { spawn } from 'node:child_process'
|
|
16
|
+
import { createHash } from 'node:crypto'
|
|
17
|
+
import { createReadStream } from 'node:fs'
|
|
16
18
|
import {
|
|
17
19
|
cpSync,
|
|
18
20
|
existsSync,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
rmSync,
|
|
24
26
|
symlinkSync,
|
|
25
27
|
} from 'node:fs'
|
|
28
|
+
import { pipeline } from 'node:stream/promises'
|
|
26
29
|
import { basename, join } from 'node:path'
|
|
27
30
|
import {
|
|
28
31
|
PKG_ROOT,
|
|
@@ -33,6 +36,7 @@ import {
|
|
|
33
36
|
isWin,
|
|
34
37
|
isMac,
|
|
35
38
|
runtimeRoot,
|
|
39
|
+
fetchFile,
|
|
36
40
|
} from './common.js'
|
|
37
41
|
|
|
38
42
|
function electronVersion() {
|
|
@@ -124,17 +128,26 @@ function ensureExecutable(ctx, target) {
|
|
|
124
128
|
async function downloadRuntime(ctx, version, root, dir) {
|
|
125
129
|
const tmpZip = join(root, `.electron-${version}.zip.tmp`)
|
|
126
130
|
rmSync(tmpZip, { force: true })
|
|
127
|
-
const
|
|
128
|
-
const
|
|
131
|
+
const candidates = await runtimeCandidates(version)
|
|
132
|
+
const zip = zipName(version)
|
|
133
|
+
const innerName = zip.replace(/\.zip$/, '')
|
|
129
134
|
|
|
130
|
-
for (const
|
|
131
|
-
ctx.logger.info(`[clean-desktop-shell] downloading electron ${version} from ${url}`)
|
|
132
|
-
if (!(await fetchFile(url, tmpZip))) {
|
|
135
|
+
for (const c of candidates) {
|
|
136
|
+
ctx.logger.info(`[clean-desktop-shell] downloading electron ${version} from ${c.url}`)
|
|
137
|
+
if (!(await fetchFile(c.url, tmpZip))) {
|
|
133
138
|
// Failed download — drop the partial file so a later run starts clean.
|
|
134
139
|
rmSync(tmpZip, { force: true })
|
|
135
140
|
continue
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
// Integrity check against the source's own SHASUMS256.txt (same check
|
|
144
|
+
// the `electron` package's installer performs). A mismatch means the
|
|
145
|
+
// bytes are corrupt or tampered — the source is skipped entirely.
|
|
146
|
+
if (!(await verifySha256(ctx, tmpZip, c.shasums, zip))) {
|
|
147
|
+
rmSync(tmpZip, { force: true })
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
|
|
138
151
|
// Extract into a scratch dir, never into the shared runtime root: a
|
|
139
152
|
// half-extracted zip there would be indistinguishable from a real
|
|
140
153
|
// runtime, and the sibling electron-v* dirs must not be disturbed.
|
|
@@ -142,7 +155,7 @@ async function downloadRuntime(ctx, version, root, dir) {
|
|
|
142
155
|
const ok = await extractZip(ctx, tmpZip, scratch)
|
|
143
156
|
rmSync(tmpZip, { force: true })
|
|
144
157
|
if (!ok) {
|
|
145
|
-
ctx.logger.warn(`[clean-desktop-shell] no extractor succeeded for ${url}`)
|
|
158
|
+
ctx.logger.warn(`[clean-desktop-shell] no extractor succeeded for ${c.url}`)
|
|
146
159
|
rmSync(scratch, { recursive: true, force: true })
|
|
147
160
|
continue
|
|
148
161
|
}
|
|
@@ -169,6 +182,54 @@ async function downloadRuntime(ctx, version, root, dir) {
|
|
|
169
182
|
throw new Error('electron download failed from all sources')
|
|
170
183
|
}
|
|
171
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Verify a downloaded zip against the source's SHASUMS256.txt — the same
|
|
187
|
+
* integrity check the `electron` package's own installer performs. Returns
|
|
188
|
+
* false on a checksum mismatch (fatal for that source); a temporarily
|
|
189
|
+
* unreachable SHASUMS file only warns, so a reachable zip is not wasted
|
|
190
|
+
* over an unrelated blocker.
|
|
191
|
+
*/
|
|
192
|
+
async function verifySha256(ctx, file, shasumsUrl, name) {
|
|
193
|
+
let expected = null
|
|
194
|
+
let reason = ''
|
|
195
|
+
try {
|
|
196
|
+
const res = await fetch(shasumsUrl, { signal: AbortSignal.timeout(10000) })
|
|
197
|
+
if (!res.ok) {
|
|
198
|
+
reason = `HTTP ${res.status}`
|
|
199
|
+
} else {
|
|
200
|
+
const text = await res.text()
|
|
201
|
+
for (const line of text.split('\n')) {
|
|
202
|
+
const m = line.trim().match(/^([0-9a-f]{64})\s+\*?(.+)$/i)
|
|
203
|
+
if (m && m[2].trim() === name) {
|
|
204
|
+
expected = m[1].toLowerCase()
|
|
205
|
+
break
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!expected) reason = 'entry not found'
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
reason = err?.message || 'fetch failed'
|
|
212
|
+
}
|
|
213
|
+
if (!expected) {
|
|
214
|
+
ctx.logger.warn(
|
|
215
|
+
`[clean-desktop-shell] SHASUMS256.txt unavailable from ${shasumsUrl} (${reason}) — skipping integrity check`,
|
|
216
|
+
)
|
|
217
|
+
return true
|
|
218
|
+
}
|
|
219
|
+
// Stream the hash — the zip can be ~270 MB and must not be read whole
|
|
220
|
+
// into memory.
|
|
221
|
+
const hash = createHash('sha256')
|
|
222
|
+
await pipeline(createReadStream(file), hash)
|
|
223
|
+
const actual = hash.digest('hex')
|
|
224
|
+
if (actual !== expected) {
|
|
225
|
+
ctx.logger.warn(
|
|
226
|
+
`[clean-desktop-shell] checksum mismatch for ${name}: expected ${expected}, got ${actual}`,
|
|
227
|
+
)
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
|
|
172
233
|
/** The dir that directly holds the electron payload right after extraction. */
|
|
173
234
|
function findPayload(scratch, wrapper) {
|
|
174
235
|
if (existsSync(join(wrapper, EXE_TOP))) return wrapper
|
|
@@ -212,21 +273,25 @@ function clearQuarantine(ctx, target) {
|
|
|
212
273
|
* (direct connection, 3s each). The fastest reachable source goes first —
|
|
213
274
|
* this naturally prefers the domestic npmmirror mirror on CN networks,
|
|
214
275
|
* the official GitHub source on international/well-proxied networks, and
|
|
215
|
-
* never wastes a full download on a dead source.
|
|
276
|
+
* never wastes a full download on a dead source. Each candidate carries
|
|
277
|
+
* its own SHASUMS256.txt location for the post-download integrity check.
|
|
216
278
|
*/
|
|
217
|
-
async function
|
|
279
|
+
async function runtimeCandidates(version) {
|
|
280
|
+
const base = (name, urlBase) => ({
|
|
281
|
+
name,
|
|
282
|
+
url: `${urlBase}/${zipName(version)}`,
|
|
283
|
+
shasums: `${urlBase}/SHASUMS256.txt`,
|
|
284
|
+
})
|
|
218
285
|
const candidates = [
|
|
219
|
-
|
|
220
|
-
|
|
286
|
+
base('github', `https://github.com/electron/electron/releases/download/v${version}`),
|
|
287
|
+
base('npmmirror', `https://npmmirror.com/mirrors/electron/${version}`),
|
|
221
288
|
]
|
|
222
289
|
const results = await Promise.all(
|
|
223
290
|
candidates.map(async (c) => {
|
|
224
291
|
const t0 = Date.now()
|
|
225
292
|
try {
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
const res = await fetch(c.url, { signal: ctrl.signal, method: 'HEAD' })
|
|
229
|
-
clearTimeout(timer)
|
|
293
|
+
// AbortSignal.timeout — the standard self-cleaning probe timeout.
|
|
294
|
+
const res = await fetch(c.url, { signal: AbortSignal.timeout(3000), method: 'HEAD' })
|
|
230
295
|
if (res.status < 500) return { ...c, ms: Date.now() - t0 }
|
|
231
296
|
} catch {
|
|
232
297
|
// unreachable — drop
|
|
@@ -237,26 +302,10 @@ async function runtimeUrls(version) {
|
|
|
237
302
|
const ok = results.filter(Boolean).sort((a, b) => a.ms - b.ms)
|
|
238
303
|
if (ok.length === 0) {
|
|
239
304
|
// Probes all failed (offline?) — still try both, mirror first (cheap).
|
|
240
|
-
return [candidates[1]
|
|
305
|
+
return [candidates[1], candidates[0]]
|
|
241
306
|
}
|
|
242
|
-
const rest = candidates.
|
|
243
|
-
return [ok[0]
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function fetchFile(url, dest) {
|
|
247
|
-
return new Promise((resolve) => {
|
|
248
|
-
// curl is available on Windows 10+; streams to disk, honors proxy env.
|
|
249
|
-
// --max-time keeps a stalled download from hanging forever (a proxy
|
|
250
|
-
// stall previously left a half-written .zip.tmp and blocked the shell
|
|
251
|
-
// launch); --retry 2 rides out transient failures.
|
|
252
|
-
const child = spawn(
|
|
253
|
-
'curl',
|
|
254
|
-
['-L', '--fail', '--silent', '--show-error', '--retry', '2', '--max-time', '600', '-o', dest, url],
|
|
255
|
-
{ windowsHide: true, stdio: 'ignore' },
|
|
256
|
-
)
|
|
257
|
-
child.on('error', () => resolve(false))
|
|
258
|
-
child.on('exit', (code) => resolve(code === 0))
|
|
259
|
-
})
|
|
307
|
+
const rest = candidates.filter((c) => c.url !== ok[0].url)
|
|
308
|
+
return [ok[0], ...rest]
|
|
260
309
|
}
|
|
261
310
|
|
|
262
311
|
/**
|
package/version.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.1.
|
|
1
|
+
0.1.10
|