@twinsearth/dsh-2xray 0.2.4 → 0.2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.5 — 2026-10-29
4
+
5
+ ### 修复
6
+
7
+ - **binary-missing 自动恢复**:未配置 `binary.path` 时,会按 `V2RAYN_HOME/V2RAYN_PATH`、`%LOCALAPPDATA%`、桌面/下载、Program Files、盘符根目录做有界搜索(只进入名字含 v2ray 的目录,3s 预算),识别 v2rayN 7.x 的 `bin/xray/xray.exe` 并直接借用。
8
+ - 找不到时错误信息仍列出显式候选与 PATH,便于诊断。
9
+
3
10
  ## 0.2.4 — 2026-10-29
4
11
 
5
12
  ### 设置页与导入
package/lib/binary.js CHANGED
@@ -1,4 +1,4 @@
1
- import { accessSync, constants, existsSync } from 'node:fs'
1
+ import { accessSync, constants, existsSync, readdirSync } from 'node:fs'
2
2
  import { delimiter, join } from 'node:path'
3
3
  import { fileURLToPath } from 'node:url'
4
4
 
@@ -54,6 +54,8 @@ export async function resolveBinary({
54
54
  launcher,
55
55
  exists = isExecutable,
56
56
  packageRoot = PACKAGE_ROOT,
57
+ discoveryRoots,
58
+ discoveryOptions = {},
57
59
  }) {
58
60
  const candidates = []
59
61
  const explicit = config.binary.path?.trim()
@@ -90,14 +92,91 @@ export async function resolveBinary({
90
92
  }
91
93
  }
92
94
 
95
+ // v2rayN / v2rayN-core ship xray under their own directory (7.x uses
96
+ // bin/xray/xray.exe). Borrowing it avoids asking the user to copy a binary.
97
+ const discovered = discoverXray({ roots: discoveryRoots, ...discoveryOptions })
98
+ if (discovered.length > 0) {
99
+ return { path: discovered[0], source: 'discovered' }
100
+ }
101
+
93
102
  throw new BinaryError(
94
103
  `找不到 xray-core 可执行文件。已尝试:${candidates.map((candidate) => candidate.path).join(', ') || '(无显式路径)'},以及 PATH 中的 ${pathEntries.join(delimiter)}。` +
95
104
  '请把 xray 放进插件自己的 bin/、数据目录的 bin/,或设置 config.binary.path。',
96
105
  'binary-missing',
97
- candidates.map((candidate) => candidate.path),
106
+ [...candidates.map((candidate) => candidate.path), ...discovered],
98
107
  )
99
108
  }
100
109
 
110
+ /** Common install roots for a borrowed xray binary (Windows-first). */
111
+ export function defaultDiscoveryRoots() {
112
+ const home = process.env.USERPROFILE ?? process.env.HOME ?? ''
113
+ const local = process.env.LOCALAPPDATA ?? ''
114
+ const roots = [
115
+ process.env.V2RAYN_HOME,
116
+ process.env.V2RAYN_PATH,
117
+ local,
118
+ home ? join(home, 'Desktop') : '',
119
+ home ? join(home, 'Downloads') : '',
120
+ 'C:\\Program Files',
121
+ 'C:\\Program Files (x86)',
122
+ 'D:\\',
123
+ 'E:\\',
124
+ 'C:\\',
125
+ ]
126
+ return [...new Set(roots.filter((root) => typeof root === 'string' && root.length > 0))]
127
+ }
128
+
129
+ /**
130
+ * Bounded search for a v2rayN-style xray layout. Directory names must look
131
+ * like v2rayN, so the walk does not crawl unrelated trees; the time budget
132
+ * keeps a failed start from stalling activation.
133
+ */
134
+ export function discoverXray({ roots = defaultDiscoveryRoots(), timeBudgetMs = 3000, max = 5 } = {}) {
135
+ const found = []
136
+ const deadline = Date.now() + timeBudgetMs
137
+ const skipped = /^(windows|programdata|recovery|\$recycle\.bin|system volume information|node_modules|\.git|appdata|temp)$/i
138
+ const layouts = [
139
+ ['bin', 'xray', 'xray.exe'],
140
+ ['bin', 'xray', 'xray'],
141
+ ['bin', 'xray.exe'],
142
+ ['bin', 'xray'],
143
+ ['xray.exe'],
144
+ ['xray'],
145
+ ]
146
+ const check = (dir) => {
147
+ for (const parts of layouts) {
148
+ const candidate = join(dir, ...parts)
149
+ if (existsSync(candidate)) {
150
+ if (!found.includes(candidate)) found.push(candidate)
151
+ return
152
+ }
153
+ }
154
+ }
155
+ const visit = (dir, depth) => {
156
+ if (found.length >= max || Date.now() > deadline) return
157
+ let entries
158
+ try {
159
+ entries = readdirSync(dir, { withFileTypes: true })
160
+ } catch {
161
+ return
162
+ }
163
+ for (const entry of entries) {
164
+ if (found.length >= max || Date.now() > deadline) return
165
+ if (!entry.isDirectory()) continue
166
+ const name = entry.name
167
+ if (skipped.test(name)) continue
168
+ const full = join(dir, name)
169
+ if (/v2ray/i.test(name)) check(full)
170
+ if (depth < 3) visit(full, depth + 1)
171
+ }
172
+ }
173
+ for (const root of roots) {
174
+ if (found.length >= max || Date.now() > deadline) break
175
+ if (existsSync(root)) visit(root, 0)
176
+ }
177
+ return found
178
+ }
179
+
101
180
  /**
102
181
  * Run `xray version` and parse the version line. A failed probe (missing
103
182
  * dynamic library, wrong architecture, antivirus block) is a startup failure,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/dsh-2xray",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "private": false,
5
5
  "description": "DSH-2Xray: supervises xray-core and routes DSH process-wide HTTP(S) traffic through its local inbound, with optional multi-node gRPC control plane",
6
6
  "type": "module",
@@ -1,5 +1,7 @@
1
1
  import test from 'node:test'
2
2
  import assert from 'node:assert/strict'
3
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
4
+ import { tmpdir } from 'node:os'
3
5
  import { join } from 'node:path'
4
6
  import { BinaryError, resolveBinary } from '../lib/binary.js'
5
7
  import { normalizeConfig } from '../lib/config.js'
@@ -89,6 +91,7 @@ test('a missing binary fails with every candidate listed', async () => {
89
91
  packageRoot,
90
92
  exists: () => false,
91
93
  launcher: {},
94
+ discoveryRoots: [],
92
95
  }),
93
96
  (error) => {
94
97
  assert.ok(error instanceof BinaryError)
@@ -101,3 +104,28 @@ test('a missing binary fails with every candidate listed', async () => {
101
104
  process.env.PATH = previousPath
102
105
  }
103
106
  })
107
+
108
+ test('discovers a v2rayN layout when nothing else resolves', async () => {
109
+ const dir = mkdtempSync(join(tmpdir(), 'dsh-xray-discover-'))
110
+ const previousPath = process.env.PATH
111
+ try {
112
+ const layout = join(dir, 'v2rayN-windows-64', 'bin', 'xray')
113
+ mkdirSync(layout, { recursive: true })
114
+ writeFileSync(join(layout, 'xray.exe'), '')
115
+ process.env.PATH = ''
116
+ const found = await resolveBinary({
117
+ config: config(),
118
+ dataDir: join(dir, 'data'),
119
+ packageRoot: join(dir, 'plugin'),
120
+ exists: () => false,
121
+ launcher: {},
122
+ discoveryRoots: [dir],
123
+ discoveryOptions: { timeBudgetMs: 2000 },
124
+ })
125
+ assert.equal(found.source, 'discovered')
126
+ assert.ok(found.path.endsWith('xray.exe'))
127
+ } finally {
128
+ process.env.PATH = previousPath
129
+ rmSync(dir, { recursive: true, force: true })
130
+ }
131
+ })