@twinsearth/dsh-2xray 0.1.0 → 0.1.1

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,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1 — 2026-10-29
4
+
5
+ ### 修复
6
+
7
+ - 插件启用时报 `cannot get property "subprocess" without inject`:Cordis 会拦截未声明 `inject` 的服务属性读取。改为 `ctx.get('subprocess')` 反射查询,并把 launcher 改为首次 spawn 时惰性解析;服务晚挂载或缺失时分别使用服务 / `child_process` 兜底,不再阻断启用。
8
+
3
9
  ## 0.1.0 — 2026-10-29
4
10
 
5
11
  首个发布版本(npm:`@twinsearth/dsh-2xray`)。
package/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { normalizeConfig } from './lib/config.js'
2
- import { createLauncher } from './lib/process.js'
2
+ import { createLazyLauncher } from './lib/process.js'
3
3
  import { RouteManager } from './lib/route.js'
4
4
  import { Supervisor } from './lib/supervisor.js'
5
5
  import { createLogger } from './lib/log.js'
@@ -49,7 +49,7 @@ export function apply(ctx, rawConfig) {
49
49
  logger.info('使用旧字段 publicKey 配置 REALITY;xray 会同时读取 password,两者都写可兼容新旧版本')
50
50
  } }
51
51
 
52
- const launcher = createLauncher(ctx, { logger })
52
+ const launcher = createLazyLauncher(ctx, { logger })
53
53
  const route = new RouteManager({ logger })
54
54
  const control =
55
55
  config.control.enabled
package/lib/process.js CHANGED
@@ -3,6 +3,28 @@ import { setTimeout as delay } from 'node:timers/promises'
3
3
 
4
4
  const COLLECT_BYTES = 64 * 1024
5
5
 
6
+ /**
7
+ * Resolve the subprocess service without tripping Cordis' inject guard.
8
+ *
9
+ * Reading `ctx.subprocess` directly throws
10
+ * `cannot get property "subprocess" without inject` on a fiber that did not
11
+ * declare it; `ctx.get(name)` is the reflective lookup and returns undefined
12
+ * instead. The service stays optional because a profile without
13
+ * `dsh-subprocess-local` still gets the `child_process` fallback.
14
+ */
15
+ function resolveSubprocess(ctx) {
16
+ if (!ctx) return undefined
17
+ try {
18
+ if (typeof ctx.get === 'function') {
19
+ const service = ctx.get('subprocess')
20
+ if (service) return service
21
+ }
22
+ } catch {
23
+ // inactive or guarded context: fall through to the fallback launcher
24
+ }
25
+ return undefined
26
+ }
27
+
6
28
  /**
7
29
  * A launcher hides whether the child runs through `ctx.subprocess` (Windows Job
8
30
  * Object, process-tree containment, DSH's environment scrub) or through a bare
@@ -16,7 +38,7 @@ const COLLECT_BYTES = 64 * 1024
16
38
  * pid?: number
17
39
  */
18
40
  export function createLauncher(ctx, { logger } = {}) {
19
- const service = ctx?.subprocess
41
+ const service = resolveSubprocess(ctx)
20
42
  if (service && typeof service.spawn === 'function' && typeof service.resolveExecutable === 'function') {
21
43
  return createServiceLauncher(service)
22
44
  }
@@ -24,6 +46,27 @@ export function createLauncher(ctx, { logger } = {}) {
24
46
  return createNodeLauncher()
25
47
  }
26
48
 
49
+ /**
50
+ * Resolve the launcher lazily at first spawn instead of at activation: the
51
+ * subprocess provider may mount after this plugin's fiber, and a strict
52
+ * `inject` dependency would stop the plugin from activating at all.
53
+ */
54
+ export function createLazyLauncher(ctx, options) {
55
+ let inner
56
+ const resolve = () => (inner ??= createLauncher(ctx, options))
57
+ return {
58
+ get kind() {
59
+ return resolve().kind
60
+ },
61
+ resolveExecutable(command, env, signal) {
62
+ return resolve().resolveExecutable(command, env, signal)
63
+ },
64
+ spawn(spec) {
65
+ return resolve().spawn(spec)
66
+ },
67
+ }
68
+ }
69
+
27
70
  function createServiceLauncher(service) {
28
71
  return {
29
72
  kind: 'ctx.subprocess',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twinsearth/dsh-2xray",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
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",
@@ -18,7 +18,7 @@ const check = (name, ok, detail = '') => checks.push({ name, ok, detail })
18
18
  const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf8'))
19
19
 
20
20
  check('name is @twinsearth/dsh-2xray', pkg.name === '@twinsearth/dsh-2xray', pkg.name)
21
- check('version is 0.1.0', pkg.version === '0.1.0', pkg.version)
21
+ check('version is semver', /^\d+\.\d+\.\d+$/.test(pkg.version), pkg.version)
22
22
  check('publishable', pkg.private !== true, `private=${pkg.private}`)
23
23
  check('license MIT', pkg.license === 'MIT', String(pkg.license))
24
24
  check('type module', pkg.type === 'module', String(pkg.type))
@@ -0,0 +1,49 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { createLauncher, createLazyLauncher } from '../lib/process.js'
4
+
5
+ const logger = { warn() {} }
6
+
7
+ test('a context that guards service properties never throws at activation', () => {
8
+ const guarded = { get: () => undefined }
9
+ Object.defineProperty(guarded, 'subprocess', {
10
+ get() {
11
+ throw new Error('cannot get property "subprocess" without inject')
12
+ },
13
+ })
14
+ assert.doesNotThrow(() => createLauncher(guarded, { logger }))
15
+ assert.equal(createLauncher(guarded, { logger }).kind, 'child_process')
16
+ })
17
+
18
+ test('ctx.get is used when it returns the service', () => {
19
+ const service = { spawn() {}, resolveExecutable() {} }
20
+ const ctx = { get: (name) => (name === 'subprocess' ? service : undefined) }
21
+ assert.equal(createLauncher(ctx, { logger }).kind, 'ctx.subprocess')
22
+ })
23
+
24
+ test('a context without ctx.get falls back to child_process', () => {
25
+ assert.equal(createLauncher({}, { logger }).kind, 'child_process')
26
+ assert.equal(createLauncher(undefined, { logger }).kind, 'child_process')
27
+ })
28
+
29
+ test('the lazy launcher resolves at first use, so a later-mounted service is seen', () => {
30
+ let service
31
+ const ctx = { get: () => service }
32
+ const lazy = createLazyLauncher(ctx, { logger })
33
+ service = {
34
+ spawn() {
35
+ return {
36
+ pid: 7,
37
+ done: Promise.resolve({ exitCode: 0, signal: null }),
38
+ terminate: async () => {},
39
+ waitForExit: async () => {},
40
+ collected: {},
41
+ }
42
+ },
43
+ resolveExecutable() {},
44
+ }
45
+ assert.equal(lazy.kind, 'ctx.subprocess')
46
+ const handle = lazy.spawn({ argv: ['x'], cwd: '.', graceMs: 1, env: {} })
47
+ assert.equal(handle.kind, 'ctx.subprocess')
48
+ assert.equal(handle.pid, 7)
49
+ })