dsh-clean-desktop-shell 0.1.11 → 0.1.13
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 +25 -19
- package/README.en.md +422 -333
- package/README.md +363 -278
- package/electron/aumid.js +17 -17
- package/electron/crashGuard.js +60 -60
- package/electron/error.html +113 -113
- package/electron/main.js +203 -187
- package/electron/outage.js +60 -0
- package/electron/preload.js +216 -49
- package/electron/progress-preload.js +11 -11
- package/electron/progress.html +79 -79
- package/electron/progress.js +56 -56
- package/electron/service.js +559 -458
- package/electron/shortcut.js +122 -122
- package/electron/tray.js +232 -232
- package/electron/window.js +603 -264
- package/lib/client.js +41 -6
- package/lib/common.js +74 -74
- package/lib/icon.js +85 -51
- package/lib/index.js +90 -15
- package/lib/runtime.js +423 -423
- package/package.json +140 -127
- package/scripts/check-syntax.mjs +54 -54
- package/scripts/selftest-recovery.mjs +140 -0
- package/scripts/selftest-runtime.mjs +90 -90
- package/src/client/client.js +41 -6
- package/src/host/common.js +74 -74
- package/src/host/icon.js +85 -51
- package/src/host/index.js +90 -15
- package/src/host/runtime.js +423 -423
- package/version.txt +1 -1
|
@@ -1,90 +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)
|
|
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/client/client.js
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-clean-desktop-shell — client half (web browser bundle).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Bridges the page's DSH client-runtime connection lifecycle into the
|
|
5
|
+
* Electron shell so HTTP health alone can never hide a terminal disconnect
|
|
6
|
+
* (e.g. the backend restarted under the page: HTTP answers again while the
|
|
7
|
+
* page's WebSocket generation is dead).
|
|
8
|
+
*
|
|
9
|
+
* - ctx.connection.state ('connected' | 'connecting' | 'disconnected') is
|
|
10
|
+
* forwarded to the main process (shell:client-connection → preload
|
|
11
|
+
* shellAPI.connectionReport);
|
|
12
|
+
* - main-process reconnect requests (shell:client-reconnect →
|
|
13
|
+
* shellAPI.onReconnectRequest) call ctx.connection.reconnect() —
|
|
14
|
+
* recovery through the app's own reconnect loop, never a reload.
|
|
15
|
+
*
|
|
16
|
+
* The supported baseline provides ctx.connection.state / reconnect();
|
|
17
|
+
* missing APIs fail activation loudly instead of degrading. A plain browser
|
|
18
|
+
* (no shellAPI — no Electron shell) is the only silent case.
|
|
9
19
|
*/
|
|
10
20
|
window.__ModuleLoader__.load({
|
|
11
21
|
id: 'dsh-clean-desktop-shell',
|
|
@@ -13,7 +23,32 @@ window.__ModuleLoader__.load({
|
|
|
13
23
|
var module = { exports: {} };
|
|
14
24
|
var exports = module.exports;
|
|
15
25
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
16
|
-
exports.
|
|
26
|
+
exports.inject = ['connection'];
|
|
27
|
+
exports.apply = function (ctx) {
|
|
28
|
+
var api = window.shellAPI;
|
|
29
|
+
if (!api) return;
|
|
30
|
+
var connection = ctx.connection;
|
|
31
|
+
var latest = null;
|
|
32
|
+
var notify = function () {
|
|
33
|
+
var state = connection.state.getSnapshot();
|
|
34
|
+
if (state === latest) return;
|
|
35
|
+
latest = state;
|
|
36
|
+
if (state === 'connected' || state === 'connecting' || state === 'disconnected') {
|
|
37
|
+
api.connectionReport(state);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var unsubscribe = connection.state.subscribe(notify);
|
|
41
|
+
notify(); // report whatever the runtime already is — a lost initial disconnect must not be hidden
|
|
42
|
+
var unregisterRequest = api.onReconnectRequest(function () {
|
|
43
|
+
connection.reconnect();
|
|
44
|
+
});
|
|
45
|
+
ctx.effect(function () {
|
|
46
|
+
return function () {
|
|
47
|
+
unsubscribe();
|
|
48
|
+
unregisterRequest();
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
};
|
|
17
52
|
return module.exports;
|
|
18
53
|
},
|
|
19
54
|
});
|
package/src/host/common.js
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared constants and helpers for the host half modules.
|
|
3
|
-
*
|
|
4
|
-
* These compiled files sit at lib/<name>.js, so two dirname hops reach the
|
|
5
|
-
* package root — the same layout as src/host/ before build, and the same
|
|
6
|
-
* location electron/ and build/ live in the published package.
|
|
7
|
-
*/
|
|
8
|
-
import { homedir } from 'node:os'
|
|
9
|
-
import { spawn } from 'node:child_process'
|
|
10
|
-
import { dirname, join } from 'node:path'
|
|
11
|
-
import { fileURLToPath } from 'node:url'
|
|
12
|
-
|
|
13
|
-
export const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
14
|
-
export const MAIN_JS = join(PKG_ROOT, 'electron', 'main.js')
|
|
15
|
-
export const isWin = process.platform === 'win32'
|
|
16
|
-
export const isMac = process.platform === 'darwin'
|
|
17
|
-
export const ARCH = process.arch === 'arm64' ? 'arm64' : 'x64'
|
|
18
|
-
export const PLATFORM = isWin ? 'win32' : isMac ? 'darwin' : 'linux'
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Electron binary path *relative to the extracted runtime dir*.
|
|
22
|
-
*
|
|
23
|
-
* The three upstream archives do not share a layout — only darwin ships an
|
|
24
|
-
* app bundle, and it is the one case with no top-level executable:
|
|
25
|
-
* win32 → electron.exe
|
|
26
|
-
* linux → electron
|
|
27
|
-
* darwin → Electron.app/Contents/MacOS/Electron
|
|
28
|
-
*
|
|
29
|
-
* Authoritative source: the `electron` package's own install.js, which writes
|
|
30
|
-
* exactly this relative path into path.txt for `require('electron')`.
|
|
31
|
-
*/
|
|
32
|
-
export const EXE_RELPATH = isWin
|
|
33
|
-
? 'electron.exe'
|
|
34
|
-
: isMac
|
|
35
|
-
? join('Electron.app', 'Contents', 'MacOS', 'Electron')
|
|
36
|
-
: 'electron'
|
|
37
|
-
|
|
38
|
-
/** First path segment of EXE_RELPATH — what a successful extract must leave behind. */
|
|
39
|
-
export const EXE_TOP = isWin ? 'electron.exe' : isMac ? 'Electron.app' : 'electron'
|
|
40
|
-
|
|
41
|
-
/** DSH home, honouring DSH_HOME the same way dsh-home-paths does. */
|
|
42
|
-
export function dshHome() {
|
|
43
|
-
return process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Where the self-provisioned runtimes live (shared with icon.js). */
|
|
47
|
-
export function runtimeRoot() {
|
|
48
|
-
return join(dshHome(), 'desktop-shell-runtime')
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/** Launch diagnostics land here — the only thing a headless user can send us. */
|
|
52
|
-
export function launchLogPath() {
|
|
53
|
-
return join(dshHome(), 'desktop-shell-launch.log')
|
|
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
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Shared constants and helpers for the host half modules.
|
|
3
|
+
*
|
|
4
|
+
* These compiled files sit at lib/<name>.js, so two dirname hops reach the
|
|
5
|
+
* package root — the same layout as src/host/ before build, and the same
|
|
6
|
+
* location electron/ and build/ live in the published package.
|
|
7
|
+
*/
|
|
8
|
+
import { homedir } from 'node:os'
|
|
9
|
+
import { spawn } from 'node:child_process'
|
|
10
|
+
import { dirname, join } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
export const PKG_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
14
|
+
export const MAIN_JS = join(PKG_ROOT, 'electron', 'main.js')
|
|
15
|
+
export const isWin = process.platform === 'win32'
|
|
16
|
+
export const isMac = process.platform === 'darwin'
|
|
17
|
+
export const ARCH = process.arch === 'arm64' ? 'arm64' : 'x64'
|
|
18
|
+
export const PLATFORM = isWin ? 'win32' : isMac ? 'darwin' : 'linux'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Electron binary path *relative to the extracted runtime dir*.
|
|
22
|
+
*
|
|
23
|
+
* The three upstream archives do not share a layout — only darwin ships an
|
|
24
|
+
* app bundle, and it is the one case with no top-level executable:
|
|
25
|
+
* win32 → electron.exe
|
|
26
|
+
* linux → electron
|
|
27
|
+
* darwin → Electron.app/Contents/MacOS/Electron
|
|
28
|
+
*
|
|
29
|
+
* Authoritative source: the `electron` package's own install.js, which writes
|
|
30
|
+
* exactly this relative path into path.txt for `require('electron')`.
|
|
31
|
+
*/
|
|
32
|
+
export const EXE_RELPATH = isWin
|
|
33
|
+
? 'electron.exe'
|
|
34
|
+
: isMac
|
|
35
|
+
? join('Electron.app', 'Contents', 'MacOS', 'Electron')
|
|
36
|
+
: 'electron'
|
|
37
|
+
|
|
38
|
+
/** First path segment of EXE_RELPATH — what a successful extract must leave behind. */
|
|
39
|
+
export const EXE_TOP = isWin ? 'electron.exe' : isMac ? 'Electron.app' : 'electron'
|
|
40
|
+
|
|
41
|
+
/** DSH home, honouring DSH_HOME the same way dsh-home-paths does. */
|
|
42
|
+
export function dshHome() {
|
|
43
|
+
return process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Where the self-provisioned runtimes live (shared with icon.js). */
|
|
47
|
+
export function runtimeRoot() {
|
|
48
|
+
return join(dshHome(), 'desktop-shell-runtime')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Launch diagnostics land here — the only thing a headless user can send us. */
|
|
52
|
+
export function launchLogPath() {
|
|
53
|
+
return join(dshHome(), 'desktop-shell-launch.log')
|
|
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
|
@@ -1,51 +1,85 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Patch the runtime electron.exe's icon resource so the Windows taskbar
|
|
3
|
-
* shows our whale icon.
|
|
4
|
-
*
|
|
5
|
-
* A bare runtime exe ships Electron's default icon and — as documented —
|
|
6
|
-
* no runtime API (BrowserWindow icon, setAppDetails, AUMID shortcuts) can
|
|
7
|
-
* change the taskbar button: it reads the exe's icon resource. rcedit
|
|
8
|
-
* (electron team's official tool) rewrites it in place.
|
|
9
|
-
*
|
|
10
|
-
* Best-effort: icon patching must never block the shell from launching.
|
|
11
|
-
* Idempotent: a marker file next to the exe records success; a re-provisioned
|
|
12
|
-
* (new version) exe has no marker and gets patched again.
|
|
13
|
-
*/
|
|
14
|
-
import { spawn } from 'node:child_process'
|
|
15
|
-
import { existsSync, writeFileSync } from 'node:fs'
|
|
16
|
-
import { join } from 'node:path'
|
|
17
|
-
import { PKG_ROOT, isWin, runtimeRoot, fetchFile } from './common.js'
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Patch the runtime electron.exe's icon resource so the Windows taskbar
|
|
3
|
+
* shows our whale icon.
|
|
4
|
+
*
|
|
5
|
+
* A bare runtime exe ships Electron's default icon and — as documented —
|
|
6
|
+
* no runtime API (BrowserWindow icon, setAppDetails, AUMID shortcuts) can
|
|
7
|
+
* change the taskbar button: it reads the exe's icon resource. rcedit
|
|
8
|
+
* (electron team's official tool) rewrites it in place.
|
|
9
|
+
*
|
|
10
|
+
* Best-effort: icon patching must never block the shell from launching.
|
|
11
|
+
* Idempotent: a marker file next to the exe records success; a re-provisioned
|
|
12
|
+
* (new version) exe has no marker and gets patched again.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from 'node:child_process'
|
|
15
|
+
import { existsSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
import { PKG_ROOT, isWin, runtimeRoot, fetchFile } from './common.js'
|
|
18
|
+
|
|
19
|
+
const RCEDIT_NAME = 'rcedit-x64.exe'
|
|
20
|
+
const RCEDIT_URL = 'https://github.com/electron/rcedit/releases/download/v2.0.0/rcedit-x64.exe'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* rcedit is a ~1.3 MB self-contained exe, and its download sits on the launch
|
|
24
|
+
* path (Windows locks a running image, so the patch has to happen before the
|
|
25
|
+
* exe is spawned). fetchFile's default budget — 600 s — is sized for the
|
|
26
|
+
* ~100 MB Electron runtime, not for this: behind a stalled proxy it could keep
|
|
27
|
+
* the window off screen for ten minutes. Cap it well below that; a miss only
|
|
28
|
+
* costs the default icon.
|
|
29
|
+
*/
|
|
30
|
+
const RCEDIT_TIMEOUT_SEC = 20
|
|
31
|
+
|
|
32
|
+
let rceditPromise = null
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fetch (once) the cached rcedit binary next to the runtimes. Memoised so the
|
|
36
|
+
* host half can start this alongside the Electron runtime download and have
|
|
37
|
+
* the icon step reuse the same fetch instead of serialising behind it.
|
|
38
|
+
*
|
|
39
|
+
* Never rejects: every failure mode just means "no rcedit, default icon".
|
|
40
|
+
*/
|
|
41
|
+
export function ensureRcedit(ctx) {
|
|
42
|
+
if (!rceditPromise) {
|
|
43
|
+
rceditPromise = (async () => {
|
|
44
|
+
try {
|
|
45
|
+
const rcedit = join(runtimeRoot(), RCEDIT_NAME)
|
|
46
|
+
if (existsSync(rcedit)) return rcedit
|
|
47
|
+
ctx.logger.info('[clean-desktop-shell] downloading rcedit for icon patching')
|
|
48
|
+
if (await fetchFile(RCEDIT_URL, rcedit, RCEDIT_TIMEOUT_SEC)) return rcedit
|
|
49
|
+
ctx.logger.warn('[clean-desktop-shell] rcedit download failed — taskbar icon stays default')
|
|
50
|
+
} catch (err) {
|
|
51
|
+
ctx.logger.warn(`[clean-desktop-shell] rcedit unavailable (${err?.message ?? err}) — taskbar icon stays default`)
|
|
52
|
+
}
|
|
53
|
+
// Not memoised as a failure: a later launch deserves a fresh attempt.
|
|
54
|
+
rceditPromise = null
|
|
55
|
+
return null
|
|
56
|
+
})()
|
|
57
|
+
}
|
|
58
|
+
return rceditPromise
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function patchExeIcon(ctx, exe) {
|
|
62
|
+
if (!isWin) return
|
|
63
|
+
const ico = join(PKG_ROOT, 'build', 'icon.ico')
|
|
64
|
+
if (!existsSync(ico)) return
|
|
65
|
+
const marker = `${exe}.whale-icon`
|
|
66
|
+
if (existsSync(marker)) return
|
|
67
|
+
|
|
68
|
+
const rcedit = await ensureRcedit(ctx)
|
|
69
|
+
if (!rcedit) return
|
|
70
|
+
|
|
71
|
+
const child = spawn(rcedit, [exe, '--set-icon', ico], {
|
|
72
|
+
windowsHide: true,
|
|
73
|
+
stdio: 'ignore',
|
|
74
|
+
})
|
|
75
|
+
const ok = await new Promise((resolve) => {
|
|
76
|
+
child.on('error', () => resolve(false))
|
|
77
|
+
child.on('exit', (code) => resolve(code === 0))
|
|
78
|
+
})
|
|
79
|
+
if (ok) {
|
|
80
|
+
writeFileSync(marker, String(Date.now()), 'utf8')
|
|
81
|
+
ctx.logger.info('[clean-desktop-shell] taskbar icon patched (rcedit)')
|
|
82
|
+
} else {
|
|
83
|
+
ctx.logger.warn('[clean-desktop-shell] rcedit patch failed — taskbar icon stays default')
|
|
84
|
+
}
|
|
85
|
+
}
|