spawnLauncher.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. const { spawn, exec } = require('child_process');
  2. const fs = require('fs').promises;
  3. const path = require('path');
  4. class SpawnLauncher {
  5. constructor() {
  6. this.processes = new Map();
  7. }
  8. /**
  9. * 安全的启动方法
  10. */
  11. async safeLaunch(exePath, args = [], options = {}) {
  12. try {
  13. console.log(`safe start: ${exePath}`);
  14. // 1. 验证文件
  15. const validation = await this.validateFile(exePath);
  16. if (!validation.valid) {
  17. throw new Error(validation.error);
  18. }
  19. // 2. 检查权限
  20. const canExecute = await this.checkPermissions(exePath);
  21. if (!canExecute) {
  22. throw new Error(`None access: ${exePath}`);
  23. }
  24. // 3. 准备参数
  25. const spawnOptions = this.prepareSpawnOptions(options);
  26. // 4. 执行启动
  27. return await this.executeSpawn(exePath, args, spawnOptions);
  28. } catch (error) {
  29. console.error('start fail:', error);
  30. // 5. 尝试备用方法
  31. if (error.code === 'EACCES') {
  32. return await this.tryAlternativeMethods(exePath, args, options);
  33. }
  34. throw error;
  35. }
  36. }
  37. /**
  38. * 验证文件
  39. */
  40. async validateFile(filePath) {
  41. console.log(`验证文件: ${filePath}`);
  42. // 检查是否为字符串
  43. if (typeof filePath !== 'string' || filePath.trim() === '') {
  44. return { valid: false, error: 'appPath is null' };
  45. }
  46. // 标准化路径
  47. const normalizedPath = this.normalizePath(filePath);
  48. try {
  49. // 检查文件是否存在
  50. await fs.access(normalizedPath, fs.constants.F_OK);
  51. } catch {
  52. return {
  53. valid: false,
  54. error: `file not exist: ${normalizedPath}`
  55. };
  56. }
  57. // 检查是否是文件
  58. try {
  59. const stats = await fs.stat(normalizedPath);
  60. if (!stats.isFile()) {
  61. return {
  62. valid: false,
  63. error: `not a file: ${normalizedPath}`
  64. };
  65. }
  66. } catch {
  67. return {
  68. valid: false,
  69. error: `can not access file: ${normalizedPath}`
  70. };
  71. }
  72. // 检查文件扩展名
  73. const ext = path.extname(normalizedPath).toLowerCase();
  74. if (process.platform === 'win32') {
  75. if (ext !== '.exe' && ext !== '.bat' && ext !== '.cmd') {
  76. console.warn(`no support extension: ${ext}`);
  77. }
  78. }
  79. return { valid: true, normalizedPath };
  80. }
  81. /**
  82. * 检查权限
  83. */
  84. async checkPermissions(filePath) {
  85. try {
  86. // 检查读权限
  87. await fs.access(filePath, fs.constants.R_OK);
  88. // 检查执行权限
  89. await fs.access(filePath, fs.constants.X_OK);
  90. return true;
  91. } catch (error) {
  92. console.log(`check access error: ${error.message}`);
  93. return false;
  94. }
  95. }
  96. /**
  97. * 准备 spawn 选项
  98. */
  99. prepareSpawnOptions(options) {
  100. const defaultOptions = {
  101. stdio: 'ignore',
  102. detached: true,
  103. windowsHide: true,
  104. shell: false, // 重要:Windows 上可能需要设为 true
  105. cwd: process.cwd(),
  106. env: { ...process.env },
  107. ...options
  108. };
  109. return defaultOptions;
  110. }
  111. /**
  112. * 执行 spawn
  113. */
  114. async executeSpawn(exePath, args, options) {
  115. return new Promise((resolve, reject) => {
  116. console.log(`exec spawn: ${exePath}`, args);
  117. const processId = Date.now().toString();
  118. const child = spawn(exePath, args, options);
  119. // 设置超时
  120. const timeout = setTimeout(() => {
  121. reject(new Error('exec timeout'));
  122. }, 10000);
  123. child.on('error', (error) => {
  124. clearTimeout(timeout);
  125. console.error('Spawn error:', error);
  126. reject(error);
  127. });
  128. child.on('spawn', () => {
  129. clearTimeout(timeout);
  130. console.log(`proecess start success, PID: ${child.pid}`);
  131. // 保存进程信息
  132. this.processes.set(processId, {
  133. id: processId,
  134. pid: child.pid,
  135. process: child,
  136. startTime: new Date()
  137. });
  138. // 允许父进程独立退出
  139. if (options.detached) {
  140. child.unref();
  141. }
  142. resolve({
  143. success: true,
  144. processId,
  145. pid: child.pid,
  146. command: `${exePath} ${args.join(' ')}`
  147. });
  148. });
  149. // 监听退出
  150. child.on('exit', (code) => {
  151. console.log(`进程 ${processId} 退出, 代码: ${code}`);
  152. this.processes.delete(processId);
  153. });
  154. });
  155. }
  156. /**
  157. * 尝试备用方法
  158. */
  159. async tryAlternativeMethods(exePath, args, options) {
  160. console.log('try start nativeMethods...');
  161. //1: shell: true
  162. try {
  163. console.log('1. shell: true');
  164. const result = await this.executeSpawn(exePath, args, {
  165. ...options,
  166. shell: true
  167. });
  168. return result;
  169. } catch (error1) {
  170. console.log('shell fail:', error1.message);
  171. }
  172. //2: exec
  173. try {
  174. console.log('2. exec');
  175. const result = await this.executeWithExec(exePath, args, options);
  176. return result;
  177. } catch (error2) {
  178. console.log('exec fail:', error2.message);
  179. }
  180. // 3: 管理员身份运行
  181. try {
  182. console.log('3: admin');
  183. const result = await this.runAsAdmin(exePath, args);
  184. return result;
  185. } catch (error3) {
  186. console.log('admin fail:', error3.message);
  187. }
  188. throw new Error('all fail');
  189. }
  190. /**
  191. * 使用 exec
  192. */
  193. async executeWithExec(exePath, args, options) {
  194. return new Promise((resolve, reject) => {
  195. const command = this.buildCommand(exePath, args);
  196. console.log(`exec: ${command}`);
  197. exec(command, options, (error, stdout, stderr) => {
  198. if (error) {
  199. reject(error);
  200. } else {
  201. resolve({
  202. success: true,
  203. stdout,
  204. stderr,
  205. pid: process.pid
  206. });
  207. }
  208. });
  209. });
  210. }
  211. /**
  212. * 以管理员身份运行
  213. */
  214. async runAsAdmin(exePath, args) {
  215. if (process.platform !== 'win32') {
  216. throw new Error('only support Windows');
  217. }
  218. return new Promise((resolve, reject) => {
  219. const escapedArgs = args.map(arg =>
  220. arg.includes(' ') ? `"${arg}"` : arg
  221. ).join(' ');
  222. const command = `"${exePath}" ${escapedArgs}`;
  223. const psCommand = `
  224. $psi = New-Object System.Diagnostics.ProcessStartInfo
  225. $psi.FileName = '${exePath}'
  226. $psi.Arguments = '${escapedArgs}'
  227. $psi.Verb = 'RunAs'
  228. $psi.UseShellExecute = $true
  229. [System.Diagnostics.Process]::Start($psi)
  230. `;
  231. exec(`powershell -Command "${psCommand}"`, (error) => {
  232. if (error) {
  233. reject(error);
  234. } else {
  235. resolve({
  236. success: true,
  237. admin: true
  238. });
  239. }
  240. });
  241. });
  242. }
  243. buildCommand(exePath, args) {
  244. const escapedExe = exePath.includes(' ') ? `"${exePath}"` : exePath;
  245. const escapedArgs = args.map(arg =>
  246. arg.includes(' ') ? `"${arg}"` : arg
  247. ).join(' ');
  248. return `${escapedExe} ${escapedArgs}`.trim();
  249. }
  250. normalizePath(filePath) {
  251. // 解析相对路径
  252. if (!path.isAbsolute(filePath)) {
  253. filePath = path.resolve(process.cwd(), filePath);
  254. }
  255. // 标准化路径分隔符
  256. return filePath.replace(/\//g, '\\');
  257. }
  258. /**
  259. * 获取文件信息
  260. */
  261. async getFileInfo(filePath) {
  262. try {
  263. const stats = await fs.stat(filePath);
  264. const permissions = {
  265. readable: !!(stats.mode & fs.constants.R_OK),
  266. writable: !!(stats.mode & fs.constants.W_OK),
  267. executable: !!(stats.mode & fs.constants.X_OK)
  268. };
  269. return {
  270. exists: true,
  271. isFile: stats.isFile(),
  272. isDirectory: stats.isDirectory(),
  273. size: stats.size,
  274. createdAt: stats.birthtime,
  275. modifiedAt: stats.mtime,
  276. permissions,
  277. mode: stats.mode.toString(8) // 八进制权限
  278. };
  279. } catch (error) {
  280. return {
  281. exists: false,
  282. error: error.message
  283. };
  284. }
  285. }
  286. }
  287. module.exports = SpawnLauncher;