dsh-clean-desktop-shell 0.1.6 → 0.1.8
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/CONTRIBUTORS.md +19 -0
- package/README.en.md +108 -3
- package/README.md +80 -3
- package/electron/assets/trayTemplate.png +0 -0
- package/electron/assets/trayTemplate@2x.png +0 -0
- package/electron/tray.js +8 -2
- package/electron/update.js +43 -4
- package/lib/common.js +34 -3
- package/lib/index.js +42 -2
- package/lib/runtime.js +196 -35
- package/package.json +5 -3
- package/scripts/selftest-runtime.mjs +90 -0
- package/src/host/common.js +34 -3
- package/src/host/index.js +42 -2
- package/src/host/runtime.js +196 -35
- package/version.txt +1 -1
package/lib/runtime.js
CHANGED
|
@@ -23,8 +23,17 @@ import {
|
|
|
23
23
|
rmSync,
|
|
24
24
|
symlinkSync,
|
|
25
25
|
} from 'node:fs'
|
|
26
|
-
import { join } from 'node:path'
|
|
27
|
-
import {
|
|
26
|
+
import { basename, join } from 'node:path'
|
|
27
|
+
import {
|
|
28
|
+
PKG_ROOT,
|
|
29
|
+
EXE_RELPATH,
|
|
30
|
+
EXE_TOP,
|
|
31
|
+
ARCH,
|
|
32
|
+
PLATFORM,
|
|
33
|
+
isWin,
|
|
34
|
+
isMac,
|
|
35
|
+
runtimeRoot,
|
|
36
|
+
} from './common.js'
|
|
28
37
|
|
|
29
38
|
function electronVersion() {
|
|
30
39
|
try {
|
|
@@ -49,18 +58,19 @@ export async function ensureRuntime(ctx) {
|
|
|
49
58
|
if (!version) throw new Error('desktopShell.electronVersion missing in package.json')
|
|
50
59
|
const root = runtimeRoot()
|
|
51
60
|
const dir = versionDir(root, version)
|
|
52
|
-
const exe = join(dir,
|
|
61
|
+
const exe = join(dir, EXE_RELPATH)
|
|
53
62
|
|
|
54
63
|
// 1) Already provisioned for this version?
|
|
55
64
|
if (existsSync(exe)) {
|
|
56
65
|
cleanupOldVersions(root, dir)
|
|
66
|
+
if (!isWin) await ensureExecutable(ctx, exe)
|
|
57
67
|
return exe
|
|
58
68
|
}
|
|
59
69
|
|
|
60
70
|
// 2) Local reuse: DSH_SHELL_ELECTRON_DIR → link/copy its dist/ (fast).
|
|
61
71
|
const localSrc = process.env.DSH_SHELL_ELECTRON_DIR
|
|
62
72
|
if (localSrc) {
|
|
63
|
-
const srcExe = join(localSrc, 'dist',
|
|
73
|
+
const srcExe = join(localSrc, 'dist', EXE_RELPATH)
|
|
64
74
|
if (existsSync(srcExe) && provisionLocalDist(localSrc, dir)) {
|
|
65
75
|
ctx.logger.info(`[clean-desktop-shell] reused electron runtime from ${localSrc}`)
|
|
66
76
|
}
|
|
@@ -73,43 +83,130 @@ export async function ensureRuntime(ctx) {
|
|
|
73
83
|
}
|
|
74
84
|
|
|
75
85
|
if (!existsSync(exe)) {
|
|
86
|
+
// Name the exact path we expected. Without it a user on an untested
|
|
87
|
+
// platform has nothing to report back (this failure used to be silent).
|
|
76
88
|
throw new Error(
|
|
77
|
-
|
|
89
|
+
`electron runtime provisioning failed — expected binary at ${exe} ` +
|
|
90
|
+
`(platform=${PLATFORM} arch=${ARCH}, layout differs per platform); ` +
|
|
91
|
+
'check network, or point DSH_SHELL_ELECTRON_DIR at an electron package',
|
|
78
92
|
)
|
|
79
93
|
}
|
|
80
94
|
cleanupOldVersions(root, dir)
|
|
95
|
+
if (!isWin) await ensureExecutable(ctx, exe)
|
|
81
96
|
ctx.logger.info(`[clean-desktop-shell] electron runtime ${version} ready at ${dir}`)
|
|
82
97
|
return exe
|
|
83
98
|
}
|
|
84
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Zip extraction does not reliably restore the executable bit — bsdtar
|
|
102
|
+
* (Windows tar.exe, macOS /usr/bin/tar) in particular drops it — and a
|
|
103
|
+
* non-executable binary fails later with a bare EACCES on spawn. Guarantee
|
|
104
|
+
* it instead of trusting the extractor.
|
|
105
|
+
*/
|
|
106
|
+
function ensureExecutable(ctx, target) {
|
|
107
|
+
return new Promise((resolve) => {
|
|
108
|
+
let child
|
|
109
|
+
try {
|
|
110
|
+
child = spawn('chmod', ['+x', target], { stdio: 'ignore' })
|
|
111
|
+
} catch {
|
|
112
|
+
return resolve(false)
|
|
113
|
+
}
|
|
114
|
+
child.on('error', () => resolve(false))
|
|
115
|
+
child.on('exit', (code) => {
|
|
116
|
+
if (code !== 0) {
|
|
117
|
+
ctx.logger.warn(`[clean-desktop-shell] chmod +x failed (exit ${code}) on ${target}`)
|
|
118
|
+
}
|
|
119
|
+
resolve(code === 0)
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
85
124
|
async function downloadRuntime(ctx, version, root, dir) {
|
|
86
125
|
const tmpZip = join(root, `.electron-${version}.zip.tmp`)
|
|
87
126
|
rmSync(tmpZip, { force: true })
|
|
88
127
|
const urls = await runtimeUrls(version)
|
|
128
|
+
const innerName = zipName(version).replace(/\.zip$/, '')
|
|
129
|
+
|
|
89
130
|
for (const url of urls) {
|
|
90
131
|
ctx.logger.info(`[clean-desktop-shell] downloading electron ${version} from ${url}`)
|
|
91
|
-
if (await fetchFile(url, tmpZip)) {
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
await extractZip(tmpZip, root)
|
|
96
|
-
if (existsSync(join(inner, EXE_NAME)) && inner !== dir) {
|
|
97
|
-
rmSync(dir, { recursive: true, force: true })
|
|
98
|
-
renameSync(inner, dir)
|
|
99
|
-
}
|
|
100
|
-
rmSync(tmpZip, { force: true })
|
|
101
|
-
return
|
|
102
|
-
} catch (err) {
|
|
103
|
-
ctx.logger.warn(`[clean-desktop-shell] extract failed: ${err?.message ?? err}`)
|
|
104
|
-
rmSync(inner, { recursive: true, force: true })
|
|
105
|
-
}
|
|
132
|
+
if (!(await fetchFile(url, tmpZip))) {
|
|
133
|
+
// Failed download — drop the partial file so a later run starts clean.
|
|
134
|
+
rmSync(tmpZip, { force: true })
|
|
135
|
+
continue
|
|
106
136
|
}
|
|
107
|
-
|
|
137
|
+
|
|
138
|
+
// Extract into a scratch dir, never into the shared runtime root: a
|
|
139
|
+
// half-extracted zip there would be indistinguishable from a real
|
|
140
|
+
// runtime, and the sibling electron-v* dirs must not be disturbed.
|
|
141
|
+
const scratch = join(root, `.extract-${version}-${Date.now()}`)
|
|
142
|
+
const ok = await extractZip(ctx, tmpZip, scratch)
|
|
108
143
|
rmSync(tmpZip, { force: true })
|
|
144
|
+
if (!ok) {
|
|
145
|
+
ctx.logger.warn(`[clean-desktop-shell] no extractor succeeded for ${url}`)
|
|
146
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
147
|
+
continue
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Both archive layouts exist in the wild: some Electron zips wrap
|
|
151
|
+
// everything in a dir named after the zip, others (notably darwin, whose
|
|
152
|
+
// payload is Electron.app/) unpack flat into the destination. Detect
|
|
153
|
+
// rather than assume — the old code assumed the wrapper and silently
|
|
154
|
+
// produced nothing on macOS.
|
|
155
|
+
const payload = findPayload(scratch, join(scratch, innerName))
|
|
156
|
+
if (!payload) {
|
|
157
|
+
ctx.logger.warn(
|
|
158
|
+
`[clean-desktop-shell] unexpected archive layout: no ${EXE_TOP} under ${scratch}`,
|
|
159
|
+
)
|
|
160
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
161
|
+
continue
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
movePayloadInto(dir, payload)
|
|
165
|
+
rmSync(scratch, { recursive: true, force: true })
|
|
166
|
+
if (isMac) await clearQuarantine(ctx, join(dir, EXE_TOP))
|
|
167
|
+
return
|
|
109
168
|
}
|
|
110
169
|
throw new Error('electron download failed from all sources')
|
|
111
170
|
}
|
|
112
171
|
|
|
172
|
+
/** The dir that directly holds the electron payload right after extraction. */
|
|
173
|
+
function findPayload(scratch, wrapper) {
|
|
174
|
+
if (existsSync(join(wrapper, EXE_TOP))) return wrapper
|
|
175
|
+
if (existsSync(join(scratch, EXE_TOP))) return scratch
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function movePayloadInto(dir, payload) {
|
|
180
|
+
rmSync(dir, { recursive: true, force: true })
|
|
181
|
+
renameSync(payload, dir)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* macOS: a bundle carrying com.apple.quarantine is refused by Gatekeeper with
|
|
186
|
+
* the infamous "已损坏,无法打开" dialog. We download with curl (which does
|
|
187
|
+
* not set the attribute), but archive members themselves can carry it, so
|
|
188
|
+
* clear it once right after extraction rather than debugging it per user.
|
|
189
|
+
*/
|
|
190
|
+
function clearQuarantine(ctx, target) {
|
|
191
|
+
return new Promise((resolve) => {
|
|
192
|
+
let child
|
|
193
|
+
try {
|
|
194
|
+
child = spawn('xattr', ['-cr', target], { windowsHide: true, stdio: 'ignore' })
|
|
195
|
+
} catch {
|
|
196
|
+
return resolve(false)
|
|
197
|
+
}
|
|
198
|
+
child.on('error', () => resolve(false))
|
|
199
|
+
child.on('exit', (code) => {
|
|
200
|
+
ctx.logger.info(
|
|
201
|
+
code === 0
|
|
202
|
+
? `[clean-desktop-shell] cleared extended attributes on ${target}`
|
|
203
|
+
: `[clean-desktop-shell] xattr cleanup skipped (exit ${code})`,
|
|
204
|
+
)
|
|
205
|
+
resolve(code === 0)
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
}
|
|
209
|
+
|
|
113
210
|
/**
|
|
114
211
|
* Pick the download source by racing a HEAD probe against each candidate
|
|
115
212
|
* (direct connection, 3s each). The fastest reachable source goes first —
|
|
@@ -162,21 +259,81 @@ function fetchFile(url, dest) {
|
|
|
162
259
|
})
|
|
163
260
|
}
|
|
164
261
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Extract a zip using whatever the platform actually provides.
|
|
264
|
+
*
|
|
265
|
+
* The archives are ordinary zips, but the readers differ:
|
|
266
|
+
* - Windows: bsdtar ships as tar.exe and reads zip; Expand-Archive is the
|
|
267
|
+
* fallback for hardened images where tar is unavailable.
|
|
268
|
+
* - macOS: ditto -xk is Apple's own extractor and preserves the symlinks
|
|
269
|
+
* inside Electron.app; unzip next; bsdtar last.
|
|
270
|
+
* - Linux: unzip, then tar.
|
|
271
|
+
*
|
|
272
|
+
* Success is decided by the caller (findPayload), not by the exit code — a
|
|
273
|
+
* zip can unpack "successfully" into a layout nobody expects.
|
|
274
|
+
*/
|
|
275
|
+
function extractZip(ctx, zipPath, dest) {
|
|
276
|
+
const strategies = isWin
|
|
277
|
+
? [
|
|
278
|
+
{ cmd: 'tar', args: ['-xf', zipPath, '-C', dest] },
|
|
279
|
+
{
|
|
280
|
+
cmd: 'powershell',
|
|
281
|
+
args: [
|
|
282
|
+
'-NoProfile',
|
|
283
|
+
'-NonInteractive',
|
|
284
|
+
'-Command',
|
|
285
|
+
`Expand-Archive -LiteralPath '${zipPath.replace(/'/g, "''")}' -DestinationPath '${dest.replace(/'/g, "''")}' -Force`,
|
|
286
|
+
],
|
|
287
|
+
},
|
|
288
|
+
]
|
|
289
|
+
: isMac
|
|
290
|
+
? [
|
|
291
|
+
{ cmd: 'ditto', args: ['-xk', zipPath, dest] },
|
|
292
|
+
{ cmd: 'unzip', args: ['-q', '-o', zipPath, '-d', dest] },
|
|
293
|
+
{ cmd: 'tar', args: ['-xf', zipPath, '-C', dest] },
|
|
294
|
+
]
|
|
295
|
+
: [
|
|
296
|
+
{ cmd: 'unzip', args: ['-q', '-o', zipPath, '-d', dest] },
|
|
297
|
+
{ cmd: 'tar', args: ['-xf', zipPath, '-C', dest] },
|
|
298
|
+
]
|
|
299
|
+
|
|
300
|
+
return (async () => {
|
|
301
|
+
mkdirSync(dest, { recursive: true })
|
|
302
|
+
for (const s of strategies) {
|
|
303
|
+
const ok = await runExtractor(s.cmd, s.args)
|
|
304
|
+
if (ok) {
|
|
305
|
+
ctx.logger.info(`[clean-desktop-shell] extracted with ${s.cmd}`)
|
|
306
|
+
return true
|
|
307
|
+
}
|
|
308
|
+
ctx.logger.warn(`[clean-desktop-shell] extractor unavailable or failed: ${s.cmd}`)
|
|
309
|
+
}
|
|
310
|
+
return false
|
|
311
|
+
})()
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function runExtractor(cmd, args) {
|
|
315
|
+
return new Promise((resolve) => {
|
|
316
|
+
let child
|
|
317
|
+
try {
|
|
318
|
+
child = spawn(cmd, args, { windowsHide: true, stdio: 'ignore' })
|
|
319
|
+
} catch {
|
|
320
|
+
return resolve(false)
|
|
321
|
+
}
|
|
322
|
+
child.on('error', () => resolve(false))
|
|
323
|
+
child.on('exit', (code) => resolve(code === 0))
|
|
172
324
|
})
|
|
173
325
|
}
|
|
174
326
|
|
|
175
|
-
/**
|
|
327
|
+
/**
|
|
328
|
+
* Drop version dirs older than the current one (dead weight), plus any
|
|
329
|
+
* scratch dir a crashed extraction left behind.
|
|
330
|
+
*/
|
|
176
331
|
function cleanupOldVersions(root, currentDir) {
|
|
177
332
|
try {
|
|
178
333
|
for (const entry of readdirSync(root)) {
|
|
179
|
-
|
|
334
|
+
const isStaleRuntime = entry.startsWith('electron-v')
|
|
335
|
+
const isScratch = entry.startsWith('.extract-') || entry.startsWith('.electron-')
|
|
336
|
+
if (!isStaleRuntime && !isScratch) continue
|
|
180
337
|
const full = join(root, entry)
|
|
181
338
|
if (full === currentDir) continue
|
|
182
339
|
rmSync(full, { recursive: true, force: true })
|
|
@@ -187,11 +344,15 @@ function cleanupOldVersions(root, currentDir) {
|
|
|
187
344
|
}
|
|
188
345
|
|
|
189
346
|
/**
|
|
190
|
-
* Provision a local electron package's dist/ as the version dir itself,
|
|
191
|
-
*
|
|
192
|
-
* version-dir root
|
|
193
|
-
*
|
|
194
|
-
*
|
|
347
|
+
* Provision a local electron package's dist/ as the version dir itself, so
|
|
348
|
+
* the layout matches a downloaded runtime: <dir>/<EXE_RELPATH> at the
|
|
349
|
+
* version-dir root (on macOS that is <dir>/Electron.app/Contents/MacOS/Electron).
|
|
350
|
+
*
|
|
351
|
+
* Windows: junction (zero-copy, instant) — a 269MB recursive cpSync can be
|
|
352
|
+
* killed by sandbox/AV on large trees, so only fall back to a copy.
|
|
353
|
+
* macOS/Linux: a symlink to dist/ would work too, but a bare .app reached
|
|
354
|
+
* through a symlink is a common source of Gatekeeper/entitlement surprises,
|
|
355
|
+
* so always copy there.
|
|
195
356
|
*/
|
|
196
357
|
function provisionLocalDist(srcPkg, destDir) {
|
|
197
358
|
if (process.platform === 'win32') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-clean-desktop-shell",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.8",
|
|
4
4
|
"description": "Clean desktop shell for DeepSeek Harness (DSH) as a DSH plugin — wraps your web profile in a native window, tray-managed backend, offline auto-reconnect, zero visual changes. DSH 插件形态的纯净桌面壳:复用现有 web profile,托盘管理后端,零视觉改造。",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"cordis.patch.yml",
|
|
23
23
|
"README.md",
|
|
24
24
|
"README.en.md",
|
|
25
|
+
"CONTRIBUTORS.md",
|
|
25
26
|
"LICENSE",
|
|
26
27
|
"version.txt"
|
|
27
28
|
],
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"build": "node scripts/build.mjs",
|
|
35
|
-
"check": "node --check lib/index.js",
|
|
36
|
+
"check": "node --check lib/index.js && node scripts/selftest-runtime.mjs",
|
|
36
37
|
"dev": "electron electron/main.js",
|
|
37
38
|
"icons": "node scripts/gen-icons.mjs",
|
|
38
39
|
"pack": "electron-builder --win nsis",
|
|
@@ -97,7 +98,8 @@
|
|
|
97
98
|
"target": [
|
|
98
99
|
"dmg"
|
|
99
100
|
],
|
|
100
|
-
"category": "public.app-category.developer-tools"
|
|
101
|
+
"category": "public.app-category.developer-tools",
|
|
102
|
+
"icon": "build/icon.png"
|
|
101
103
|
},
|
|
102
104
|
"nsis": {
|
|
103
105
|
"oneClick": true,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-check for the Electron runtime path logic.
|
|
3
|
+
*
|
|
4
|
+
* Guards the macOS launch bug: Electron's three archives have three
|
|
5
|
+
* different layouts, and only darwin ships an app bundle with no top-level
|
|
6
|
+
* binary. This fakes each platform and each archive layout, then asserts
|
|
7
|
+
* join(versionDir, EXE_RELPATH) resolves to a real file — the exact
|
|
8
|
+
* condition ensureRuntime() checks before spawning the shell.
|
|
9
|
+
*
|
|
10
|
+
* Run: node scripts/selftest-runtime.mjs
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
13
|
+
import { dirname, join, sep } from 'node:path'
|
|
14
|
+
import { tmpdir } from 'node:os'
|
|
15
|
+
import { pathToFileURL } from 'node:url'
|
|
16
|
+
|
|
17
|
+
// Layouts as Electron actually ships them (authoritative mapping lives in
|
|
18
|
+
// the `electron` package's own install.js, which writes path.txt):
|
|
19
|
+
// win32 → <dir>/electron.exe
|
|
20
|
+
// linux → <dir>/electron
|
|
21
|
+
// darwin → <dir>/Electron.app/Contents/MacOS/Electron
|
|
22
|
+
const LAYOUTS = {
|
|
23
|
+
win32: ['electron.exe', 'resources/placeholder'],
|
|
24
|
+
linux: ['electron', 'resources/placeholder'],
|
|
25
|
+
darwin: [
|
|
26
|
+
'Electron.app/Contents/MacOS/Electron',
|
|
27
|
+
'Electron.app/Contents/Info.plist',
|
|
28
|
+
],
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let failures = 0
|
|
32
|
+
|
|
33
|
+
function check(label, cond) {
|
|
34
|
+
console.log(` ${cond ? 'ok ' : 'FAIL'} ${label}`)
|
|
35
|
+
if (!cond) failures++
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Materialise an extracted-archive tree under dir. */
|
|
39
|
+
function buildLayout(dir, entries) {
|
|
40
|
+
for (const rel of entries) {
|
|
41
|
+
const full = join(dir, ...rel.split('/').map((s) => s.split('\\').join(sep)))
|
|
42
|
+
mkdirSync(dirname(full), { recursive: true })
|
|
43
|
+
writeFileSync(full, 'binary')
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function forPlatform(platform) {
|
|
48
|
+
Object.defineProperty(process, 'platform', { value: platform, configurable: true })
|
|
49
|
+
// Cache-bust so the module re-evaluates its platform-derived constants.
|
|
50
|
+
const { EXE_RELPATH, EXE_TOP } = await import(
|
|
51
|
+
`${pathToFileURL(join(process.cwd(), 'lib', 'common.js')).href}?p=${platform}`
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const root = mkdtempSync(join(tmpdir(), `dsh-shell-${platform}-`))
|
|
55
|
+
const dir = join(root, 'electron-v33.4.11')
|
|
56
|
+
console.log(`\n[${platform}]`)
|
|
57
|
+
try {
|
|
58
|
+
buildLayout(dir, LAYOUTS[platform])
|
|
59
|
+
const exe = join(dir, EXE_RELPATH)
|
|
60
|
+
|
|
61
|
+
check(`EXE_TOP = ${EXE_TOP}`, typeof EXE_TOP === 'string' && EXE_TOP.length > 0)
|
|
62
|
+
check(`binary resolves: ${EXE_RELPATH}`, existsSync(exe))
|
|
63
|
+
check('resolved path lives inside the version dir', exe.startsWith(dir))
|
|
64
|
+
} finally {
|
|
65
|
+
rmSync(root, { recursive: true, force: true })
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Regression guard: prove the pre-fix constant was broken on darwin, so this
|
|
70
|
+
// test would actually have caught the bug rather than just passing forever.
|
|
71
|
+
async function regression() {
|
|
72
|
+
const root = mkdtempSync(join(tmpdir(), 'dsh-shell-legacy-'))
|
|
73
|
+
const dir = join(root, 'electron-v33.4.11')
|
|
74
|
+
console.log('\n[regression: pre-fix darwin behaviour]')
|
|
75
|
+
try {
|
|
76
|
+
buildLayout(dir, LAYOUTS.darwin)
|
|
77
|
+
const legacyExe = 'electron' // what EXE_NAME used to be on every non-Windows platform
|
|
78
|
+
check(`legacy join(dir, 'electron') is absent — the bug`, !existsSync(join(dir, legacyExe)))
|
|
79
|
+
} finally {
|
|
80
|
+
rmSync(root, { recursive: true, force: true })
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
await regression()
|
|
85
|
+
for (const p of ['win32', 'linux', 'darwin']) {
|
|
86
|
+
await forPlatform(p)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.log(`\n${failures === 0 ? 'PASS' : `FAIL (${failures} check(s))`}`)
|
|
90
|
+
process.exit(failures === 0 ? 0 : 1)
|
package/src/host/common.js
CHANGED
|
@@ -12,11 +12,42 @@ import { fileURLToPath } from 'node:url'
|
|
|
12
12
|
export const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
13
13
|
export const MAIN_JS = join(PKG_ROOT, 'electron', 'main.js')
|
|
14
14
|
export const isWin = process.platform === 'win32'
|
|
15
|
-
export const
|
|
15
|
+
export const isMac = process.platform === 'darwin'
|
|
16
16
|
export const ARCH = process.arch === 'arm64' ? 'arm64' : 'x64'
|
|
17
|
-
export const PLATFORM = isWin ? 'win32' :
|
|
17
|
+
export const PLATFORM = isWin ? 'win32' : isMac ? 'darwin' : 'linux'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Electron binary path *relative to the extracted runtime dir*.
|
|
21
|
+
*
|
|
22
|
+
* The three upstream archives do not share a layout — only darwin ships an
|
|
23
|
+
* app bundle, and it is the one case with no top-level executable:
|
|
24
|
+
* win32 → electron.exe
|
|
25
|
+
* linux → electron
|
|
26
|
+
* darwin → Electron.app/Contents/MacOS/Electron
|
|
27
|
+
*
|
|
28
|
+
* Authoritative source: the `electron` package's own install.js, which writes
|
|
29
|
+
* exactly this relative path into path.txt for `require('electron')`.
|
|
30
|
+
*/
|
|
31
|
+
export const EXE_RELPATH = isWin
|
|
32
|
+
? 'electron.exe'
|
|
33
|
+
: isMac
|
|
34
|
+
? join('Electron.app', 'Contents', 'MacOS', 'Electron')
|
|
35
|
+
: 'electron'
|
|
36
|
+
|
|
37
|
+
/** First path segment of EXE_RELPATH — what a successful extract must leave behind. */
|
|
38
|
+
export const EXE_TOP = isWin ? 'electron.exe' : isMac ? 'Electron.app' : 'electron'
|
|
39
|
+
|
|
40
|
+
/** DSH home, honouring DSH_HOME the same way dsh-home-paths does. */
|
|
41
|
+
export function dshHome() {
|
|
42
|
+
return process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
43
|
+
}
|
|
18
44
|
|
|
19
45
|
/** Where the self-provisioned runtimes live (shared with icon.js). */
|
|
20
46
|
export function runtimeRoot() {
|
|
21
|
-
return join(
|
|
47
|
+
return join(dshHome(), 'desktop-shell-runtime')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Launch diagnostics land here — the only thing a headless user can send us. */
|
|
51
|
+
export function launchLogPath() {
|
|
52
|
+
return join(dshHome(), 'desktop-shell-launch.log')
|
|
22
53
|
}
|
package/src/host/index.js
CHANGED
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* icon.js (Windows taskbar icon).
|
|
10
10
|
*/
|
|
11
11
|
import { spawn } from 'node:child_process'
|
|
12
|
-
import {
|
|
12
|
+
import { appendFileSync } from 'node:fs'
|
|
13
|
+
import { PKG_ROOT, MAIN_JS, dshHome, runtimeRoot, launchLogPath } from './common.js'
|
|
13
14
|
import { ensureRuntime } from './runtime.js'
|
|
14
15
|
import { patchExeIcon } from './icon.js'
|
|
15
16
|
|
|
@@ -36,11 +37,50 @@ export function apply(ctx) {
|
|
|
36
37
|
await patchExeIcon(ctx, exe).catch(() => {})
|
|
37
38
|
launchShell(exe, ctx)
|
|
38
39
|
} catch (err) {
|
|
39
|
-
ctx
|
|
40
|
+
reportLaunchFailure(ctx, err)
|
|
40
41
|
}
|
|
41
42
|
})()
|
|
42
43
|
}
|
|
43
44
|
|
|
45
|
+
/**
|
|
46
|
+
* A provisioning failure used to vanish into ctx.logger — invisible to
|
|
47
|
+
* anyone who is not already tailing the DSH log. That is precisely how the
|
|
48
|
+
* macOS launch bug survived several releases: there was no window, no error
|
|
49
|
+
* dialog, and nothing on disk to send back.
|
|
50
|
+
*
|
|
51
|
+
* Write a diagnostics file next to the runtime and name it in the warning,
|
|
52
|
+
* so a user on an untested platform can hand us something actionable.
|
|
53
|
+
*/
|
|
54
|
+
function reportLaunchFailure(ctx, err) {
|
|
55
|
+
const message = err?.message ?? String(err)
|
|
56
|
+
ctx.logger.warn(`[clean-desktop-shell] shell launch failed: ${message}`)
|
|
57
|
+
|
|
58
|
+
const logPath = launchLogPath()
|
|
59
|
+
const body = [
|
|
60
|
+
`time: ${new Date().toISOString()}`,
|
|
61
|
+
`platform: ${process.platform} (${process.arch})`,
|
|
62
|
+
`node: ${process.version}`,
|
|
63
|
+
`dsh home: ${dshHome()}`,
|
|
64
|
+
`runtime: ${runtimeRoot()}`,
|
|
65
|
+
`entry: ${MAIN_JS}`,
|
|
66
|
+
`error: ${message}`,
|
|
67
|
+
'',
|
|
68
|
+
'Things worth checking:',
|
|
69
|
+
' - first launch downloads the Electron runtime; a blocked network fails here',
|
|
70
|
+
' - set DSH_SHELL_ELECTRON_DIR to an electron package to skip the download',
|
|
71
|
+
' - macOS binary: <runtime>/electron-v<ver>/Electron.app/Contents/MacOS/Electron',
|
|
72
|
+
' - macOS: unsandboxed extractors may drop the executable bit (chmod +x)',
|
|
73
|
+
'',
|
|
74
|
+
].join('\n')
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
appendFileSync(logPath, body + '\n')
|
|
78
|
+
ctx.logger.warn(`[clean-desktop-shell] diagnostics written to ${logPath}`)
|
|
79
|
+
} catch {
|
|
80
|
+
// Nothing else a headless host process can do.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
44
84
|
function launchShell(exe, ctx) {
|
|
45
85
|
if (launched) return
|
|
46
86
|
const child = spawn(exe, [MAIN_JS], {
|