run-process.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. #!/usr/bin/env node
  2. /**
  3. * run-process.js
  4. * 接收两个参数:ip 数组 (JSON)、脚本名
  5. * 异步根据每个 ip 执行脚本;执行前先 adb connect 该设备,确保连接成功再跑流程。
  6. * 运行日志与错误一律写入 static/process/<脚本名>/log.txt,便于打包后排查。
  7. *
  8. * 调用示例:node run-process.js '["192.168.2.5","192.168.2.6"]' 'RedNoteAIThumbsUp'
  9. */
  10. const path = require('path')
  11. const fs = require('fs')
  12. const { execSync } = require('child_process')
  13. // 先确定 process 目录和 log 路径(仅依赖 env 和 argv),确保失败时也能写 log.txt
  14. const staticRoot = process.env.STATIC_ROOT ? path.resolve(process.env.STATIC_ROOT) : path.join(__dirname, '..', 'static')
  15. const scriptName = process.argv[3] || 'Unknown'
  16. const folderPath = path.resolve(path.join(staticRoot, 'process', scriptName))
  17. const logFilePath = path.join(folderPath, 'log.txt')
  18. function ensureProcessDirAndLog() {
  19. try {
  20. if (!fs.existsSync(folderPath)) fs.mkdirSync(folderPath, { recursive: true })
  21. } catch (e) {}
  22. }
  23. const UTF8_BOM = Buffer.from('\uFEFF', 'utf8')
  24. function appendLog(text) {
  25. ensureProcessDirAndLog()
  26. try {
  27. const str = typeof text === 'string' ? text : String(text)
  28. const exists = fs.existsSync(logFilePath)
  29. const needsBom = !exists || (exists && fs.statSync(logFilePath).size === 0)
  30. if (needsBom) fs.appendFileSync(logFilePath, UTF8_BOM)
  31. fs.appendFileSync(logFilePath, Buffer.from(str, 'utf8'))
  32. } catch (e) {}
  33. }
  34. function writeErrorAndExit(message, err) {
  35. const line = `[${new Date().toISOString()}] [run-process] ERROR: ${message}${err ? ' ' + (err.stack || err.message) : ''}\n`
  36. process.stderr.write(line)
  37. appendLog(line)
  38. process.exit(1)
  39. }
  40. ensureProcessDirAndLog()
  41. let config, projectRoot, adbPath, ipListJson, ipList, actions, resolution, executeActionSequence
  42. try {
  43. const configPath = process.env.STATIC_ROOT ? path.join(path.dirname(staticRoot), 'config.js') : path.join(__dirname, '..', 'config.js')
  44. config = require(configPath)
  45. projectRoot = (config.projectRoot && fs.existsSync(config.projectRoot))
  46. ? config.projectRoot
  47. : path.dirname(path.resolve(configPath))
  48. adbPath = config.adbPath?.path
  49. ? (path.isAbsolute(config.adbPath.path) ? config.adbPath.path : path.resolve(projectRoot, config.adbPath.path))
  50. : path.join(projectRoot, 'lib', 'scrcpy-adb', process.platform === 'win32' ? 'adb.exe' : 'adb')
  51. ipListJson = process.argv[2]
  52. ipList = JSON.parse(ipListJson || '[]')
  53. const processJsonPath = path.join(folderPath, 'process.json')
  54. if (!fs.existsSync(processJsonPath)) {
  55. writeErrorAndExit(`process.json not found: ${processJsonPath}`)
  56. }
  57. const efCompiler = require('./ef-compiler/ef-compiler.js')
  58. const workflow = JSON.parse(fs.readFileSync(processJsonPath, 'utf8'))
  59. actions = efCompiler.parseWorkflow(workflow).actions
  60. executeActionSequence = efCompiler.executeActionSequence
  61. resolution = { width: 1080, height: 1920 }
  62. } catch (e) {
  63. writeErrorAndExit('run-process init failed', e)
  64. }
  65. const ADB_PORT = 5555
  66. let shouldStop = false
  67. function logLine(msg) {
  68. const line = `[${new Date().toISOString()}] [run-process] ${msg}\n`
  69. process.stdout.write(line)
  70. appendLog(line)
  71. }
  72. function sleep(ms) {
  73. return new Promise((resolve) => setTimeout(resolve, ms))
  74. }
  75. /** Windows 下 cmd/adb 的 stderr 多为 GBK,需按 GBK 解码再写入 log 才能正确显示中文 */
  76. function decodeStderrForLog(stderr) {
  77. if (stderr == null) return ''
  78. if (Buffer.isBuffer(stderr) && process.platform === 'win32') {
  79. try {
  80. const iconv = require('iconv-lite')
  81. return (iconv.decode(stderr, 'gbk') || stderr.toString('utf8')).trim()
  82. } catch (_) {
  83. return stderr.toString('utf8').trim()
  84. }
  85. }
  86. const s = typeof stderr === 'string' ? stderr : (stderr && stderr.toString ? stderr.toString() : '')
  87. return s.trim()
  88. }
  89. /** 对指定 IP 执行 adb connect,与 bat-tool/adb-connect-test 行为一致:可重试、短延迟,确保设备就绪 */
  90. async function ensureDeviceConnected(ip, port, logLineFn) {
  91. const deviceId = `${ip}:${port}`
  92. const maxTries = 3
  93. const delayMs = 2000
  94. const execOpts = process.platform === 'win32' ? { encoding: null } : { encoding: 'utf-8' }
  95. for (let i = 0; i < maxTries; i++) {
  96. try {
  97. const out = execSync(`"${adbPath}" connect ${deviceId}`, execOpts)
  98. const outStr = Buffer.isBuffer(out) ? out.toString('utf8') : String(out)
  99. if (outStr.trim().includes('connected') || outStr.trim().includes('already connected')) return true
  100. } catch (e) {
  101. const errText = process.platform === 'win32' ? decodeStderrForLog(e.stderr) : (e.stderr || e.message || '').trim()
  102. const pathExists = fs.existsSync(adbPath) ? 'exists' : 'NOT FOUND'
  103. if (logLineFn) logLineFn(`Connect attempt ${i + 1}/${maxTries}: ${errText || e.message || 'failed'} (adb path: ${adbPath}, ${pathExists})`)
  104. }
  105. if (i < maxTries - 1) {
  106. if (logLineFn) logLineFn(`Wait ${delayMs / 1000}s, retry connect ${deviceId} ...`)
  107. await sleep(delayMs)
  108. }
  109. }
  110. return false
  111. }
  112. /** 启动执行:多设备串行(一台跑完再跑下一台,与流程步骤同步语义一致);任一台失败则 shouldStop;log.txt 仅写入报错与带 log:true 的 echo */
  113. async function start() {
  114. logLine(`Process "${scriptName}" start, devices: ${ipList.length}`)
  115. let failedIp = null
  116. const runOne = async (ip) => {
  117. if (shouldStop) return { ip, success: false, stopped: true }
  118. const deviceId = ip.includes(':') ? ip : `${ip}:${ADB_PORT}`
  119. const [deviceIp, devicePort] = deviceId.includes(':') ? deviceId.split(':') : [ip, ADB_PORT]
  120. logLine(`Connecting ${deviceId} ...`)
  121. const connected = await ensureDeviceConnected(deviceIp, devicePort, logLine)
  122. if (!connected) {
  123. logLine(`Failed to connect ${deviceId}`)
  124. return { ip, success: false }
  125. }
  126. logLine(`Running on ${deviceId}`)
  127. const result = await executeActionSequence(actions, deviceId, folderPath, resolution, 1000, null, () => shouldStop)
  128. const ok = result && result.success === true
  129. if (!ok) {
  130. if (!failedIp) { failedIp = ip; shouldStop = true }
  131. }
  132. return { ip, success: ok }
  133. }
  134. const results = []
  135. for (const ip of ipList) {
  136. results.push(await runOne(ip))
  137. }
  138. const output = failedIp
  139. ? { success: false, failedIp, results }
  140. : { success: true, results }
  141. logLine(failedIp ? `Process finished with failed device: ${failedIp}` : 'Process finished successfully.')
  142. const resultLine = JSON.stringify(output) + '\n'
  143. process.stdout.write(resultLine)
  144. process.exit(failedIp ? 1 : 0)
  145. }
  146. /** 停止执行(SIGTERM/SIGINT 时调用) */
  147. function stop() {
  148. shouldStop = true
  149. }
  150. process.on('SIGTERM', () => { stop(); process.exit(130) })
  151. process.on('SIGINT', () => { stop(); process.exit(130) })
  152. start().catch((e) => {
  153. writeErrorAndExit('run-process start failed', e)
  154. })