@wwkit/shared 1.0.32 → 1.0.34
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/package.json +1 -1
- package/src/client-ops.js +225 -0
- package/src/index.js +1 -0
package/package.json
CHANGED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 跨平台客户端操作:打开浏览器 / 查找可执行文件 / 提权预检
|
|
3
|
+
*
|
|
4
|
+
* 封装 Windows / macOS / Linux 的客户端侧系统差异,供 @wwkit 各包复用:
|
|
5
|
+
* - openBrowser 跨平台打开浏览器打开 URL(mac open / win cmd start / linux xdg-open)
|
|
6
|
+
* - whichPath 跨平台查找可执行文件路径(win32 用 where,其余用 which)
|
|
7
|
+
* - isWindowsAdmin Windows 管理员预检(net session;非 Windows 视为无需该权限)
|
|
8
|
+
*
|
|
9
|
+
* 设计原则:纯通用,不依赖任何包的配置/路径/状态目录;门控(如测试不弹浏览器)
|
|
10
|
+
* 由调用方自行判断,不进本模块。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import net from 'node:net'
|
|
16
|
+
import os from 'node:os'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 跨平台打开浏览器打开 URL。
|
|
21
|
+
* 打开失败静默返回 false(如无默认浏览器),不抛异常。
|
|
22
|
+
* @param {string} url
|
|
23
|
+
* @returns {boolean} 是否成功发起打开
|
|
24
|
+
*/
|
|
25
|
+
export function openBrowser(url) {
|
|
26
|
+
try {
|
|
27
|
+
const opts = { detached: true, stdio: 'ignore', windowsHide: true } // windowsHide:避免启动浏览器时 cmd 闪窗
|
|
28
|
+
let child
|
|
29
|
+
if (process.platform === 'win32') {
|
|
30
|
+
child = spawn('cmd', ['/c', 'start', '', url], opts)
|
|
31
|
+
} else if (process.platform === 'darwin') {
|
|
32
|
+
child = spawn('open', [url], opts)
|
|
33
|
+
} else {
|
|
34
|
+
child = spawn('xdg-open', [url], opts)
|
|
35
|
+
}
|
|
36
|
+
child.on('error', () => {}) // 打开失败静默(如无默认浏览器)
|
|
37
|
+
child.unref()
|
|
38
|
+
return true
|
|
39
|
+
} catch {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// which 结果缓存:环境级探测(where/which 查 PATH)在轮询期反复调用,
|
|
45
|
+
// 每 3s 一次 spawnSync 既浪费也不必要;TTL 60s 内直接复用。
|
|
46
|
+
const _whichCache = new Map()
|
|
47
|
+
const _WHICH_TTL_MS = 60_000
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 跨平台查找可执行文件路径(win32 用 where,其余用 which);找不到返回 null。
|
|
51
|
+
* 结果缓存 60s,避免轮询期反复 spawn。
|
|
52
|
+
* @param {string} cmd
|
|
53
|
+
* @returns {string|null}
|
|
54
|
+
*/
|
|
55
|
+
export function whichPath(cmd) {
|
|
56
|
+
const now = Date.now()
|
|
57
|
+
const hit = _whichCache.get(cmd)
|
|
58
|
+
if (hit && now - hit.ts < _WHICH_TTL_MS) {
|
|
59
|
+
return hit.value
|
|
60
|
+
}
|
|
61
|
+
const tool = process.platform === 'win32' ? 'where' : 'which'
|
|
62
|
+
// windowsHide:Windows 下 where/which 是控制台程序,无此配置每次调用都会弹一个黑窗
|
|
63
|
+
const result = spawnSync(tool, [cmd], { encoding: 'utf-8', windowsHide: true })
|
|
64
|
+
const first = result.status === 0 ? String(result.stdout || '').trim().split('\n')[0] : null
|
|
65
|
+
_whichCache.set(cmd, { ts: now, value: first || null })
|
|
66
|
+
return first || null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Windows 管理员预检:net session 仅提权 shell 可成功;非 Windows 视为无需该权限。
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
export function isWindowsAdmin() {
|
|
74
|
+
if (process.platform !== 'win32') return true
|
|
75
|
+
try {
|
|
76
|
+
spawnSync('net', ['session'], { stdio: 'pipe', windowsHide: true })
|
|
77
|
+
return true
|
|
78
|
+
} catch {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 后台分离进程(detached + unref,父进程退出不受影响);返回 child。
|
|
85
|
+
* @param {string} command
|
|
86
|
+
* @param {string[]} args
|
|
87
|
+
* @param {object} [opts]
|
|
88
|
+
* @returns {import('node:child_process').ChildProcess}
|
|
89
|
+
*/
|
|
90
|
+
export function spawnDetached(command, args, opts = {}) {
|
|
91
|
+
const proc = spawn(command, args, {
|
|
92
|
+
detached: true,
|
|
93
|
+
stdio: 'ignore',
|
|
94
|
+
windowsHide: false,
|
|
95
|
+
...opts,
|
|
96
|
+
})
|
|
97
|
+
proc.unref()
|
|
98
|
+
return proc
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* pgrep 查找匹配进程 pid(win32 无 pgrep,返回 [])。
|
|
103
|
+
* 容错:spawnSync 在参数非法时同步抛错、在 EAGAIN/maxBuffer 溢出时返回 {error,status:null},
|
|
104
|
+
* 一律按「无匹配」兜底,不抛异常。
|
|
105
|
+
* @param {string} pattern
|
|
106
|
+
* @returns {number[]}
|
|
107
|
+
*/
|
|
108
|
+
export function findPgrepPids(pattern) {
|
|
109
|
+
if (process.platform === 'win32') return []
|
|
110
|
+
let result
|
|
111
|
+
try {
|
|
112
|
+
result = spawnSync('pgrep', ['-f', pattern])
|
|
113
|
+
} catch {
|
|
114
|
+
return []
|
|
115
|
+
}
|
|
116
|
+
if (!result || result.status !== 0) return []
|
|
117
|
+
const pids = []
|
|
118
|
+
for (const line of String(result.stdout).trim().split('\n')) {
|
|
119
|
+
const pid = parseInt(line.trim(), 10)
|
|
120
|
+
if (Number.isFinite(pid)) pids.push(pid)
|
|
121
|
+
}
|
|
122
|
+
return pids
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* TCP 可达探测(连接成功 = true)。
|
|
127
|
+
* @param {string} host
|
|
128
|
+
* @param {number|string} port
|
|
129
|
+
* @param {number} [timeout=3000]
|
|
130
|
+
* @returns {Promise<boolean>}
|
|
131
|
+
*/
|
|
132
|
+
export function probeTcp(host, port, timeout = 3000) {
|
|
133
|
+
return new Promise((resolve) => {
|
|
134
|
+
const sock = net.connect({ host, port: Number(port), timeout })
|
|
135
|
+
sock.on('connect', () => { sock.destroy(); resolve(true) })
|
|
136
|
+
sock.on('error', () => resolve(false))
|
|
137
|
+
sock.on('timeout', () => { sock.destroy(); resolve(false) })
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 本地端口是否已被监听。
|
|
143
|
+
* @param {number|string} port
|
|
144
|
+
* @param {string} [host='127.0.0.1']
|
|
145
|
+
* @returns {Promise<boolean>}
|
|
146
|
+
*/
|
|
147
|
+
export function isPortBusy(port, host = '127.0.0.1') {
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
const s = net.createServer()
|
|
150
|
+
s.once('error', () => resolve(true))
|
|
151
|
+
s.once('listening', () => s.close(() => resolve(false)))
|
|
152
|
+
s.listen(Number(port), host)
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 跨平台查找 Chrome 可执行文件路径;找不到返回 null。
|
|
158
|
+
* macOS 多候选(App/Canary/Chromium/Edge/Homebrew);Linux 用 whichPath;
|
|
159
|
+
* Windows 用户级安装 → 注册表 App Paths → Program Files 两目录。
|
|
160
|
+
* @returns {string|null}
|
|
161
|
+
*/
|
|
162
|
+
export function getChromePath() {
|
|
163
|
+
if (process.platform === 'darwin') {
|
|
164
|
+
const candidates = [
|
|
165
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
166
|
+
'/Applications/Google Chrome Beta.app/Contents/MacOS/Google Chrome Beta',
|
|
167
|
+
'/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
|
|
168
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
169
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
170
|
+
path.join(os.homedir(), 'Applications', 'Google Chrome.app', 'Contents', 'MacOS', 'Google Chrome'),
|
|
171
|
+
whichPath('chromium'),
|
|
172
|
+
whichPath('google-chrome'),
|
|
173
|
+
].filter(Boolean)
|
|
174
|
+
for (const c of candidates) {
|
|
175
|
+
if (fs.existsSync(c)) return c
|
|
176
|
+
}
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
if (process.platform === 'linux') {
|
|
180
|
+
return whichPath('google-chrome') ?? whichPath('google-chrome-stable')
|
|
181
|
+
}
|
|
182
|
+
if (process.platform === 'win32') {
|
|
183
|
+
const candidates = [
|
|
184
|
+
process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'Google', 'Chrome', 'Application', 'chrome.exe') : null,
|
|
185
|
+
regAppPath('chrome.exe'),
|
|
186
|
+
process.env.PROGRAMFILES ? path.join(process.env.PROGRAMFILES, 'Google', 'Chrome', 'Application', 'chrome.exe') : null,
|
|
187
|
+
process.env['PROGRAMFILES(X86)'] ? path.join(process.env['PROGRAMFILES(X86)'], 'Google', 'Chrome', 'Application', 'chrome.exe') : null,
|
|
188
|
+
]
|
|
189
|
+
for (const c of candidates) {
|
|
190
|
+
if (c && fs.existsSync(c)) return c
|
|
191
|
+
}
|
|
192
|
+
return null
|
|
193
|
+
}
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 从注册表 App Paths 读取可执行文件路径(Windows 内置 reg.exe,无额外依赖);找不到返回 null */
|
|
198
|
+
function regAppPath(name) {
|
|
199
|
+
if (process.platform !== 'win32') return null
|
|
200
|
+
try {
|
|
201
|
+
const r = spawnSync('reg', [
|
|
202
|
+
'query',
|
|
203
|
+
`HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${name}`,
|
|
204
|
+
'/ve',
|
|
205
|
+
], { encoding: 'utf-8', windowsHide: true })
|
|
206
|
+
if (r.status !== 0) {
|
|
207
|
+
const r2 = spawnSync('reg', [
|
|
208
|
+
'query',
|
|
209
|
+
`HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\${name}`,
|
|
210
|
+
'/ve',
|
|
211
|
+
], { encoding: 'utf-8', windowsHide: true })
|
|
212
|
+
if (r2.status !== 0) return null
|
|
213
|
+
return parseRegDefault(r2.stdout)
|
|
214
|
+
}
|
|
215
|
+
return parseRegDefault(r.stdout)
|
|
216
|
+
} catch {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function parseRegDefault(out) {
|
|
222
|
+
// reg query /ve 输出形如: " (默认) REG_SZ C:\path\chrome.exe"
|
|
223
|
+
const m = String(out ?? '').match(/REG_SZ\s+(\S.*)$/m)
|
|
224
|
+
return m ? m[1].trim() : null
|
|
225
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { createConfigLoader, getXdgConfigDir, deepMerge, resolveEnvVars, resolvePkgVars } from './config-loader.js'
|
|
2
2
|
export { Shell } from './shell.js'
|
|
3
|
+
export { openBrowser, whichPath, isWindowsAdmin, spawnDetached, findPgrepPids, probeTcp, isPortBusy, getChromePath } from './client-ops.js'
|
|
3
4
|
export { parseProxy, openTunnel, fetchText, downloadToFile } from './http.js'
|
|
4
5
|
export { parseFlags, DEFAULT_BOOLEAN_FLAGS } from './args.js'
|
|
5
6
|
export { default as JSON5 } from 'json5'
|