@mzzsfy/dsh-shell-select 0.4.4 → 0.4.5
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/package.json +1 -1
- package/src/client.js +1 -1
- package/src/guard.js +12 -4
- package/test/guard-entry.test.mjs +3 -0
- package/test/guard-lifecycle.test.mjs +110 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mzzsfy/dsh-shell-select",
|
|
3
3
|
"description": "Windows shell 接管:替换默认 pwsh 工具为可配置多客户端的 shell 工具(git bash/WSL/cmd/pwsh,自定义路径,默认客户端,模型按名选择);支持命令 deny 黑名单、会话终端卡增强和直接 pwsh 接管徽章,接管失败自动回退官方 pwsh 链",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"exports": {
|
package/src/client.js
CHANGED
|
@@ -159,7 +159,7 @@ window.__ModuleLoader__.load({
|
|
|
159
159
|
})
|
|
160
160
|
.catch((error) => {
|
|
161
161
|
pathMapRequested = false
|
|
162
|
-
console.warn('shell-select: pathmap fetch failed'
|
|
162
|
+
console.warn('shell-select: pathmap fetch failed: ' + (error instanceof Error ? error.message : String(error)))
|
|
163
163
|
})
|
|
164
164
|
}
|
|
165
165
|
// LOGIC-END loadPathMap
|
package/src/guard.js
CHANGED
|
@@ -208,6 +208,9 @@ export async function installGuard(ctx, {
|
|
|
208
208
|
const dead = detectDeadState(ctx.loader, { applyState: shellSelectApplyState })
|
|
209
209
|
if (dead === true && mounted.size === 0 && mounting === null) await mountOfficial()
|
|
210
210
|
else if (dead === false && (mounted.size > 0 || mounting !== null)) await unmountOfficial()
|
|
211
|
+
} catch {
|
|
212
|
+
// 停机竞态:fiber 已停而末次 tick 先于 effect 清理触发,loader 访问必抛;
|
|
213
|
+
// rejection 逃逸会被宿主 fail-loud 判 fatal load failure 杀进程,静默放行
|
|
211
214
|
} finally {
|
|
212
215
|
sweeping = false
|
|
213
216
|
}
|
|
@@ -246,10 +249,15 @@ export async function installGuard(ctx, {
|
|
|
246
249
|
ctx.logger.info?.(`shell-select/guard: 初始判定 dead=${initial}`)
|
|
247
250
|
if (initial === true) await mountOfficial()
|
|
248
251
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
252
|
+
// 定时器随 fiber 清理:裸 setInterval 会在行停用后残留 tick,停机序列中
|
|
253
|
+
// 访问 loader 抛错并经宿主 fail-loud 升级为进程级 fatal
|
|
254
|
+
ctx.effect(() => {
|
|
255
|
+
const timer = setInterval(() => {
|
|
256
|
+
void sweep()
|
|
257
|
+
}, sweepIntervalMs)
|
|
258
|
+
timer.unref?.()
|
|
259
|
+
return () => clearInterval(timer)
|
|
260
|
+
}, 'shell-select/guard sweep timer')
|
|
253
261
|
return undefined
|
|
254
262
|
}
|
|
255
263
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// guard 生命周期:定时器随 fiber 清理(effect),停机竞态异常不逃逸为
|
|
2
|
+
// unhandledRejection——宿主 fail-loud 会把这类逃逸判为 fatal load failure
|
|
3
|
+
// 杀掉整个进程(dsh-maintain 重启停机窗口实测事故)。
|
|
4
|
+
|
|
5
|
+
import { test } from 'node:test'
|
|
6
|
+
import assert from 'node:assert/strict'
|
|
7
|
+
import { setTimeout as sleep } from 'node:timers/promises'
|
|
8
|
+
import { installGuard } from '../src/guard.js'
|
|
9
|
+
import { MAIN_ROW_ID } from '../src/guard-state.mjs'
|
|
10
|
+
|
|
11
|
+
const OFFICIAL_MODULE = (name) => ({ __name: name, name, apply() {} })
|
|
12
|
+
const makeImport = async (packageName) => OFFICIAL_MODULE(packageName)
|
|
13
|
+
|
|
14
|
+
function row({ id, disabled = false, running = false }) {
|
|
15
|
+
return {
|
|
16
|
+
options: { id },
|
|
17
|
+
get disabled() {
|
|
18
|
+
return disabled
|
|
19
|
+
},
|
|
20
|
+
fiber: running ? { uid: 'f' } : undefined,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function deadWorld() {
|
|
25
|
+
return [
|
|
26
|
+
row({ id: MAIN_ROW_ID, disabled: true }),
|
|
27
|
+
row({ id: 'tool-pwsh', disabled: true }),
|
|
28
|
+
row({ id: 'pwsh-sandbox', disabled: true }),
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function stubCtx({ rows, resolveCount = { value: 0 } }) {
|
|
33
|
+
const log = []
|
|
34
|
+
const mounts = []
|
|
35
|
+
const effects = []
|
|
36
|
+
const listeners = new Map()
|
|
37
|
+
const ctx = {
|
|
38
|
+
logger: {
|
|
39
|
+
warn: (msg) => log.push(['warn', msg]),
|
|
40
|
+
error: (msg) => log.push(['error', msg]),
|
|
41
|
+
info: (msg) => log.push(['info', msg]),
|
|
42
|
+
},
|
|
43
|
+
get loader() {
|
|
44
|
+
return {
|
|
45
|
+
resolve(id) {
|
|
46
|
+
resolveCount.value += 1
|
|
47
|
+
const found = rows.find((candidate) => candidate.options.id === id)
|
|
48
|
+
if (found) return found
|
|
49
|
+
throw new Error('not found')
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
on(event, handler) {
|
|
54
|
+
listeners.set(event, handler)
|
|
55
|
+
},
|
|
56
|
+
effect(execute, label) {
|
|
57
|
+
const dispose = execute()
|
|
58
|
+
effects.push({ label, dispose })
|
|
59
|
+
return dispose
|
|
60
|
+
},
|
|
61
|
+
plugin(module, config) {
|
|
62
|
+
mounts.push({ moduleName: module.__name, config })
|
|
63
|
+
return Promise.resolve({
|
|
64
|
+
dispose: () => {
|
|
65
|
+
mounts.length = 0
|
|
66
|
+
return Promise.resolve()
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
return { ctx, log, mounts, effects, resolveCount }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test('定时器随 fiber 清理:effect dispose 后不再扫描', async () => {
|
|
75
|
+
const sweepIntervalMs = 20
|
|
76
|
+
const { ctx, mounts, effects, resolveCount } = stubCtx({ rows: deadWorld() })
|
|
77
|
+
await installGuard(ctx, { importOfficial: makeImport, sweepIntervalMs })
|
|
78
|
+
assert.equal(mounts.length, 2)
|
|
79
|
+
const timerEffect = effects.find((entry) => entry.label.includes('sweep'))
|
|
80
|
+
assert.ok(timerEffect, 'sweep 定时器必须注册为 effect')
|
|
81
|
+
assert.equal(typeof timerEffect.dispose, 'function')
|
|
82
|
+
timerEffect.dispose()
|
|
83
|
+
await sleep(sweepIntervalMs * 3)
|
|
84
|
+
const settled = resolveCount.value
|
|
85
|
+
await sleep(sweepIntervalMs * 3)
|
|
86
|
+
assert.equal(resolveCount.value, settled, 'dispose 后不得再有 sweep 扫描')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('sweep 异常不逃逸:停机竞态下 loader 抛错不产生 unhandledRejection', async () => {
|
|
90
|
+
const sweepIntervalMs = 20
|
|
91
|
+
const { ctx, mounts } = stubCtx({ rows: deadWorld() })
|
|
92
|
+
await installGuard(ctx, { importOfficial: makeImport, sweepIntervalMs })
|
|
93
|
+
assert.equal(mounts.length, 2)
|
|
94
|
+
// 模拟 fiber 停用后 ctx.loader 访问抛 inactive context 错误
|
|
95
|
+
Object.defineProperty(ctx, 'loader', {
|
|
96
|
+
get() {
|
|
97
|
+
throw new Error('cannot get required service "loader" in inactive context')
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
const rejections = []
|
|
101
|
+
const onRejection = (error) => rejections.push(error)
|
|
102
|
+
process.on('unhandledRejection', onRejection)
|
|
103
|
+
try {
|
|
104
|
+
await sleep(sweepIntervalMs * 4)
|
|
105
|
+
assert.equal(mounts.length, 2, '异常不得破坏已挂载状态')
|
|
106
|
+
assert.deepEqual(rejections, [])
|
|
107
|
+
} finally {
|
|
108
|
+
process.off('unhandledRejection', onRejection)
|
|
109
|
+
}
|
|
110
|
+
})
|