每路入站配一组专属策略组,换线不再改配置文件
原本换一条线要改 config.yaml、跑校验、等内核重载,几秒起步;而换线恰恰是这个工具 最高频的动作,把它放在最重的路径上方向就错了。现在每一路配一个 select 组加 AUTO、 FALLBACK 两个,入站的 proxy 永久指向 select,换线只是调一次内核接口。这样做的前提是 选中不会丢:内核把它写进 cache 文件,重启后由 patchSelectGroup 恢复,而 profile.store-selected 默认为真。 链式代理靠给来源订阅复制一份声明、挂上 override.dialer-proxy 实现,因为这个字段写在 策略组上内核只会打条日志然后忽略。副本是 type: file 读原订阅已经落盘的缓存,而不是 自己再拉一次:自己拉的话订阅地址一不可达副本就是空的,那一路入站会一条线都不剩, 表现是端口还在监听却没有任何出口。 顺带把 shared/outbound-chain.ts 里「只有 Selector 接受手动选择」那条断言删了,它是 错的(内核检查的是 SelectAble 接口,URLTest 和 Fallback 也实现了)。界面仍然只让 Selector 可点,但理由换成了另外两种的手动指定会被它们自己撤销。 完整取舍见工作区决策记录 0007。
This commit is contained in:
@@ -117,6 +117,16 @@ export async function applyConfig(
|
||||
}
|
||||
}
|
||||
|
||||
// 落盘链路有五步各自可能变慢(校验要起一个内核进程、重载要等内核跑完、备份和写盘是磁盘
|
||||
// IO)。出问题时不靠猜,日志里直接给出每步耗时。
|
||||
const clock = { start: Date.now(), mark: Date.now() }
|
||||
const lap = (stage: string) => {
|
||||
const now = Date.now()
|
||||
const ms = now - clock.mark
|
||||
clock.mark = now
|
||||
return `${stage} ${String(ms)}ms`
|
||||
}
|
||||
|
||||
const nextYaml = rewriteYaml(current.yaml, nextData)
|
||||
|
||||
// 内容没变就不要走后面那一串——不必要的备份和重载只会制造噪音。
|
||||
@@ -138,9 +148,11 @@ export async function applyConfig(
|
||||
|
||||
// 先写到临时文件交给内核校验,别碰真文件。
|
||||
const tempPath = join(dirname(env.configPath), `.patchbay-check-${String(Date.now())}.yaml`)
|
||||
let checkTiming = ''
|
||||
try {
|
||||
await writeFile(tempPath, nextYaml, 'utf8')
|
||||
const checked = await validateConfigFile(env, tempPath)
|
||||
checkTiming = lap('内核校验')
|
||||
if (!checked.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -156,6 +168,7 @@ export async function applyConfig(
|
||||
|
||||
// 校验过了才动真文件。备份放在写入之前。
|
||||
const backupPath = await createBackup(env)
|
||||
const backupTiming = lap('备份')
|
||||
try {
|
||||
await writeFile(env.configPath, nextYaml, 'utf8')
|
||||
} catch (error) {
|
||||
@@ -168,7 +181,9 @@ export async function applyConfig(
|
||||
}
|
||||
}
|
||||
|
||||
const writeTiming = lap('写盘')
|
||||
const reloaded = await reloadConfig(env, env.configPath)
|
||||
const reloadTiming = lap('重载')
|
||||
if (!reloaded.ok) {
|
||||
// 文件已经写进去了但内核没接受。把文件还原,再让内核载回原样,
|
||||
// 避免留下「文件是新的、内核跑的是旧的」这种最难排查的状态。
|
||||
@@ -192,6 +207,9 @@ export async function applyConfig(
|
||||
}
|
||||
|
||||
await pruneBackups(env)
|
||||
console.log(
|
||||
`[apply] 共 ${String(Date.now() - clock.start)}ms(${checkTiming},${backupTiming},${writeTiming},${reloadTiming})`,
|
||||
)
|
||||
return {
|
||||
ok: true,
|
||||
snapshot: await readConfig(env),
|
||||
|
||||
+3
-1
@@ -8,7 +8,7 @@ import { readEnv } from './env.ts'
|
||||
import { resolveToken } from './auth.ts'
|
||||
import { createRouter } from './routes.ts'
|
||||
import { readConfig } from './config-store.ts'
|
||||
import { kernelVersion } from './kernel.ts'
|
||||
import { kernelVersion, warmUpKernelBinary } from './kernel.ts'
|
||||
|
||||
const env = readEnv()
|
||||
const { token, source } = await resolveToken(env)
|
||||
@@ -29,6 +29,8 @@ try {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
warmUpKernelBinary(env)
|
||||
|
||||
const kernel = await kernelVersion(env)
|
||||
console.log(
|
||||
kernel.online
|
||||
|
||||
@@ -50,6 +50,24 @@ export async function validateConfigFile(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动时先把内核二进制跑一次,什么都不做。
|
||||
*
|
||||
* `mihomo -t` 的第一次调用要 4 秒,之后 55 毫秒(在目标机器那种规格上实测差距只会更大)。
|
||||
* 差的是把这个几十兆的可执行文件读进系统文件缓存的时间,跟配置内容无关。不预热的话,
|
||||
* 这 4 秒会落在用户按下的第一次保存上,而那正是他最容易以为「这个工具很慢」的时刻。
|
||||
*
|
||||
* 失败不用管:内核二进制不可用这件事,第一次真正校验时会带着完整错误信息报出来。
|
||||
*/
|
||||
export function warmUpKernelBinary(env: Env): void {
|
||||
try {
|
||||
const proc = Bun.spawn([env.mihomoBin, '-v'], { stdout: 'ignore', stderr: 'ignore' })
|
||||
void proc.exited
|
||||
} catch {
|
||||
// 同上
|
||||
}
|
||||
}
|
||||
|
||||
/** 带上认证头访问管理接口。 */
|
||||
async function callApi(env: Env, path: string, init?: RequestInit): Promise<Response> {
|
||||
return fetch(`${env.mihomoApi}${path}`, {
|
||||
|
||||
Reference in New Issue
Block a user