dsh-ris-yds 1.0.0 → 1.1.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.md +20 -1
- package/lib/cli.js +158 -31
- package/lib/ide-proxy.js +296 -72
- package/lib/ide.js +31 -5
- package/lib/proc.js +270 -0
- package/lib/risd-peer.js +353 -0
- package/lib/rules.js +193 -0
- package/lib/socks.js +51 -32
- package/lib/yms.js +59 -25
- package/package.json +1 -1
- package/test/cli-stop.test.mjs +40 -0
- package/test/hints.test.mjs +13 -0
- package/test/ide-config.test.mjs +4 -0
- package/test/ide-proxy.test.mjs +80 -18
- package/test/lease-release.test.mjs +76 -0
- package/test/pidfile.test.mjs +105 -0
- package/test/proc.test.mjs +172 -0
- package/test/proxy-standalone.test.mjs +3 -0
- package/test/risd-leases.test.mjs +111 -0
- package/test/risd-tier.test.mjs +361 -0
- package/test/rules-e2e.test.mjs +122 -0
- package/test/rules.test.mjs +154 -0
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ YonBIP YDS 客开后端的本地运行时,做成 **DSH 插件 + CLI** 双形
|
|
|
21
21
|
|
|
22
22
|
- **本地运行**:构建、启动、停止、查看状态与实时日志;启动失败自动回收,不留下半启动的进程。
|
|
23
23
|
- **平台配置**:从平台拉取配置并适配到本地运行时;支持离线缓存,网络不可达时也能启动。
|
|
24
|
-
- **内网访问**:数据库 / 缓存 /
|
|
24
|
+
- **内网访问**:数据库 / 缓存 / 内网服务可经代理或直连访问;本机其它客户端也能共用同一条隧道。dsh-ris-ide 的代理 daemon(risd)在跑时自动共用它的引擎,`yds stop` 只撤自己的规则、不打断别人
|
|
25
25
|
(需要系统权限的可选能力)。
|
|
26
26
|
- **平台凭据**:生成调用平台 API 的 YYCToken,新环境无需额外安装。
|
|
27
27
|
- **前端调试**:让远端页面加载本地前端资源,三种接入方式(浏览器扩展规则 / 请求改路 / URL 参数)。
|
|
@@ -94,6 +94,25 @@ yds stop
|
|
|
94
94
|
| 本地运行时配置 | 改写成 `127.0.0.1` 映射地址 | 平台地址原样保留 |
|
|
95
95
|
| git 代理 | 自动按克隆地址主机写入 | 不写 |
|
|
96
96
|
|
|
97
|
+
### 隧道从哪来(自动选择,无需配置)
|
|
98
|
+
|
|
99
|
+
1. **dsh-ris-ide 的 risd daemon 在跑**:规则、租约与系统层拦截交给它(写规则 + 登记租约),
|
|
100
|
+
daemon 的引擎不另起;但 YMS 配置被改写成 `127.0.0.1:<dbForwardPort>/<redisForwardPort>`,
|
|
101
|
+
而 daemon 引擎的 127.0.0.1 映射只做同端口映射,覆盖不到这两个端口,所以**内置 SOCKS 转发器
|
|
102
|
+
仍会作为兜底 127.0.0.1 映射同时启动**(代理模式下是两个进程:daemon 引擎 + 内置转发器;
|
|
103
|
+
直连模式下都不起);
|
|
104
|
+
2. 装了 dsh-ris-ide 但 daemon 没跑:借用它已有的代理引擎(127.0.0.1 映射由它按规则提供);
|
|
105
|
+
3. 都没有:用本插件内置的 SOCKS 转发器(零依赖,不需要 dsh-ris-ide)。
|
|
106
|
+
|
|
107
|
+
规则表是**全局一张表**(面板/文件只有一份,保存即整表覆盖,不区分 workspace)。yds 每次启动注入自己
|
|
108
|
+
那一段时是「保留非 yds 行 + 用本次的 yds 行整体替换旧的 yds 行」,所以 `yds stop` 会把表里**全部** yds 行
|
|
109
|
+
撤掉(同一台机器上的多个项目共用这张表,没有交叉配置);非 yds 行(你自己加的规则、其它工具注入的)不受影响。
|
|
110
|
+
非 Windows(含 WSL)除了 `java.exe`/`git.exe` 还会注入本地侧 `java`/`git` 两条规则(`side` 标明侧别),
|
|
111
|
+
WSL 里的 JVM/git 才会被拦截。`yds stop` **不会主动停掉机器级引擎**(IDE 可能还在用)。
|
|
112
|
+
输出里的「已从共享规则表移除 N 条规则/M 个 host」是本次真正减掉的部分,减掉 0 时会明确说没改动规则表。
|
|
113
|
+
注意 `yds stop` 释放的是本项目的租约:如果 daemon 的生命周期配置是「无租约即拆除代理」,而 yds 恰好是
|
|
114
|
+
最后一个租约持有者,daemon 会在约 300ms 后自己关闭隧道(这不是 yds 去停它,进程日志里会打出来)。
|
|
115
|
+
|
|
97
116
|
同一台机器可以混合:dsh 走代理、外部 IDE 直连(`.env` 里设 `ide.launch=direct`):
|
|
98
117
|
|
|
99
118
|
```bash
|
package/lib/cli.js
CHANGED
|
@@ -12,7 +12,9 @@ import { startForwarder, stopForwarder, runForwarder, isForwarderUp, probeSocksC
|
|
|
12
12
|
import {
|
|
13
13
|
ensureBorrowedProxy, syncBorrowedRules, stopBorrowedProxy, borrowedProxyStatus, applyGitProxy, loadIdeProxy,
|
|
14
14
|
loadIdeRedirector, syncRedirectorProfile, startBorrowedRedirector, stopBorrowedSideEffects,
|
|
15
|
+
holdBorrowedLease, releaseBorrowedLease, dropBorrowedLease,
|
|
15
16
|
} from './ide-proxy.js'
|
|
17
|
+
import { probeRisd } from './risd-peer.js'
|
|
16
18
|
import { buildYmsHome, ymsHomeIsBuilt, ensureMavenSettings, findBootstrapDir } from './build.js'
|
|
17
19
|
import { startDetached, startForeground, stopYms, readRunning, status, rdbgaValue, tailFile } from './yms.js'
|
|
18
20
|
import { classifyRdbgaCookies, describeRdbgaCookies, hostVariants, isPlausibleRdbga, probeDevtoolsPorts, rdbgaFromRunningJvm, readRdbgaCookies, repairRdbgaCookie, workbenchTargets, DEVTOOLS_PORTS } from './browser.js'
|
|
@@ -252,6 +254,7 @@ export async function run(argv, { cwd = process.cwd() } = {}) {
|
|
|
252
254
|
+ `yms-home: ${ymsHomeIsBuilt(cfg) ? '已构建' : '未构建'}\n`
|
|
253
255
|
+ `${describeYoncode(detectYoncode(cfg))}\n`
|
|
254
256
|
+ (st !== null ? `YMS: ${st.process ? `pid ${st.process.pid}` : '未运行'}${st.ready !== null ? `, /_yms/ready=${st.ready}` : ''}\n` : '')
|
|
257
|
+
+ ((st === null || !st.process) ? '提示: 运行 yds start 可拉起本地运行时(YMS);yds start 会写代理规则与转发器\n' : '')
|
|
255
258
|
+ (problems.length > 0 ? `\n问题:\n- ${problems.join('\n- ')}` : '\n无配置问题'))
|
|
256
259
|
}
|
|
257
260
|
case 'config': {
|
|
@@ -319,6 +322,10 @@ export async function run(argv, { cwd = process.cwd() } = {}) {
|
|
|
319
322
|
// dsh-ris-ide run-with-log.sh capture, so mvn/progress is trackable
|
|
320
323
|
// via ris_dev_logs.
|
|
321
324
|
const log = (t) => process.stdout.write(`[yds] ${t}\n`)
|
|
325
|
+
// Hold a lease BEFORE the tunnel comes up: prepareStart may start the shared
|
|
326
|
+
// risd engine, and a 3 s lease sweep would tear it down in the window before
|
|
327
|
+
// the YMS pid exists (review D-P1-2). The CLI pid stands in until then.
|
|
328
|
+
const earlyLease = await holdBorrowedLease(cfg, { ownerPid: process.pid, log })
|
|
322
329
|
const prep = await prepareStart(cfg, { log })
|
|
323
330
|
if (prep !== true) {
|
|
324
331
|
if (foreground) process.stderr.write(`[yds] 启动中止: ${prep}\n`)
|
|
@@ -332,6 +339,8 @@ export async function run(argv, { cwd = process.cwd() } = {}) {
|
|
|
332
339
|
log(`前台启动 YMS(Liberty,日志同时输出到这里)`)
|
|
333
340
|
log(`rdbga 回调: ${cfg.module},${cfg.context}@${cfg.serverBindAddress}:${cfg.serverPort} (base64 ${rdbgaValue(cfg)})`)
|
|
334
341
|
log(browserCookieHint(cfg, rdbgaValue(cfg)))
|
|
342
|
+
// Foreground CLI lives exactly as long as the JVM: it is the lease owner.
|
|
343
|
+
await holdBorrowedLease(cfg, { ownerPid: process.pid, log })
|
|
335
344
|
const child = startForeground(cfg, { jvmArgs })
|
|
336
345
|
return new Promise((resolve_, reject_) => {
|
|
337
346
|
child.once('error', reject_)
|
|
@@ -345,6 +354,23 @@ export async function run(argv, { cwd = process.cwd() } = {}) {
|
|
|
345
354
|
await rollbackForwarder(cfg)
|
|
346
355
|
throw e
|
|
347
356
|
}
|
|
357
|
+
if (result.unknown === true) {
|
|
358
|
+
// Do NOT start a second YMS and do NOT kill blindly: the pidfile owner could
|
|
359
|
+
// not be identified (PowerShell/EPERM/timeout), which used to be conflated with
|
|
360
|
+
// "not our process" (review D-P1-6).
|
|
361
|
+
await rollbackForwarder(cfg)
|
|
362
|
+
if (earlyLease.ok === true) await dropBorrowedLease(earlyLease.id, { log })
|
|
363
|
+
return fail(`无法确认 YMS 是否在运行:pidfile 指向 pid ${result.pid},但${result.reason}。`
|
|
364
|
+
+ `\n为避免重复启动或误杀,已中止;请手动确认该进程后删除 ${join(cfg.stateDir, 'yms.pid')}`)
|
|
365
|
+
}
|
|
366
|
+
// The YMS process outlives this CLI, so it owns the risd lease: the engine
|
|
367
|
+
// (and its 127.0.0.1 shadows) must stay up while YMS runs.
|
|
368
|
+
if (typeof result.pid === 'number' && result.pid > 0) {
|
|
369
|
+
await holdBorrowedLease(cfg, { ownerPid: result.pid, log })
|
|
370
|
+
// The YMS pid now owns the lease; the CLI's stand-in would otherwise keep
|
|
371
|
+
// the engine alive for a process that has exited.
|
|
372
|
+
if (earlyLease.ok === true) await dropBorrowedLease(earlyLease.id, { log })
|
|
373
|
+
}
|
|
348
374
|
if (result.alreadyRunning === true) {
|
|
349
375
|
const healed = await healBrowserCookie(cfg, { log })
|
|
350
376
|
const healNote = healed.ok === true ? `浏览器 rdbga 已校准(清理 ${healed.removed} 个)` : `浏览器 rdbga 未校准:${healed.reason}`
|
|
@@ -360,22 +386,45 @@ export async function run(argv, { cwd = process.cwd() } = {}) {
|
|
|
360
386
|
+ browserCookieHint(cfg, rdbgaValue(cfg)))
|
|
361
387
|
}
|
|
362
388
|
case 'stop': {
|
|
389
|
+
const log = (line) => process.stdout.write(`[yds] ${line}\n`)
|
|
390
|
+
const force = flags['--force'] === true
|
|
363
391
|
const yms = await stopYms(cfg)
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
}
|
|
392
|
+
// A stop must not pull the tunnel out from under a YMS that is still running:
|
|
393
|
+
// when the pidfile proves a process exists but it could not be stopped (or its
|
|
394
|
+
// identity is `unknown`), abort BEFORE touching rules/forwarder/layers
|
|
395
|
+
// (review D-P1-2). `--force` is the explicit override.
|
|
396
|
+
const abort = stopAbortReason(yms, { force })
|
|
397
|
+
if (abort !== '') return fail(abort)
|
|
398
|
+
if (yms.stopped !== true && force) log('--force:未能确认 YMS 已停止,仍继续撤掉隧道规则')
|
|
399
|
+
// `--keep-socks` keeps only the bundled forwarder. It cannot keep the engine:
|
|
400
|
+
// the YMS pid that owned the risd lease is already gone, so the daemon's 3 s
|
|
401
|
+
// sweep would reap the lease and settle the tunnel anyway (review D-P2-7).
|
|
402
|
+
// Rules are withdrawn and the machine layers settle in every case.
|
|
403
|
+
const keepSocks = flags['--keep-socks'] === true
|
|
404
|
+
// Release our daemon lease before withdrawing the rules: with the lease still
|
|
405
|
+
// held the daemon would keep the engine for a dead YMS pid.
|
|
406
|
+
await releaseBorrowedLease(cfg, { log })
|
|
407
|
+
const borrowed = await stopBorrowedProxy(cfg, { log })
|
|
408
|
+
const own = keepSocks ? { stopped: false, reason: '保留(--keep-socks)' } : await stopForwarder(cfg)
|
|
409
|
+
const stopped = [borrowed, own].filter((r) => r && r.stopped === true)
|
|
410
|
+
// Two tunnels can exist (a daemon engine the daemon keeps, plus our own
|
|
411
|
+
// forwarder): report them separately instead of letting the engine line hide
|
|
412
|
+
// whether the forwarder was kept or stopped (review D-P2-7).
|
|
413
|
+
const engineNote = borrowed.engine === 'kept'
|
|
414
|
+
? `引擎保留(机器共享;${withdrawnText(borrowed.withdrawn)};daemon 若配置「无租约即拆除代理」会随后关闭)`
|
|
415
|
+
: (stopped.length > 0 ? `已停止 pid=${stopped.map((r) => r.pid).filter(Boolean).join(',')}` : '未运行')
|
|
416
|
+
const forwarderNote = own.stopped === true
|
|
417
|
+
? `内置 socks 转发器已停止(pid ${own.pid})`
|
|
418
|
+
: (keepSocks ? '内置 socks 转发器:保留(--keep-socks)' : `内置 socks 转发器:${own.reason ?? '未运行'}`)
|
|
419
|
+
const fwd = { stopped: false, engine: borrowed.engine, reason: `${engineNote};${forwarderNote}` }
|
|
420
|
+
// Machine-scoped layers that went up with the tunnel come down with it. In the
|
|
421
|
+
// risd tier the daemon already reloaded the bridge while saving the table, so
|
|
422
|
+
// pass its note instead of reloading the bridge locally a second time
|
|
423
|
+
// (review D-P2-10).
|
|
424
|
+
const side = await stopBorrowedSideEffects(cfg, {
|
|
425
|
+
log,
|
|
426
|
+
daemonReload: borrowed.mode === 'risd' ? (borrowed.reloadNote ?? '') : null,
|
|
427
|
+
})
|
|
379
428
|
const bridgeLine = side.redirector.stopped === true
|
|
380
429
|
? `已停止 pid=${side.redirector.pid}`
|
|
381
430
|
: (side.redirector.reason ?? '未运行')
|
|
@@ -431,11 +480,21 @@ export async function run(argv, { cwd = process.cwd() } = {}) {
|
|
|
431
480
|
let fwd = '不适用'
|
|
432
481
|
if (cfg.socks !== null) {
|
|
433
482
|
const borrowed = await borrowedProxyStatus(cfg)
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
483
|
+
// The engine has an owner, and the label must say which one: a risd daemon
|
|
484
|
+
// engine is not the borrowed dsh-ris-ide engine (review D-P2-14).
|
|
485
|
+
const owner = borrowed.mode === 'risd' ? 'risd daemon 引擎' : '借用 dsh-ris-ide 引擎'
|
|
486
|
+
if (borrowed.available === true) {
|
|
487
|
+
fwd = borrowed.running
|
|
488
|
+
? `运行中(${owner} pid ${borrowed.pid ?? '?'})`
|
|
489
|
+
: `未运行(${owner}可用,yds start 会拉起)`
|
|
490
|
+
} else {
|
|
491
|
+
fwd = (await isForwarderUp(cfg)) ? '运行中(内置转发器)' : `未运行(${borrowed.error ?? '代理引擎不可用'})`
|
|
492
|
+
}
|
|
437
493
|
}
|
|
438
|
-
|
|
494
|
+
const processLine = st.process
|
|
495
|
+
? `pid ${st.process.pid}${st.processUnknown === true ? `(身份未确认:${st.process.reason ?? '探测失败'})` : ''}`
|
|
496
|
+
: '未运行'
|
|
497
|
+
return text(`YMS 进程: ${processLine}\n端口 ${cfg.ymsPort}: ${st.port ? '监听中' : '未监听'}\n/_yms/ready: ${st.ready ?? '不可达'}\nyms-home: ${st.ymsHomeBuilt ? '已构建' : '未构建'}\nconfig: ${st.configPresent ? '已就绪' : '缺失'}\nsocks 转发器: ${fwd}` + (st.process ? '' : '\n提示: 运行 yds start 可拉起本地运行时(YMS);未构建时先 yds build'))
|
|
439
498
|
}
|
|
440
499
|
case 'logs': {
|
|
441
500
|
const lines = Number(flags['--lines'] ?? 80) || 80
|
|
@@ -552,14 +611,17 @@ export async function run(argv, { cwd = process.cwd() } = {}) {
|
|
|
552
611
|
if (proxyArgProblem !== '') return fail(`${proxyArgProblem}\n${proxyUsage}`)
|
|
553
612
|
const loaded = await loadIdeProxy(cfg)
|
|
554
613
|
cfg.__ideProxy = loaded
|
|
614
|
+
// Tiers: a running risd daemon is enough (it owns the engine even when this
|
|
615
|
+
// process cannot locate the dsh-ris-ide module), then the borrowed engine of
|
|
616
|
+
// an installed dsh-ris-ide, then the bundled SOCKS forwarder as last resort.
|
|
617
|
+
const risd = await probeRisd()
|
|
555
618
|
// dsh-ris-ide is optional: without it the bundled SOCKS forwarder is the
|
|
556
619
|
// tunnel (the same fallback `yds start` uses in prepareStart). Only the
|
|
557
|
-
// actions that genuinely need
|
|
558
|
-
|
|
559
|
-
const engineMissing = loaded.available !== true
|
|
620
|
+
// actions that genuinely need an engine (rules, the panel) report absence.
|
|
621
|
+
const engineMissing = loaded.available !== true && risd.usable !== true
|
|
560
622
|
const engineNote = engineMissing
|
|
561
623
|
? `代理引擎不可用:${loaded.error}\n(未安装 dsh-ris-ide 时 yds 使用内置 socks 转发器;也可用 .env 的 proxy.engine=self 强制)`
|
|
562
|
-
: ''
|
|
624
|
+
: (risd.usable === true ? `代理引擎:risd daemon(pid ${risd.state?.pid ?? '?'},端口 ${risd.state?.port ?? '?'})` : '')
|
|
563
625
|
if (engineMissing && action === 'status') {
|
|
564
626
|
const targets = cfg.socks === null ? [] : forwarderTargets(cfg)
|
|
565
627
|
const up = cfg.socks !== null && await isForwarderUp(cfg)
|
|
@@ -570,7 +632,10 @@ export async function run(argv, { cwd = process.cwd() } = {}) {
|
|
|
570
632
|
if (action === 'status') {
|
|
571
633
|
const st = await borrowedProxyStatus(cfg)
|
|
572
634
|
const own = st.rules.filter((rule) => rule.source === 'yds')
|
|
573
|
-
|
|
635
|
+
// `loaded.path` is empty when this process cannot load dsh-ris-ide, which is
|
|
636
|
+
// exactly the risd-only case where the engine still has a name: risd:<port>
|
|
637
|
+
// (review D-P2-1).
|
|
638
|
+
return text(`引擎: ${st.path || loaded.path || '(未知)'}\n运行: ${st.running ? `是(pid ${st.pid ?? '?'})` : '否'}\n监听端口: ${st.listenPort ?? '(未知)'}${Array.isArray(st.listenPorts) && st.listenPorts.length > 0 ? `(引擎监听 ${st.listenPorts.join(', ')})` : ''}\n`
|
|
574
639
|
+ `yds 规则: ${own.length === 0 ? '(尚未生成,yds start 时自动写)' : own.map((rule) => `${rule.hosts}:${rule.ports}${rule.shadow ? ' [127.0.0.1 映射]' : ''}`).join(', ')}\n`
|
|
575
640
|
+ `其它规则: ${st.rules.length - own.length} 条(用户/IDE 自己的)`)
|
|
576
641
|
}
|
|
@@ -609,12 +674,33 @@ export async function run(argv, { cwd = process.cwd() } = {}) {
|
|
|
609
674
|
const applied = applyLaunchConfig(cfg)
|
|
610
675
|
launchNote = `\nlaunch.json: ${applied.action}${applied.changed ? '(已写入 java 代理参数,YonCode/VS Code F5 启动的 java 也会走 SOCKS)' : ''}`
|
|
611
676
|
}
|
|
612
|
-
|
|
677
|
+
// What the engine binds is not the same thing as the 127.0.0.1 mappings YMS
|
|
678
|
+
// needs: the daemon's shadows are same-port, while the adapted config points at
|
|
679
|
+
// dbForwardPort/redisForwardPort and is served by the bundled forwarder. So the
|
|
680
|
+
// message names the engine's listeners, and `proxy start` brings that forwarder
|
|
681
|
+
// up exactly like `yds start` does (review D-P2-4).
|
|
682
|
+
const listenPorts = Array.isArray(result.listenPorts) ? result.listenPorts : (result.shadowPorts ?? [])
|
|
683
|
+
const notes = [`代理引擎已就绪(pid ${result.pid ?? '?'}):引擎监听 ${listenPorts.length} 个端口${listenPorts.length > 0 ? `(${listenPorts.join(', ')})` : ''}`]
|
|
684
|
+
if (result.mode === 'risd' && cfg.socks !== null) {
|
|
685
|
+
if (await isForwarderUp(cfg)) {
|
|
686
|
+
notes.push('内置 socks 转发器已在运行(127.0.0.1 兜底映射)')
|
|
687
|
+
} else {
|
|
688
|
+
try {
|
|
689
|
+
const started = await startForwarder(cfg, targets)
|
|
690
|
+
notes.push(started.started === true
|
|
691
|
+
? `内置 socks 转发器已启动(pid ${started.pid}):${started.targets.join(', ')}`
|
|
692
|
+
: `内置 socks 转发器未启动:${started.reason ?? '(未知原因)'}`)
|
|
693
|
+
} catch (e) {
|
|
694
|
+
notes.push(`内置 socks 转发器未启动:${e.message}`)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return text(`${notes.join('\n')}${launchNote}`)
|
|
613
699
|
}
|
|
614
700
|
if (action === 'stop') {
|
|
615
701
|
const r = engineMissing
|
|
616
702
|
? { ok: false, error: loaded.error }
|
|
617
|
-
: await stopBorrowedProxy(cfg)
|
|
703
|
+
: await stopBorrowedProxy(cfg, { log: (line) => process.stdout.write(`[yds] ${line}\n`) })
|
|
618
704
|
// The bundled forwarder is a tunnel too: stopping the proxy has to take
|
|
619
705
|
// it down whether or not dsh-ris-ide is installed.
|
|
620
706
|
const fwd = cfg.socks === null
|
|
@@ -626,9 +712,13 @@ export async function run(argv, { cwd = process.cwd() } = {}) {
|
|
|
626
712
|
const stopNote = stripped.changed
|
|
627
713
|
? '\nlaunch.json: 已移除 java 代理参数(-DsocksProxyHost/-DsocksProxyPort)'
|
|
628
714
|
: ''
|
|
629
|
-
const engineLine = r.
|
|
630
|
-
? '
|
|
631
|
-
|
|
715
|
+
const engineLine = r.engine === 'kept'
|
|
716
|
+
? (r.mode === 'risd'
|
|
717
|
+
? `risd: 已释放租约,${withdrawnText(r.withdrawn)}(引擎保留;若 daemon 生命周期配置为「无租约即拆除代理」,会在约 300ms 后关闭)`
|
|
718
|
+
: `代理引擎保留(机器共享,IDE 可继续用);${withdrawnText(r.withdrawn)}`)
|
|
719
|
+
: r.ok === true || r.stopped === true
|
|
720
|
+
? '代理引擎已停止'
|
|
721
|
+
: engineMissing ? `代理引擎未启用(${loaded.error})` : `代理引擎未停止:${r.error ?? r.reason ?? '未运行'}`
|
|
632
722
|
const fwdLine = fwd.stopped === true ? `\n内置转发器已停止(pid ${fwd.pid})` : ''
|
|
633
723
|
return text(engineLine + fwdLine + stopNote)
|
|
634
724
|
}
|
|
@@ -773,7 +863,7 @@ function splitJvmArgs(raw) {
|
|
|
773
863
|
return out
|
|
774
864
|
}
|
|
775
865
|
|
|
776
|
-
const BOOLEAN_FLAGS = new Set(['--foreground', '--offline', '--keep-socks', '--no-socks', '--dry-run'])
|
|
866
|
+
const BOOLEAN_FLAGS = new Set(['--foreground', '--offline', '--keep-socks', '--no-socks', '--dry-run', '--force'])
|
|
777
867
|
|
|
778
868
|
function asFlagValue(v) {
|
|
779
869
|
if (v === 'true' || v === '1' || v === 'yes') return true
|
|
@@ -960,7 +1050,7 @@ function usage() {
|
|
|
960
1050
|
' build maven 部署 yms-home(首次含 kernel 下载)+ 拉配置',
|
|
961
1051
|
' start [--jvm-args "…"] 后台启动 YMS(SOCKS 自动转发; 幂等)',
|
|
962
1052
|
' start --foreground 前台启动(dsh-ris-ide startCommand 用)',
|
|
963
|
-
' stop [--keep-socks]
|
|
1053
|
+
' stop [--keep-socks] [--force] 停 YMS(默认连转发器一起停;--force 表示无法确认 YMS 已停也继续撤规则)',
|
|
964
1054
|
' status 进程/端口/ready/配置状态',
|
|
965
1055
|
' logs [--lines N] tail 控制台与应用日志',
|
|
966
1056
|
' ide [apply|remove|check] 维护 .dsh-ris-ide/config.json 的 yds 程序条目(+ .vscode/launch.json)',
|
|
@@ -989,6 +1079,43 @@ function fail(value) {
|
|
|
989
1079
|
return { text: value, exitCode: 1 }
|
|
990
1080
|
}
|
|
991
1081
|
|
|
1082
|
+
/**
|
|
1083
|
+
* Why `yds stop` must abort after stopYms, or '' when it may continue. A YMS with a
|
|
1084
|
+
* live pid that could not be stopped (or whose identity is `unknown`) must keep its
|
|
1085
|
+
* tunnel: withdrawing rules/forwarder/layers under it is the D-P1-2 bug. Exported so
|
|
1086
|
+
* the guard is unit-testable without spawning a real YMS.
|
|
1087
|
+
* @param {{stopped?: boolean, pid?: number|string, reason?: string}} yms
|
|
1088
|
+
* @param {{force?: boolean}} [options]
|
|
1089
|
+
* @returns {string} '' to continue, otherwise the abort message body
|
|
1090
|
+
*/
|
|
1091
|
+
export function stopAbortReason(yms, { force = false } = {}) {
|
|
1092
|
+
if (force === true) return ''
|
|
1093
|
+
if (yms?.stopped === true) return ''
|
|
1094
|
+
const pid = Number(yms?.pid)
|
|
1095
|
+
if (!Number.isInteger(pid) || pid <= 0) return ''
|
|
1096
|
+
return `YMS 可能仍在运行(pid ${pid}):${yms?.reason ?? '未能确认已停止'}\n`
|
|
1097
|
+
+ '为避免把仍在运行的服务抽掉隧道,已中止:yds 规则、内置转发器、系统层拦截都未改动。\n'
|
|
1098
|
+
+ '请先确认该进程(ps / 任务管理器)或修复权限后重试;确需强行撤掉规则时加 --force。'
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* How much of the yds rule set a stop actually removed. `yds stop` used to print
|
|
1103
|
+
* "已撤掉 yds 规则" unconditionally, which was a lie both when it removed nothing and
|
|
1104
|
+
* when it silently deleted another workspace's hosts (review D-P0-1). The table is one
|
|
1105
|
+
* global list and a stop removes every yds row, so the text reports exactly that.
|
|
1106
|
+
*/
|
|
1107
|
+
export function withdrawnText(withdrawn) {
|
|
1108
|
+
const rules = Number(withdrawn?.rules ?? 0)
|
|
1109
|
+
const hosts = Number(withdrawn?.hosts ?? 0)
|
|
1110
|
+
if (!Number.isFinite(rules) || !Number.isFinite(hosts) || (rules <= 0 && hosts <= 0)) {
|
|
1111
|
+
return '共享规则表里没有 yds 规则(未改动)'
|
|
1112
|
+
}
|
|
1113
|
+
const parts = []
|
|
1114
|
+
if (rules > 0) parts.push(`${rules} 条规则`)
|
|
1115
|
+
if (hosts > 0) parts.push(`${hosts} 个 host`)
|
|
1116
|
+
return `已从共享规则表移除 yds 的 ${parts.join('、')}(全局一张表,不区分 workspace)`
|
|
1117
|
+
}
|
|
1118
|
+
|
|
992
1119
|
/**
|
|
993
1120
|
* Reject stray positionals/options instead of silently ignoring them: `yds browser fixx`
|
|
994
1121
|
* used to run nothing and still report success.
|