@leiyi-works/dsh-cloud-sync 0.1.6 → 0.1.7

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.
Files changed (2) hide show
  1. package/exec.js +112 -0
  2. package/package.json +2 -1
package/exec.js ADDED
@@ -0,0 +1,112 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { EventEmitter } from 'node:events'
3
+
4
+ /**
5
+ * 命令执行层:优先走 DSH 的 `ctx.subprocess`(可移植层),
6
+ * 拿不到时退回 node:child_process。
7
+ *
8
+ * 为什么需要这一层:手机上(DSHM 鸿蒙外壳)应用沙箱不允许 Host 直接 fork/exec,
9
+ * `spawn('ssh')` 会 ENOENT;但 DSH 的 subprocess 服务可以被平台改派到自带的 guest 环境,
10
+ * 那里 ssh 可用。所以「能不能跑」取决于这里,而不是密钥或配置。
11
+ */
12
+
13
+ let provider = null
14
+
15
+ /** 由插件 apply() 调用,注入 ctx.subprocess(可能为 undefined)。 */
16
+ export function setSubprocessProvider(value) {
17
+ provider = value ?? null
18
+ }
19
+
20
+ export function hasSubprocessProvider() {
21
+ return provider !== null
22
+ }
23
+
24
+ /** 在 provider 的执行世界里解析可执行文件路径;没有 provider 时原样返回。 */
25
+ export async function resolveCommand(command) {
26
+ if (provider === null) return command
27
+ try {
28
+ const resolved = await provider.resolveExecutable(command)
29
+ return typeof resolved === 'string' && resolved !== '' ? resolved : command
30
+ } catch {
31
+ return command
32
+ }
33
+ }
34
+
35
+ /** 伪造一个「启动失败」的 child,行为与 EventEmitter 版子进程一致。 */
36
+ function failedChild(error) {
37
+ const child = new EventEmitter()
38
+ child.stdin = new EventEmitter()
39
+ child.stdin.end = () => {}
40
+ child.stdin.write = () => true
41
+ child.stdout = new EventEmitter()
42
+ child.stderr = new EventEmitter()
43
+ child.kill = () => {}
44
+ setImmediate(() => {
45
+ child.emit('error', error instanceof Error ? error : new Error(String(error)))
46
+ child.emit('close', -1)
47
+ })
48
+ return child
49
+ }
50
+
51
+ /** 把 ctx.subprocess 的 handle 包成和 child_process 一样的接口。 */
52
+ class ManagedChild extends EventEmitter {
53
+ constructor(handle) {
54
+ super()
55
+ this.handle = handle
56
+ this.stdin = handle.stdin ?? null
57
+ this.stdout = handle.stdout ?? null
58
+ this.stderr = handle.stderr ?? null
59
+ this.killed = false
60
+ this.exitCode = null
61
+ handle.done.then(
62
+ (outcome) => {
63
+ this.exitCode = outcome && outcome.exitCode !== undefined ? outcome.exitCode : -1
64
+ this.emit('close', this.exitCode)
65
+ },
66
+ (error) => {
67
+ this.emit('error', error instanceof Error ? error : new Error(String(error)))
68
+ this.emit('close', -1)
69
+ },
70
+ )
71
+ // stdin 写失败不该冒泡成未捕获异常
72
+ if (this.stdin && typeof this.stdin.on === 'function') this.stdin.on('error', () => {})
73
+ }
74
+
75
+ kill() {
76
+ if (this.killed) return
77
+ this.killed = true
78
+ try {
79
+ this.handle.terminate()
80
+ } catch { /* 已经不在了 */ }
81
+ }
82
+ }
83
+
84
+ /**
85
+ * 启动一个受管子进程。
86
+ * @param {string[]} argv 完整命令行(argv[0] 为可执行文件)
87
+ * @param {{ cwd?: string, env?: Record<string,string>, graceMs?: number }} options
88
+ */
89
+ export function spawnManaged(argv, options = {}) {
90
+ const cwd = options.cwd ?? process.cwd()
91
+ const env = options.env ?? process.env
92
+ if (provider !== null) {
93
+ try {
94
+ const handle = provider.spawn({
95
+ argv,
96
+ cwd,
97
+ stdio: { stdin: 'pipe', stdout: 'pipe', stderr: 'pipe' },
98
+ graceMs: options.graceMs ?? 3000,
99
+ env,
100
+ })
101
+ return new ManagedChild(handle)
102
+ } catch (error) {
103
+ return failedChild(error)
104
+ }
105
+ }
106
+ return spawn(argv[0], argv.slice(1), {
107
+ windowsHide: true,
108
+ stdio: ['pipe', 'pipe', 'pipe'],
109
+ cwd,
110
+ env,
111
+ })
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leiyi-works/dsh-cloud-sync",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "title": "云端同步",
6
6
  "description": "云端同步:执行层走 DSH 可移植 subprocess(手机/沙箱也能跑)、ssh 兼容 dropbear、超时与停滞可配置、设备模式、永久错误停止重试;连接信息在 keyrule-secrets/ssh.json。",
@@ -23,6 +23,7 @@
23
23
  "files": [
24
24
  "index.js",
25
25
  "sync.js",
26
+ "exec.js",
26
27
  "client.js",
27
28
  "cordis.patch.yml",
28
29
  "locale/*.json",