@wwkit/cft 1.0.17 → 1.0.19
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/bin/index.js +22 -3
- package/package.json +2 -2
- package/scripts/postinstall.js +7 -2
- package/src/config.js +2 -0
- package/src/installer.js +75 -16
- package/src/proxy.js +0 -72
package/bin/index.js
CHANGED
|
@@ -2,14 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
import { execSync } from 'node:child_process'
|
|
4
4
|
import { readFileSync } from 'node:fs'
|
|
5
|
-
import { install, status, clear, showConfig, showEnv } from '../src/index.js'
|
|
5
|
+
import { install, status, clear, showConfig, showEnv, getProxy } from '../src/index.js'
|
|
6
6
|
|
|
7
7
|
const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version
|
|
8
8
|
|
|
9
9
|
const command = process.argv[2]
|
|
10
10
|
|
|
11
|
+
function parseInstallArgs(args) {
|
|
12
|
+
let proxy = ''
|
|
13
|
+
for (let i = 3; i < args.length; i++) {
|
|
14
|
+
const arg = args[i]
|
|
15
|
+
if (arg === '-p' || arg === '--proxy') {
|
|
16
|
+
proxy = args[i + 1] || ''
|
|
17
|
+
i++
|
|
18
|
+
} else if (arg.startsWith('--proxy=')) {
|
|
19
|
+
proxy = arg.slice('--proxy='.length)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return proxy
|
|
23
|
+
}
|
|
24
|
+
|
|
11
25
|
if (command === 'install') {
|
|
12
|
-
|
|
26
|
+
const proxy = parseInstallArgs(process.argv)
|
|
27
|
+
install(proxy ? { proxy } : {})
|
|
13
28
|
.then((info) => {
|
|
14
29
|
console.log()
|
|
15
30
|
console.log(' ✓ CFT 安装完成')
|
|
@@ -30,7 +45,10 @@ if (command === 'install') {
|
|
|
30
45
|
} else if (command === 'env') {
|
|
31
46
|
showEnv()
|
|
32
47
|
} else if (command === 'upgrade') {
|
|
33
|
-
|
|
48
|
+
const proxy = getProxy()
|
|
49
|
+
const npmArgs = ['install', '-g', '@wwkit/cft@latest']
|
|
50
|
+
if (proxy) npmArgs.push('--proxy', proxy)
|
|
51
|
+
execSync(`npm ${npmArgs.join(' ')}`, { stdio: 'inherit', shell: true })
|
|
34
52
|
} else if (command === 'version') {
|
|
35
53
|
console.log(pkgVersion)
|
|
36
54
|
} else if (command === 'help' || command === undefined) {
|
|
@@ -38,6 +56,7 @@ if (command === 'install') {
|
|
|
38
56
|
|
|
39
57
|
用法:
|
|
40
58
|
cft install 下载并安装 Chrome for Testing(已装同版本则跳过)
|
|
59
|
+
cft install -p <proxy> 指定代理下载安装(socks5://host:port|http://host:port),覆盖配置
|
|
41
60
|
cft status 显示安装状态
|
|
42
61
|
cft config 显示用户配置(~/.config/cft/config.json5)
|
|
43
62
|
cft env 显示 cft 创建的全局环境变量
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwkit/cft",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"author": "bluesliu <langcai163@163.com>",
|
|
5
5
|
"description": "Chrome for Testing (CFT) installer — download, extract, chmod, dep-check",
|
|
6
6
|
"type": "module",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"extract-zip": "^2.0.1",
|
|
39
|
-
"@wwkit/shared": "1.0.
|
|
39
|
+
"@wwkit/shared": "1.0.19"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"jest": "^29.7.0"
|
package/scripts/postinstall.js
CHANGED
|
@@ -4,12 +4,17 @@ import { spawnSync } from 'node:child_process'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { fileURLToPath } from 'node:url'
|
|
6
6
|
import { Shell } from '@wwkit/shared'
|
|
7
|
-
import { getLocation, getOs } from '../src/index.js'
|
|
7
|
+
import { getLocation, getOs, getProxy } from '../src/index.js'
|
|
8
8
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
10
10
|
const bin = path.join(__dirname, '..', 'bin', 'index.js')
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
// 回退链:npm 安装参数 --proxy(npm_config_proxy)> cft 配置 proxy
|
|
13
|
+
const proxy = process.env.npm_config_proxy || getProxy()
|
|
14
|
+
const args = [bin, 'install']
|
|
15
|
+
if (proxy) args.push('-p', proxy)
|
|
16
|
+
|
|
17
|
+
const result = spawnSync('node', args, { stdio: 'inherit' })
|
|
13
18
|
if (result.status !== 0) {
|
|
14
19
|
console.error('cft: postinstall (cft install) failed')
|
|
15
20
|
process.exit(1)
|
package/src/config.js
CHANGED
|
@@ -9,6 +9,8 @@ const INSTALL_DIR = getXdgConfigDir('cft')
|
|
|
9
9
|
const _loader = createConfigLoader({
|
|
10
10
|
builtinConfigPath: path.join(__dirname, 'config.json5'),
|
|
11
11
|
userConfigDir: INSTALL_DIR,
|
|
12
|
+
envContentVar: 'CFT_CONFIG_CONTENT',
|
|
13
|
+
envDirVar: 'CFT_CONFIG_DIR',
|
|
12
14
|
})
|
|
13
15
|
|
|
14
16
|
export const INSTALL_LOCATION = INSTALL_DIR
|
package/src/installer.js
CHANGED
|
@@ -9,42 +9,46 @@ import { getOs, VALID_OS_TYPES } from './platform.js'
|
|
|
9
9
|
import { find as findExe } from './exefinder.js'
|
|
10
10
|
import { check as checkDeps, mapToPackages, install as installDeps, isSupported as depsSupported, detectPackageManager } from './depchecker.js'
|
|
11
11
|
import { getVersion, getRegistry, getProxy, getLocation, copyBuiltinConfig } from './config.js'
|
|
12
|
-
import { parseProxy, openTunnel } from '
|
|
12
|
+
import { parseProxy, openTunnel } from '@wwkit/shared'
|
|
13
13
|
|
|
14
14
|
const USER_AGENT = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 cft-installer'
|
|
15
|
+
const MANUAL_DOWNLOAD_HINT = '资源下载失败,请在 http://deepbluenet.com/webwork/#/mirror 手动下载资源,并将资源解压到 ~/.config/cft 目录,然后将地址添加到 ~/.config/webwork/webwork.json5 中的 env.SELENIUM_CFT_DIR 属性上'
|
|
15
16
|
|
|
16
17
|
function log(msg) {
|
|
17
18
|
console.log(` ${msg}`)
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
function sendRequest(url, cb) {
|
|
21
|
-
const proxy = parseProxy(
|
|
21
|
+
function sendRequest(url, proxyValue, cb, method = 'GET') {
|
|
22
|
+
const proxy = parseProxy(proxyValue)
|
|
22
23
|
const target = new URL(url)
|
|
23
24
|
const headers = { 'user-agent': USER_AGENT }
|
|
24
25
|
const onError = (e) => cb(e, null)
|
|
25
26
|
|
|
26
27
|
if (!proxy) {
|
|
27
28
|
const proto = target.protocol === 'https:' ? https : http
|
|
28
|
-
const req = proto.
|
|
29
|
+
const req = proto.request(url, { headers, method }, (res) => cb(null, res))
|
|
29
30
|
req.on('error', onError)
|
|
31
|
+
req.end()
|
|
30
32
|
return
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
if (proxy.protocol === 'http:' && target.protocol === 'http:') {
|
|
34
|
-
const req = http.
|
|
36
|
+
const req = http.request({
|
|
35
37
|
host: proxy.hostname,
|
|
36
38
|
port: Number(proxy.port) || 80,
|
|
37
39
|
path: url,
|
|
40
|
+
method,
|
|
38
41
|
headers: { ...headers, host: target.host },
|
|
39
42
|
}, (res) => cb(null, res))
|
|
40
43
|
req.on('error', onError)
|
|
44
|
+
req.end()
|
|
41
45
|
return
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
openTunnel(proxy, target).then((socket) => {
|
|
45
49
|
const proto = target.protocol === 'https:' ? https : http
|
|
46
50
|
const req = proto.request({
|
|
47
|
-
method
|
|
51
|
+
method,
|
|
48
52
|
hostname: target.hostname,
|
|
49
53
|
port: Number(target.port) || (target.protocol === 'https:' ? 443 : 80),
|
|
50
54
|
path: target.pathname + target.search,
|
|
@@ -58,7 +62,40 @@ function sendRequest(url, cb) {
|
|
|
58
62
|
}).catch(onError)
|
|
59
63
|
}
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
/**
|
|
66
|
+
* 访问前预检完整路径是否可达(HEAD 请求,非 2xx/3xx 视为不可达)
|
|
67
|
+
* @param {string} url - 完整下载 URL
|
|
68
|
+
* @param {string} [proxyValue] - 代理地址
|
|
69
|
+
* @returns {Promise<boolean>}
|
|
70
|
+
*/
|
|
71
|
+
function checkUrlReachable(url, proxyValue) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
sendRequest(url, proxyValue, (err, response) => {
|
|
74
|
+
if (err) { resolve(false); return }
|
|
75
|
+
response.resume()
|
|
76
|
+
if (response.statusCode >= 200 && response.statusCode < 400) {
|
|
77
|
+
resolve(true)
|
|
78
|
+
} else if (response.statusCode === 405) {
|
|
79
|
+
// 405 Method Not Allowed — 服务器已响应,仅不支持 HEAD
|
|
80
|
+
resolve(true)
|
|
81
|
+
} else {
|
|
82
|
+
resolve(false)
|
|
83
|
+
}
|
|
84
|
+
}, 'HEAD')
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 下载失败时的统一终止提示
|
|
90
|
+
* @param {Error} [cause] - 原始错误
|
|
91
|
+
*/
|
|
92
|
+
function downloadError(cause) {
|
|
93
|
+
const err = new Error(MANUAL_DOWNLOAD_HINT)
|
|
94
|
+
if (cause) err.cause = cause
|
|
95
|
+
return err
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function downloadFile(url, destPath, proxyValue, redirects = 0) {
|
|
62
99
|
return new Promise((resolve, reject) => {
|
|
63
100
|
if (redirects > 5) {
|
|
64
101
|
reject(new Error(`Too many redirects: ${url}`))
|
|
@@ -67,7 +104,7 @@ function downloadFile(url, destPath, redirects = 0) {
|
|
|
67
104
|
const filename = path.basename(destPath)
|
|
68
105
|
log(`下载 ${filename} ...`)
|
|
69
106
|
|
|
70
|
-
sendRequest(url, (err, response) => {
|
|
107
|
+
sendRequest(url, proxyValue, (err, response) => {
|
|
71
108
|
if (err) { reject(err); return }
|
|
72
109
|
if (response.statusCode === 301 || response.statusCode === 302 || response.statusCode === 307 || response.statusCode === 308) {
|
|
73
110
|
const location = response.headers.location
|
|
@@ -76,7 +113,7 @@ function downloadFile(url, destPath, redirects = 0) {
|
|
|
76
113
|
reject(new Error(`Redirect without Location header: ${url}`))
|
|
77
114
|
return
|
|
78
115
|
}
|
|
79
|
-
downloadFile(location, destPath, redirects + 1).then(resolve).catch(reject)
|
|
116
|
+
downloadFile(location, destPath, proxyValue, redirects + 1).then(resolve).catch(reject)
|
|
80
117
|
return
|
|
81
118
|
}
|
|
82
119
|
if (response.statusCode !== 200) {
|
|
@@ -108,9 +145,9 @@ function downloadFile(url, destPath, redirects = 0) {
|
|
|
108
145
|
})
|
|
109
146
|
}
|
|
110
147
|
|
|
111
|
-
async function downloadExtract(url, saveDir, filename, extractDir) {
|
|
148
|
+
async function downloadExtract(url, saveDir, filename, extractDir, proxyValue) {
|
|
112
149
|
const filePath = path.join(saveDir, filename)
|
|
113
|
-
await downloadFile(url, filePath)
|
|
150
|
+
await downloadFile(url, filePath, proxyValue)
|
|
114
151
|
|
|
115
152
|
log(`解压到 ${path.basename(extractDir)}`)
|
|
116
153
|
if (fs.existsSync(extractDir)) {
|
|
@@ -207,12 +244,19 @@ function checkSystemDeps(chromeExe, driverExe) {
|
|
|
207
244
|
}
|
|
208
245
|
}
|
|
209
246
|
|
|
210
|
-
|
|
247
|
+
/**
|
|
248
|
+
* 安装 CFT(Chrome + ChromeDriver)
|
|
249
|
+
* @param {object} [opts]
|
|
250
|
+
* @param {string} [opts.proxy] - 显式代理地址(socks5://host:port 等);传入则直接使用,不读配置
|
|
251
|
+
* @returns {Promise<object>}
|
|
252
|
+
*/
|
|
253
|
+
export async function install(opts = {}) {
|
|
211
254
|
copyBuiltinConfig()
|
|
212
255
|
const version = getVersion()
|
|
213
256
|
const registry = getRegistry()
|
|
214
257
|
const location = getLocation()
|
|
215
258
|
const osType = getOs()
|
|
259
|
+
const proxy = opts.proxy || getProxy()
|
|
216
260
|
|
|
217
261
|
if (!VALID_OS_TYPES.includes(osType)) {
|
|
218
262
|
throw new Error(`Unsupported OS: ${osType}`)
|
|
@@ -228,13 +272,28 @@ export async function install() {
|
|
|
228
272
|
|
|
229
273
|
const chromeUrl = `${registry}/${version}/${osType}/chrome-${osType}.zip`
|
|
230
274
|
const driverUrl = `${registry}/${version}/${osType}/chromedriver-${osType}.zip`
|
|
231
|
-
log(`从网络下载 CFT ${version} (${osType})`)
|
|
232
|
-
|
|
233
275
|
const chromeDir = path.join(location, `chrome-${osType}`)
|
|
234
276
|
const driverDir = path.join(location, `chromedriver-${osType}`)
|
|
235
277
|
|
|
236
|
-
|
|
237
|
-
|
|
278
|
+
log(`从网络下载 CFT ${version} (${osType})`)
|
|
279
|
+
if (proxy) log(`使用代理: ${proxy}`)
|
|
280
|
+
|
|
281
|
+
for (const url of [chromeUrl, driverUrl]) {
|
|
282
|
+
const ok = await checkUrlReachable(url, proxy)
|
|
283
|
+
if (ok) {
|
|
284
|
+
log(`[OK] 资源可达: ${url}`)
|
|
285
|
+
} else {
|
|
286
|
+
log(`[FAIL] 资源不可达: ${url}`)
|
|
287
|
+
throw downloadError()
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await downloadExtract(chromeUrl, location, `chrome-${osType}.zip`, chromeDir, proxy)
|
|
293
|
+
await downloadExtract(driverUrl, location, `chromedriver-${osType}.zip`, driverDir, proxy)
|
|
294
|
+
} catch (cause) {
|
|
295
|
+
throw downloadError(cause)
|
|
296
|
+
}
|
|
238
297
|
|
|
239
298
|
const chromeExe = findExe(chromeDir, 'chrome')
|
|
240
299
|
const driverExe = findExe(driverDir, 'chromedriver')
|
package/src/proxy.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import net from 'node:net'
|
|
2
|
-
import http from 'node:http'
|
|
3
|
-
|
|
4
|
-
export function parseProxy(raw) {
|
|
5
|
-
if (!raw) return null
|
|
6
|
-
try {
|
|
7
|
-
const u = new URL(raw)
|
|
8
|
-
if (!u.hostname) return null
|
|
9
|
-
if (u.protocol !== 'http:' && u.protocol !== 'https:' && u.protocol !== 'socks5:' && u.protocol !== 'socks:') {
|
|
10
|
-
return null
|
|
11
|
-
}
|
|
12
|
-
return u
|
|
13
|
-
} catch {
|
|
14
|
-
return null
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function socks5Connect(proxy, host, port) {
|
|
19
|
-
return new Promise((resolve, reject) => {
|
|
20
|
-
const sock = net.connect(Number(proxy.port) || 1080, proxy.hostname)
|
|
21
|
-
let phase = 'greeting'
|
|
22
|
-
const cleanup = (err) => { sock.removeAllListeners(); sock.destroy(); reject(err) }
|
|
23
|
-
sock.once('error', cleanup)
|
|
24
|
-
sock.once('connect', () => {
|
|
25
|
-
sock.write(Buffer.from([0x05, 0x01, 0x00]))
|
|
26
|
-
})
|
|
27
|
-
sock.on('data', (data) => {
|
|
28
|
-
if (phase === 'greeting') {
|
|
29
|
-
if (data[0] !== 0x05 || data[1] !== 0x00) { cleanup(new Error(`socks5 代理拒绝无认证 (method=${data[1]})`)); return }
|
|
30
|
-
phase = 'connect'
|
|
31
|
-
const hostBuf = Buffer.from(host, 'utf8')
|
|
32
|
-
const req = Buffer.alloc(7 + hostBuf.length)
|
|
33
|
-
req[0] = 0x05; req[1] = 0x01; req[2] = 0x00; req[3] = 0x03
|
|
34
|
-
req[4] = hostBuf.length
|
|
35
|
-
hostBuf.copy(req, 5)
|
|
36
|
-
req.writeUInt16BE(port, 5 + hostBuf.length)
|
|
37
|
-
sock.write(req)
|
|
38
|
-
} else if (phase === 'connect') {
|
|
39
|
-
if (data[0] !== 0x05 || data[1] !== 0x00) { cleanup(new Error(`socks5 连接失败 (code=${data[1]})`)); return }
|
|
40
|
-
sock.removeAllListeners('data')
|
|
41
|
-
sock.removeListener('error', cleanup)
|
|
42
|
-
resolve(sock)
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function httpConnect(proxy, host, port) {
|
|
49
|
-
return new Promise((resolve, reject) => {
|
|
50
|
-
const req = http.request({
|
|
51
|
-
method: 'CONNECT',
|
|
52
|
-
host: proxy.hostname,
|
|
53
|
-
port: Number(proxy.port) || 80,
|
|
54
|
-
path: `${host}:${port}`,
|
|
55
|
-
})
|
|
56
|
-
req.once('error', reject)
|
|
57
|
-
req.once('connect', (res, socket) => {
|
|
58
|
-
if (res.statusCode !== 200) { socket.destroy(); reject(new Error(`HTTP 代理 CONNECT 返回 ${res.statusCode}`)); return }
|
|
59
|
-
resolve(socket)
|
|
60
|
-
})
|
|
61
|
-
req.end()
|
|
62
|
-
})
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function openTunnel(proxy, target) {
|
|
66
|
-
const host = target.hostname
|
|
67
|
-
const port = Number(target.port) || (target.protocol === 'https:' ? 443 : 80)
|
|
68
|
-
if (proxy.protocol === 'socks5:' || proxy.protocol === 'socks:') {
|
|
69
|
-
return socks5Connect(proxy, host, port)
|
|
70
|
-
}
|
|
71
|
-
return httpConnect(proxy, host, port)
|
|
72
|
-
}
|