@hyzyn/dsh-safe 0.3.2 → 0.4.0
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 +2 -1
- package/README.md +2 -1
- package/lib/i18n.js +12 -2
- package/lib/update.js +12 -5
- package/lib/verify.js +78 -0
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -67,6 +67,7 @@ Every short flag has an equivalent long form (`-u` = `--update`, `-y` = `--yes`,
|
|
|
67
67
|
| `--to <version>` | Target dsh version, also how you roll back (explicit downgrades allowed); dsh-safe itself always upgrades to the latest |
|
|
68
68
|
| `--self` | Update dsh-safe itself only; dsh and quarantine state untouched |
|
|
69
69
|
| `--no-restore` | Do not auto-restore quarantined plugins after upgrading dsh |
|
|
70
|
+
| `--no-verify` | Skip the post-upgrade parser self-check (boots the new dsh with a throwaway profile to confirm error recognition still works) |
|
|
70
71
|
| `--pm <npm\|pnpm>` | Force the package manager (auto-detected by default) |
|
|
71
72
|
|
|
72
73
|
### Environment variables
|
|
@@ -98,7 +99,7 @@ How upgrading works: `dsh-safe update` auto-detects the dsh package name and ins
|
|
|
98
99
|
- If the patch file itself fails YAML parsing (e.g. broken by hand-editing), plugins cannot be identified and the failure is passed through.
|
|
99
100
|
- Rows inserted via `--patch` overlay layers are not part of the mapping (only the profile patch, the home patch and bundle patches are scanned).
|
|
100
101
|
- To capture stderr, the wrapper pipes dsh's stderr (content is still echoed to the terminal in real time); stdout/stdin pass through unaffected.
|
|
101
|
-
- Match patterns target the dsh 0.1.x error formats; a major dsh upgrade that changes them requires updating the parser.
|
|
102
|
+
- Match patterns target the dsh 0.1.x error formats; a major dsh upgrade that changes them requires updating the parser. Mitigation: after update/-u upgrades dsh it runs a parser self-check — boots the new dsh with a throwaway profile and confirms failures are still recognized, warning right away on mismatch (`--no-verify` skips it).
|
|
102
103
|
- Windows is best-effort: update / --self / list / restore are adapted (.cmd shim parsing, shelled npm/pnpm invocations); the wrapped boot resolves the node entry embedded in dsh's .cmd/.ps1 shim on PATH and spawns `node <entry>` directly (.exe runs as-is, unparseable shims fall back to a shelled spawn), sidestepping Node's ban on spawning .cmd files. Not yet verified end-to-end on a real Windows machine — feedback welcome.
|
|
103
104
|
|
|
104
105
|
## Development
|
package/README.md
CHANGED
|
@@ -67,6 +67,7 @@ Error: dsh: plugin tree failed to load: failed to apply loader entry smoke-broke
|
|
|
67
67
|
| `--to <版本>` | 指定 dsh 的目标版本,也是回滚方式(显式允许降级);dsh-safe 自身始终升到最新 |
|
|
68
68
|
| `--self` | 只更新 dsh-safe 自身,不动 dsh 与隔离状态 |
|
|
69
69
|
| `--no-restore` | 升级 dsh 后不自动恢复被隔离的插件 |
|
|
70
|
+
| `--no-verify` | 跳过升级后的解析器自校验(临时 profile 试启新版 dsh,验证报错识别仍有效) |
|
|
70
71
|
| `--pm <npm\|pnpm>` | 强制指定包管理器(缺省自动探测) |
|
|
71
72
|
|
|
72
73
|
### 环境变量
|
|
@@ -98,7 +99,7 @@ Error: dsh: plugin tree failed to load: failed to apply loader entry smoke-broke
|
|
|
98
99
|
- patch 文件本身 YAML 解析错误(如手改坏了)时无法识别插件,只会透传。
|
|
99
100
|
- `--patch` 覆盖层里插入的行不参与对照表(对照表只扫 profile patch、home patch 与 bundle patch)。
|
|
100
101
|
- 为了捕获 stderr,包装器把 dsh 的 stderr 接到管道(内容仍实时回显到终端);stdout/stdin 直通不受影响。
|
|
101
|
-
- 本项目针对 dsh 0.1.x 的报错格式做匹配;dsh
|
|
102
|
+
- 本项目针对 dsh 0.1.x 的报错格式做匹配;dsh 大版本升级后格式变化时需要同步更新解析器。缓解:update/-u 升级 dsh 后会自动做解析器自校验——临时 profile 试启新版 dsh 并确认报错仍可识别,失配当场告警(`--no-verify` 跳过)。
|
|
102
103
|
- Windows 为尽力支持:update / --self / list / restore 已适配(.cmd shim 解析、shell 方式调用 npm/pnpm);包装启动会把 PATH 上 dsh 的 .cmd/.ps1 shim 解析出内嵌的 node 入口、改为 `node <入口>` 直接启动(.exe 直接运行,shim 解析失败退回 shell 方式),绕开 Node 禁止 spawn .cmd 的限制。尚未在真实 Windows 上端到端验证,欢迎反馈。
|
|
103
104
|
|
|
104
105
|
## 开发
|
package/lib/i18n.js
CHANGED
|
@@ -20,7 +20,7 @@ const ZH = {
|
|
|
20
20
|
dsh-safe list [--profile <名>] 查看隔离名单(缺省列出全部 profile)
|
|
21
21
|
dsh-safe restore --profile <名> (--id <id> | --all) [--dry-run]
|
|
22
22
|
恢复被自动禁用的插件(升级修复后使用)
|
|
23
|
-
dsh-safe update [-y] [--to <版本>] [--self] [--no-restore] [--pm npm|pnpm]
|
|
23
|
+
dsh-safe update [-y] [--to <版本>] [--self] [--no-restore] [--no-verify] [--pm npm|pnpm]
|
|
24
24
|
升级 dsh 与 dsh-safe 自身,并自动恢复被隔离的插件
|
|
25
25
|
dsh-safe help 显示本帮助
|
|
26
26
|
dsh-safe --version 显示版本
|
|
@@ -89,6 +89,11 @@ const ZH = {
|
|
|
89
89
|
updateRestoreSkipped: '[dsh-safe] 已按 --no-restore 跳过恢复;可稍后用 dsh-safe restore --profile <名> --all 恢复。',
|
|
90
90
|
updateVerifyHint: '[dsh-safe] 请启动 dsh 验证;仍不兼容的插件会自动再次隔离。',
|
|
91
91
|
updateRollbackHint: '[dsh-safe] 如需回滚: dsh-safe update --to {version}',
|
|
92
|
+
verifyRunning: '[dsh-safe] 正在自校验解析器:用临时 profile 试启新版 dsh(预期失败)…',
|
|
93
|
+
verifyPassed: '[dsh-safe] 解析器自校验通过:新版 dsh 的报错能被识别,自动隔离可用。',
|
|
94
|
+
verifyFailNoHit: '[dsh-safe] 警告:解析器自校验未通过——无法从新版 dsh 的报错里识别坏插件,自动隔离可能失效;建议回滚(dsh-safe update --to {version})并反馈给 dsh-safe。',
|
|
95
|
+
verifyFailBoot: '[dsh-safe] 警告:解析器自校验未通过——坏插件试启意外成功,未触发报错,自动隔离未验证。',
|
|
96
|
+
verifyFailTimeout: '[dsh-safe] 警告:解析器自校验未完成(试启超时或无法启动),自动隔离未验证。',
|
|
92
97
|
selfUpdateLagHint: '[dsh-safe] dsh-safe 已更新: {old} → {new}(本次运行仍是旧版,下次启动生效)',
|
|
93
98
|
updateNotify: '[dsh-safe] 提示: dsh-safe 有新版本 {new}(当前 {old})',
|
|
94
99
|
updateNotifyHow: ' 升级: npm i -g {name} 或 dsh-safe update --self',
|
|
@@ -112,7 +117,7 @@ Usage:
|
|
|
112
117
|
dsh-safe list [--profile <name>] show quarantined plugins (defaults to all profiles)
|
|
113
118
|
dsh-safe restore --profile <name> (--id <id> | --all) [--dry-run]
|
|
114
119
|
re-enable auto-disabled plugins (after a fixed plugin upgrade)
|
|
115
|
-
dsh-safe update [-y] [--to <ver>] [--self] [--no-restore] [--pm npm|pnpm]
|
|
120
|
+
dsh-safe update [-y] [--to <ver>] [--self] [--no-restore] [--no-verify] [--pm npm|pnpm]
|
|
116
121
|
upgrade dsh and dsh-safe itself, auto-restore quarantined plugins
|
|
117
122
|
dsh-safe help show this help
|
|
118
123
|
dsh-safe --version show version
|
|
@@ -184,6 +189,11 @@ Notes:
|
|
|
184
189
|
updateRestoreSkipped: '[dsh-safe] restore skipped due to --no-restore; restore later with dsh-safe restore --profile <name> --all.',
|
|
185
190
|
updateVerifyHint: '[dsh-safe] start dsh to verify; plugins still incompatible will be auto-quarantined again.',
|
|
186
191
|
updateRollbackHint: '[dsh-safe] to roll back: dsh-safe update --to {version}',
|
|
192
|
+
verifyRunning: '[dsh-safe] verifying the parser: booting the new dsh with a throwaway profile (expected to fail)…',
|
|
193
|
+
verifyPassed: '[dsh-safe] parser self-check passed: failures from the new dsh are recognized, auto-quarantine is available.',
|
|
194
|
+
verifyFailNoHit: '[dsh-safe] warning: parser self-check failed — the broken plugin could not be recognized in the new dsh errors; auto-quarantine may be broken. Consider rolling back (dsh-safe update --to {version}) and reporting it to dsh-safe.',
|
|
195
|
+
verifyFailBoot: '[dsh-safe] warning: parser self-check failed — the deliberately broken profile booted unexpectedly, no error was triggered; auto-quarantine unverified.',
|
|
196
|
+
verifyFailTimeout: '[dsh-safe] warning: parser self-check did not finish (boot timed out or could not start); auto-quarantine unverified.',
|
|
187
197
|
selfUpdateLagHint: '[dsh-safe] dsh-safe updated: {old} → {new} (this run still uses the old version; takes effect on the next run)',
|
|
188
198
|
updateNotify: '[dsh-safe] notice: a new dsh-safe version is available: {new} (current {old})',
|
|
189
199
|
updateNotifyHow: ' to upgrade: npm i -g {name} or dsh-safe update --self',
|
package/lib/update.js
CHANGED
|
@@ -13,6 +13,7 @@ import { createInterface } from 'node:readline/promises'
|
|
|
13
13
|
import { delimiter, dirname, join } from 'node:path'
|
|
14
14
|
import { loadLedger, restoreQuarantine } from './quarantine.js'
|
|
15
15
|
import { readIfExists, updateCheckFile } from './dshpaths.js'
|
|
16
|
+
import { verifyParser } from './verify.js'
|
|
16
17
|
import { t } from './i18n.js'
|
|
17
18
|
|
|
18
19
|
const err = (line) => process.stderr.write(`${line}\n`)
|
|
@@ -235,11 +236,12 @@ export async function cmdUpdateAndBoot(args, { boot } = {}) {
|
|
|
235
236
|
/**
|
|
236
237
|
* 更新主体;bootArgs 非空时更新成功(或已最新/检查失败)后继续启动。
|
|
237
238
|
* 默认同时检查 dsh 与 dsh-safe 自身,谁旧升谁(一条安装命令);--self 只更新自身。
|
|
238
|
-
*
|
|
239
|
+
* dsh 更新成功后默认跑解析器自校验(--no-verify 跳过)。
|
|
240
|
+
* @param {{ to?: string, yes?: boolean, restore?: boolean, pm?: string, selfOnly?: boolean, noVerify?: boolean }} opts
|
|
239
241
|
* @param {string[] | null} bootArgs
|
|
240
|
-
* @param {{ boot?: (args: string[]) => Promise<number
|
|
242
|
+
* @param {{ boot?: (args: string[]) => Promise<number>, verify?: typeof verifyParser }} hooks
|
|
241
243
|
*/
|
|
242
|
-
async function updateAndMaybeBoot(opts, bootArgs, { boot } = {}) {
|
|
244
|
+
async function updateAndMaybeBoot(opts, bootArgs, { boot, verify = verifyParser } = {}) {
|
|
243
245
|
const dshPkg = opts.selfOnly ? null : resolveDshPackage()
|
|
244
246
|
const selfPkg = resolveSelfPackage()
|
|
245
247
|
if (!dshPkg && !selfPkg) {
|
|
@@ -317,6 +319,9 @@ async function updateAndMaybeBoot(opts, bootArgs, { boot } = {}) {
|
|
|
317
319
|
err(t('selfUpdateLagHint', { old: p.pkg.version, new: p.target }))
|
|
318
320
|
}
|
|
319
321
|
}
|
|
322
|
+
if (dshUpdated && !opts.noVerify) {
|
|
323
|
+
await verify({ rollbackVersion: plans.find((p) => p.pkg === dshPkg)?.pkg.version, log: err })
|
|
324
|
+
}
|
|
320
325
|
|
|
321
326
|
if (opts.restore && dshUpdated) {
|
|
322
327
|
const ledger = loadLedger()
|
|
@@ -343,12 +348,13 @@ async function updateAndMaybeBoot(opts, bootArgs, { boot } = {}) {
|
|
|
343
348
|
|
|
344
349
|
/** 解析 update 子命令参数;出错返回 { error: { key, params? } } 供 i18n。 */
|
|
345
350
|
function parseUpdateArgs(args) {
|
|
346
|
-
const opts = { to: undefined, yes: false, restore: true, pm: undefined, selfOnly: false }
|
|
351
|
+
const opts = { to: undefined, yes: false, restore: true, pm: undefined, selfOnly: false, noVerify: false }
|
|
347
352
|
for (let i = 0; i < args.length; i++) {
|
|
348
353
|
const a = args[i]
|
|
349
354
|
if (a === '-y' || a === '--yes') opts.yes = true
|
|
350
355
|
else if (a === '--no-restore') opts.restore = false
|
|
351
356
|
else if (a === '--self') opts.selfOnly = true
|
|
357
|
+
else if (a === '--no-verify') opts.noVerify = true
|
|
352
358
|
else if (a === '--to') {
|
|
353
359
|
const v = args[++i]
|
|
354
360
|
if (v === undefined) return { error: { key: 'updateUnknownFlag', params: { arg: '--to' } } }
|
|
@@ -375,13 +381,14 @@ function parseUpdateArgs(args) {
|
|
|
375
381
|
* dsh 启动参数(宽松处理——不像子命令那样对未知参数报错)。
|
|
376
382
|
*/
|
|
377
383
|
function parseLeadingUpdateArgs(args) {
|
|
378
|
-
const opts = { to: undefined, yes: false, restore: true, pm: undefined, selfOnly: false }
|
|
384
|
+
const opts = { to: undefined, yes: false, restore: true, pm: undefined, selfOnly: false, noVerify: false }
|
|
379
385
|
let i = 0
|
|
380
386
|
for (; i < args.length; i++) {
|
|
381
387
|
const a = args[i]
|
|
382
388
|
if (a === '-y' || a === '--yes') opts.yes = true
|
|
383
389
|
else if (a === '--no-restore') opts.restore = false
|
|
384
390
|
else if (a === '--self') opts.selfOnly = true
|
|
391
|
+
else if (a === '--no-verify') opts.noVerify = true
|
|
385
392
|
else if (a === '--to' && args[i + 1] !== undefined) opts.to = args[++i]
|
|
386
393
|
else if (a.startsWith('--to=')) opts.to = a.slice('--to='.length)
|
|
387
394
|
else if (a === '--pm' && args[i + 1] !== undefined) opts.pm = args[++i]
|
package/lib/verify.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hyzyn/dsh-safe — 升级 dsh 后的解析器自校验。
|
|
3
|
+
*
|
|
4
|
+
* 风险背景:报错解析器与 dsh 的 stderr 格式耦合,dsh 升级可能让特征失配,
|
|
5
|
+
* 保险丝静默失效。自校验在临时 DSH_HOME 里搭一次性 profile(引用现场生成
|
|
6
|
+
* 的坏插件),用 PATH 上新装的 dsh 试启——预期启动失败,再确认解析器能从
|
|
7
|
+
* 报错里识别出坏插件。识别失败只告警(附回滚命令),不阻断后续启动。
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from 'node:child_process'
|
|
10
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { tmpdir } from 'node:os'
|
|
12
|
+
import { join } from 'node:path'
|
|
13
|
+
import { parseFailureReport } from './failures.js'
|
|
14
|
+
import { matchFailures } from './knownrows.js'
|
|
15
|
+
import { resolveDshSpawnTarget } from './dshpaths.js'
|
|
16
|
+
import { t } from './i18n.js'
|
|
17
|
+
|
|
18
|
+
const BAD_ID = 'dsh-safe-verify'
|
|
19
|
+
const BAD_NAME = '@dsh-safe-verify/broken'
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {{
|
|
23
|
+
* rollbackVersion?: string,
|
|
24
|
+
* log?: (line: string) => void,
|
|
25
|
+
* timeoutMs?: number,
|
|
26
|
+
* spawn?: typeof spawnSync,
|
|
27
|
+
* }} [options]
|
|
28
|
+
* @returns {Promise<boolean>} 自校验是否通过
|
|
29
|
+
*/
|
|
30
|
+
export async function verifyParser({ rollbackVersion, log = (line) => process.stderr.write(`${line}\n`), timeoutMs = 60_000, spawn = spawnSync } = {}) {
|
|
31
|
+
log(t('verifyRunning'))
|
|
32
|
+
const home = mkdtempSync(join(tmpdir(), 'dsh-safe-verify-'))
|
|
33
|
+
try {
|
|
34
|
+
// 一次性 profile:patch 行引用现场生成的坏插件(模块级 throw → import 失败)
|
|
35
|
+
const profileDir = join(home, 'profiles', 'verify')
|
|
36
|
+
const modDir = join(profileDir, 'node_modules', BAD_NAME)
|
|
37
|
+
mkdirSync(join(modDir, 'lib'), { recursive: true })
|
|
38
|
+
writeFileSync(join(profileDir, 'package.json'), JSON.stringify({ name: 'dsh-profile-verify', private: true }))
|
|
39
|
+
const patchPath = join(profileDir, 'cordis.patch.yml')
|
|
40
|
+
writeFileSync(patchPath, `- id: ${BAD_ID}\n name: '${BAD_NAME}'\n`)
|
|
41
|
+
writeFileSync(
|
|
42
|
+
join(modDir, 'package.json'),
|
|
43
|
+
JSON.stringify({ name: BAD_NAME, version: '0.0.1', type: 'module', main: 'lib/index.js' }),
|
|
44
|
+
)
|
|
45
|
+
writeFileSync(join(modDir, 'lib', 'index.js'), `throw new Error('dsh-safe parser self-check')\n`)
|
|
46
|
+
|
|
47
|
+
const target = resolveDshSpawnTarget('dsh')
|
|
48
|
+
const { status, stderr } = spawn(target.file, [...target.prefix, '--profile', 'verify'], {
|
|
49
|
+
env: { ...process.env, DSH_HOME: home },
|
|
50
|
+
stdio: ['ignore', 'ignore', 'pipe'],
|
|
51
|
+
timeout: timeoutMs,
|
|
52
|
+
shell: target.shell,
|
|
53
|
+
encoding: 'utf8',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
if (status === 0) {
|
|
57
|
+
log(t('verifyFailBoot'))
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
if (status === null) {
|
|
61
|
+
// 超时被杀或无法启动试启进程
|
|
62
|
+
log(t('verifyFailTimeout'))
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
const report = parseFailureReport(stderr ?? '')
|
|
66
|
+
const hits = matchFailures(report, {
|
|
67
|
+
rows: [{ id: BAD_ID, name: BAD_NAME, disabled: false, file: patchPath }],
|
|
68
|
+
})
|
|
69
|
+
if (hits.length) {
|
|
70
|
+
log(t('verifyPassed'))
|
|
71
|
+
return true
|
|
72
|
+
}
|
|
73
|
+
log(t('verifyFailNoHit', { version: rollbackVersion ?? '' }))
|
|
74
|
+
return false
|
|
75
|
+
} finally {
|
|
76
|
+
rmSync(home, { recursive: true, force: true })
|
|
77
|
+
}
|
|
78
|
+
}
|