yichael vor 2 Wochen
Ursprung
Commit
d4e497a052

+ 60 - 0
doc/打包流程.md

@@ -0,0 +1,60 @@
+# 打包流程
+
+1. 关掉已运行的 AndroidRemoteController.exe。
+2. 项目根目录执行:`npm run build`,再执行 `npx electron-builder --win --arm64`(或 `--x64`)。
+3. 产物在 `dist\win-arm64-unpacked`(或 `dist\win-unpacked`)。
+4. 运行:`dist\win-arm64-unpacked\AndroidRemoteController.exe`。
+
+---
+
+## 打包前需确认的配置
+
+### package.json → build
+
+- **directories.output**
+  打包输出目录。例如 `"dist"` 时,产物在 `dist\win-arm64-unpacked` 或 `dist\win-unpacked`。
+
+- **files**
+  打进 asar 的内容。当前为 `["dist/**","electron/**","configs/**","nodejs/**","python/**","lib/**"]`,不要漏掉运行时用到的目录。
+
+- **asarUnpack**
+  不压进 asar、解出到 `app.asar.unpacked` 的目录。当前为 `nodejs/**`、`configs/**`、`lib/**`、`python/**`,主进程和 Node 脚本会从这里读。
+
+- **win.target**
+  - `target: "dir"`:只输出目录,不打安装包。
+  - `arch: ["arm64"]` 或 `["x64"]`:架构,与命令行 `--arm64` / `--x64` 一致。
+
+### configs/config.js
+
+- **window**
+  窗口宽高、是否隐藏菜单栏。
+
+- **adbPath.path**
+  相对项目根的 adb 路径,默认 `lib/scrcpy-adb/adb.exe`。打包后以 `app.asar.unpacked` 为根解析,一般不用改。
+
+- **pythonPath.path**
+  Python 运行时目录,按 `process.arch` 用 `python/arm64` 或 `python/x64`,打包后同上。
+
+### 目录与运行时
+
+- **lib/scrcpy-adb/**
+  需存在 adb.exe、scrcpy.exe 及 scrcpy 依赖 dll/bat;会被 `files` 打进包,在 unpacked 中供主进程和 node 脚本调用。
+
+- **python/arm64** 或 **python/x64**
+  按打包架构保留对应目录(内含嵌入式 Python 或 venv),仅在使用图像匹配/裁剪等流程时需要。打 arm64 包需有 `python/arm64`,打 x64 需有 `python/x64`。
+
+### vite.config.mjs 与打包
+
+- **base: './'**
+  打包后的 exe 从“本地文件”打开页面。不设为 `'./'` 时,js/css 链接会指到错误位置,白屏或报错;设为 `'./'` 后为相对路径“当前目录下的 assets/xxx”,可正常加载。**不要删或改成别的。**
+
+- **server**
+  只影响开发时的地址和端口,与打 exe 无关。
+
+- **构建产物**
+  `npm run build` 会生成 `dist` 文件夹(index.html + assets 等)。打 exe 时会把 `dist` 打进包,exe 启动时加载此处的 index.html。
+
+### package-lock.json
+
+- 执行 `npm install`、`npm run build` 时会按 **package-lock.json** 安装依赖,保证各环境版本一致、构建可复现。
+- 该文件不打进 exe(不在 `build.files` 里),只用于本地/CI 的安装与构建。

+ 46 - 39
electron/main.js

@@ -1,45 +1,34 @@
-const { app, BrowserWindow ,ipcMain} = require('electron')
+const { app, BrowserWindow, ipcMain } = require('electron')
 const path = require('path')
 const os = require('os')
 const fs = require('fs')
-const config = require('../configs/config.js')
+
+const unpackedRoot = app.isPackaged
+  ? path.join(path.dirname(process.execPath), 'resources', 'app.asar.unpacked')
+  : path.join(__dirname, '..')
+const config = require(path.join(unpackedRoot, 'configs', 'config.js'))
 const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged
 
-// 修复缓存权限问题:设置用户数据目录到有权限的位置
-// 必须在 app.whenReady() 之前调用
+const staticDir = path.join(unpackedRoot, 'static')
+if (!fs.existsSync(staticDir)) {
+  fs.mkdirSync(staticDir, { recursive: true })
+}
+
+// 修复缓存权限:userData 与缓存目录设到临时目录,避免安装目录只读导致 "Unable to move the cache (0x5)"
 if (process.platform === 'win32') {
-  try {
-    // 设置缓存目录到用户临时目录,避免权限问题
-    const userDataPath = path.join(os.tmpdir(), 'electron-react-vite-app')
-    
-    // 确保目录存在
-    if (!fs.existsSync(userDataPath)) {
-      fs.mkdirSync(userDataPath, { recursive: true })
-    }
-    
-    // 创建缓存子目录
-    const cacheDir = path.join(userDataPath, 'cache')
-    const gpuCacheDir = path.join(userDataPath, 'gpu-cache')
-    if (!fs.existsSync(cacheDir)) {
-      fs.mkdirSync(cacheDir, { recursive: true })
-    }
-    if (!fs.existsSync(gpuCacheDir)) {
-      fs.mkdirSync(gpuCacheDir, { recursive: true })
-    }
-    
-    // 设置用户数据路径(必须在 app.whenReady() 之前)
-    app.setPath('userData', userDataPath)
-    
-    // 设置缓存目录到有权限的位置
-    app.commandLine.appendSwitch('disk-cache-dir', cacheDir)
-    app.commandLine.appendSwitch('gpu-disk-cache-dir', gpuCacheDir)
-    
-    console.log(`[OK] Cache directories set to: ${userDataPath}`)
-  } catch (error) {
-    console.warn('[WARN] Failed to set cache directories:', error.message)
-    // 如果设置失败,尝试禁用 GPU 缓存作为备选方案
-    app.commandLine.appendSwitch('disable-gpu-sandbox')
+  const userDataPath = path.join(os.tmpdir(), 'AndroidRemoteController')
+  if (!fs.existsSync(userDataPath)) {
+    fs.mkdirSync(userDataPath, { recursive: true })
   }
+  const cacheDir = path.join(userDataPath, 'Cache')
+  const gpuCacheDir = path.join(userDataPath, 'GPUCache')
+  if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true })
+  if (!fs.existsSync(gpuCacheDir)) fs.mkdirSync(gpuCacheDir, { recursive: true })
+
+  app.setPath('userData', userDataPath)
+  app.commandLine.appendSwitch('disk-cache-dir', cacheDir)
+  app.commandLine.appendSwitch('gpu-disk-cache-dir', gpuCacheDir)
+  app.commandLine.appendSwitch('disable-gpu-shader-disk-cache')
 }
 
 // 保存主窗口引用,用于推送消息
@@ -71,7 +60,7 @@ function createWindow() {
       mainWindow.webContents.openDevTools()
     }
   } else {
-    mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
+    mainWindow.loadFile(path.join(unpackedRoot, 'dist', 'index.html'))
   }
 }
 
@@ -83,7 +72,7 @@ const runningProcesses = new Map()
 // Execute Node.js script
 ipcMain.handle('run-nodejs-script', async (event, scriptName, ...parameters) => {
   return new Promise((resolve, reject) => {
-    const scriptPath = path.join(__dirname, '../nodejs', `${scriptName}.js`)
+    const scriptPath = path.join(unpackedRoot, 'nodejs', `${scriptName}.js`)
     const processKey = `${scriptName}-${parameters.join('-')}`
     
     // 如果进程已运行,先停止它
@@ -175,6 +164,24 @@ ipcMain.handle('kill-nodejs-script', async (event, scriptName, ...parameters) =>
   return { killed: false }
 })
 
+/** 检查 scrcpy 是否仍在运行(读 pid 文件并用 process.kill(pid,0) 探测),用于用户直接关窗口时同步按钮状态 */
+ipcMain.handle('check-scrcpy-running', async () => {
+  const pidFile = path.join(unpackedRoot, 'static', 'scrcpy-pid.json')
+  if (!fs.existsSync(pidFile)) return { running: false }
+  let pid
+  try {
+    pid = JSON.parse(fs.readFileSync(pidFile, 'utf-8')).pid
+  } catch (_) {
+    return { running: false }
+  }
+  try {
+    process.kill(pid, 0)
+    return { running: true }
+  } catch (_) {
+    return { running: false }
+  }
+})
+
 // Execute Python script
 ipcMain.handle('run-python-script', async (event, scriptName, ...parameters) => {
   return new Promise((resolve, reject) => {
@@ -187,7 +194,7 @@ ipcMain.handle('run-python-script', async (event, scriptName, ...parameters) =>
       }
     }
     
-    const scriptPath = path.join(__dirname, '../python/scripts', `${scriptName}.py`)
+    const scriptPath = path.join(unpackedRoot, 'python', 'scripts', `${scriptName}.py`)
     
     if (!fs.existsSync(scriptPath)) {
       reject({
@@ -239,7 +246,7 @@ ipcMain.handle('ipc-request', async (event, channel, data) => {
     // 通过 IPC 调用 run-nodejs-script
     const { scriptName, ...parameters } = data
     return new Promise((resolve, reject) => {
-      const scriptPath = path.join(__dirname, '../nodejs', `${scriptName}.js`)
+      const scriptPath = path.join(unpackedRoot, 'nodejs', `${scriptName}.js`)
       const processKey = `${scriptName}-${parameters.join('-')}`
       
       if (runningProcesses.has(processKey)) {

+ 1 - 0
electron/preload.js

@@ -5,6 +5,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
     ipcRenderer.invoke('run-nodejs-script', scriptName, ...parameters),
   killNodejsScript: (scriptName, ...parameters) =>
     ipcRenderer.invoke('kill-nodejs-script', scriptName, ...parameters),
+  checkScrcpyRunning: () => ipcRenderer.invoke('check-scrcpy-running'),
   runPythonScript: (scriptName, ...parameters) =>
     ipcRenderer.invoke('run-python-script', scriptName, ...parameters)
 })

+ 2 - 1
lib/scrcpy-adb/scrcpy-console.bat

@@ -1,2 +1,3 @@
 @echo off
-scrcpy.exe --pause-on-exit=if-error %*
+cd /d "%~dp0"
+scrcpy.exe -e --pause-on-exit=if-error %*

+ 9 - 0
lib/scrcpy-adb/scrcpy-noconsole-debug.bat

@@ -0,0 +1,9 @@
+@echo off
+cd /d "%~dp0"
+echo [DEBUG] 当前目录: %CD%
+echo [DEBUG] 启动 scrcpy(带 -e 优先 TCPIP 设备),错误会显示在下方...
+echo.
+scrcpy.exe -e --pause-on-exit=if-error %*
+echo.
+echo [DEBUG] 退出码: %ERRORLEVEL%
+pause

+ 3 - 0
lib/scrcpy-adb/scrcpy-noconsole.bat

@@ -0,0 +1,3 @@
+@echo off
+cd /d "%~dp0"
+start "" scrcpy.exe %*

+ 6 - 2
lib/scrcpy-adb/scrcpy-noconsole.vbs

@@ -1,5 +1,9 @@
-' 在 VBS 所在目录执行 scrcpy.exe,确保能找到 exe 及同目录 dll
-strCommand = "cmd /c cd /d """ & CreateObject("Scripting.FileSystemObject").GetParentFolderName(WScript.ScriptFullName) & """ && scrcpy.exe"
+' 在 VBS 所在目录执行 scrcpy.exe(先 cd 再带完整路径执行),保证双击即可运行且能加载同目录 dll
+Dim fso, scriptDir, exePath, strCommand, Arg
+Set fso = CreateObject("Scripting.FileSystemObject")
+scriptDir = fso.GetParentFolderName(WScript.ScriptFullName)
+exePath = scriptDir & "\scrcpy.exe"
+strCommand = "cmd /c cd /d """ & scriptDir & """ && """ & exePath & """"
 
 For Each Arg In WScript.Arguments
     strCommand = strCommand & " """ & Replace(Arg, """", """") & """"

+ 0 - 53
nodejs/adb/scan-connect-ip.js

@@ -1,53 +0,0 @@
-#!/usr/bin/env node
-const { spawnSync } = require('child_process')
-const path = require('path')
-const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
-
-const projectRoot = path.resolve(__dirname, '..', '..')
-
-/** 从 config 解析并返回 adb 可执行文件路径 */
-function getAdbPath() {
-  return config.adbPath?.path
-    ? path.resolve(projectRoot, config.adbPath.path)
-    : path.join(projectRoot, 'lib', 'scrcpy-adb', 'adb.exe')
-}
-
-/** 判断 adb connect 输出是否表示连接成功 */
-function isConnectSuccess(output) {
-  const s = (output || '').trim()
-  return s.includes('connected') || s.includes('already connected')
-}
-
-/** 对单个 IP 执行 adb connect,返回是否成功(不抛错,失败返回 false) */
-function tryConnectOne(adbPath, ip, port) {
-  const quotedAdb = adbPath.includes(' ') ? `"${adbPath}"` : adbPath
-  const cmd = `${quotedAdb} connect ${ip}:${port}`
-  const shell = process.platform === 'win32' ? 'cmd' : 'sh'
-  const shellArg = process.platform === 'win32' ? '/c' : '-c'
-  const r = spawnSync(shell, [shellArg, cmd], { encoding: 'utf-8', timeout: 500 })
-  return r.status === 0 && isConnectSuccess(r.stdout)
-}
-
-/** 扫描网段 basePrefix.1~.255,返回连接成功的 IP 列表 */
-function scanSubnet(adbPath, basePrefix, port) {
-  const connected = []
-  for (let i = 1; i <= 255; i++) {
-    const ip = `${basePrefix}.${i}`
-    if (tryConnectOne(adbPath, ip, port)) {
-      connected.push(ip)
-    }
-  }
-  return connected
-}
-
-const basePrefix = process.argv[2]
-const port = process.argv[3] || '5555'
-
-if (!basePrefix || !/^\d+\.\d+\.\d+$/.test(basePrefix)) {
-  process.exit(1)
-}
-
-const adbPath = getAdbPath()
-const connected = scanSubnet(adbPath, basePrefix, port)
-process.stdout.write(connected.join('\n') + (connected.length ? '\n' : ''))
-process.exit(0)

+ 39 - 49
nodejs/adb/screenshot.js

@@ -4,32 +4,19 @@ const path = require('path')
 const fs = require('fs')
 
 const config = require(path.join(__dirname, '..', '..', 'configs', 'config.js'))
-const projectRoot = path.resolve(__dirname, '..', '..')
+let projectRoot = path.resolve(__dirname, '..', '..')
+if (projectRoot.includes('app.asar') && !projectRoot.includes('app.asar.unpacked')) {
+  projectRoot = projectRoot.replace('app.asar', 'app.asar.unpacked')
+}
 const adbPath = path.resolve(projectRoot, config.adbPath.path)
 const scrcpyDir = path.dirname(adbPath)
 const scrcpyPath = path.join(scrcpyDir, 'scrcpy.exe')
-const scrcpyVbs = path.join(scrcpyDir, 'scrcpy-noconsole.vbs')
-const stopBat = path.join(scrcpyDir, 'stop.bat')
 const pidFile = path.join(projectRoot, 'static', 'scrcpy-pid.json')
 
 const action = process.argv[2]
 const pidArg = process.argv[3]
 
-// 获取 scrcpy 进程 PID
-function getScrcpyPid() {
-  const output = execSync(`tasklist /FI "IMAGENAME eq scrcpy.exe" /FO CSV`, { encoding: 'utf-8' })
-  const lines = output.split('\n').filter(line => line.includes('scrcpy.exe'))
-  if (lines.length === 0) {
-    return null
-  }
-  const pidMatch = lines[0].match(/"(\d+)"/)
-  if (!pidMatch) {
-    return null
-  }
-  return parseInt(pidMatch[1])
-}
-
-// 检查并终止指定 PID 的进程
+/** 终止指定 PID 的进程 */
 function killPidIfRunning(pid) {
   spawn('taskkill.exe', ['/F', '/PID', pid.toString()], {
     stdio: 'ignore',
@@ -37,6 +24,15 @@ function killPidIfRunning(pid) {
   }).unref()
 }
 
+/** 从 tasklist 获取 scrcpy.exe 的 PID(用 bat start 启动后需轮询得到 pid) */
+function getScrcpyPid() {
+  const output = execSync('tasklist /FI "IMAGENAME eq scrcpy.exe" /FO CSV', { encoding: 'utf-8' })
+  const lines = output.split('\n').filter(line => line.includes('scrcpy.exe'))
+  if (lines.length === 0) return null
+  const pidMatch = lines[0].match(/"(\d+)"/)
+  return pidMatch ? parseInt(pidMatch[1]) : null
+}
+
 /** 判断是否为 IP(或 IP:port),用于无线设备选择器 */
 function isDeviceIp(val) {
   return val && !/^\d+$/.test(val) && /[\d.]/.test(val)
@@ -47,10 +43,12 @@ function toDeviceSelector(ipOrSelector) {
   return ipOrSelector.includes(':') ? ipOrSelector : `${ipOrSelector}:5555`
 }
 
-// 启动 scrcpy:若传入 IP 则先 adb connect 再以该设备启动 lib/scrcpy-adb/scrcpy-noconsole.vbs
+/** 启动 scrcpy:先 adb connect(若有 IP),再用 scrcpy-noconsole.bat(start "" scrcpy.exe %*)无控制台启动 */
 function startScrcpy() {
-  const targetPid = pidArg && /^\d+$/.test(pidArg) ? parseInt(pidArg) : 7788
-  killPidIfRunning(targetPid)
+  if (fs.existsSync(pidFile)) {
+    const { pid } = JSON.parse(fs.readFileSync(pidFile, 'utf-8'))
+    killPidIfRunning(pid)
+  }
 
   let deviceSelector = ''
   if (isDeviceIp(pidArg)) {
@@ -58,45 +56,37 @@ function startScrcpy() {
     execSync(`"${adbPath}" connect ${deviceSelector}`, { encoding: 'utf-8', cwd: scrcpyDir })
   }
 
-  const vbsArgs = deviceSelector ? ` "${deviceSelector}"` : (pidArg && !/^\d+$/.test(pidArg) ? ` "${pidArg}"` : '')
-  execSync(`wscript.exe "${scrcpyVbs}"${vbsArgs}`, {
-    cwd: scrcpyDir,
-    stdio: 'ignore'
-  })
-  
-  const sleep = (ms) => {
-    const start = Date.now()
-    while (Date.now() - start < ms) {}
-  }
-  sleep(2000)
-  
+  const serial = deviceSelector || (pidArg && !/^\d+$/.test(pidArg) ? pidArg : '')
+  const args = serial
+    ? ['--pause-on-exit=if-error', '-s', serial]
+    : ['-e', '--pause-on-exit=if-error']
+  const argsStr = args.join(' ')
+  const vbsPath = path.join(scrcpyDir, 'scrcpy-noconsole.vbs')
+  execSync(`wscript "${vbsPath.replace(/"/g, '""')}" ${argsStr}`, { cwd: scrcpyDir })
+
+  const sleep = (ms) => { const t = Date.now(); while (Date.now() - t < ms) {} }
+  sleep(1500)
   const runningPid = getScrcpyPid()
   if (!runningPid) {
-    console.log(JSON.stringify({ 
-      success: false, 
-      error: 'Failed to start scrcpy'
-    }))
+    console.log(JSON.stringify({ success: false, error: 'scrcpy did not appear in tasklist' }))
     process.exit(1)
-    return
   }
-  
   fs.writeFileSync(pidFile, JSON.stringify({ pid: runningPid }), 'utf-8')
-  console.log(JSON.stringify({ 
-    success: true, 
-    pid: runningPid
-  }))
+  console.log(JSON.stringify({ success: true, pid: runningPid }))
   process.exit(0)
 }
 
-// 停止 scrcpy
+/** 停止 scrcpy:有 pid 则 taskkill 并删文件,返回 action: 'stop';无 pid 文件返回 action: 'none',供前端区分是否继续执行 start */
 function stopScrcpy() {
+  if (!fs.existsSync(pidFile)) {
+    console.log(JSON.stringify({ success: true, action: 'none' }))
+    process.exit(0)
+    return
+  }
   const pid = JSON.parse(fs.readFileSync(pidFile, 'utf-8')).pid
-  execSync(`"${stopBat}" ${pid}`, { stdio: 'ignore', cwd: scrcpyDir })
+  killPidIfRunning(pid)
   fs.unlinkSync(pidFile)
-
-  console.log(JSON.stringify({ 
-    success: true 
-  }))
+  console.log(JSON.stringify({ success: true, action: 'stop' }))
   process.exit(0)
 }
 

+ 6 - 45
src/page/device/connect-item/connect-item.js

@@ -2,6 +2,7 @@ class ConnectItemClass {
     constructor() {
     }
 
+    /** 初始化连接项:IP、预览状态与回调 */
     async init(ipAddress, isPreviewing, setIsPreviewing, onPreview, onRemove) {
         this.ipAddress = ipAddress
         this.is_previewing = isPreviewing || false
@@ -10,63 +11,23 @@ class ConnectItemClass {
         this.onPreviewCallback = onPreview
     }
 
+    /** 点击预览:调用 triggerPreview,按结果更新预览状态并回调 */
     async onPreview() {
-        let result
-        try {
-            result = await this.triggerPreview()
-        } catch (err) {
-            console.warn('[ConnectItem] onPreview error:', err)
-            this.setIsPreviewing?.(false)
-            return
-        }
-        if (!result.success) {
-            return
-        }
-
-        switch (result.action) {
-            case 'start':
-                this.is_previewing = true
-                this.setIsPreviewing(true)
-                break
-            case 'stop':
-                this.is_previewing = false
-                this.setIsPreviewing(false)
-                break
-        }
-        this.onPreviewCallback?.(this.ipAddress)
+        const result = await this.triggerPreview()
     }
 
+    /** 点击移除:调用 remove 后执行 onRemove 回调 */
     async onRemove() {
         await this.remove()
         this.onRemoveCallback(this.ipAddress)
     }
 
-    // 触发 scrcpy 预览(切换启动/停止状态)
-    // 主进程对 adb/screenshot 会提前 resolve,exitCode 可能为 null,需按成功处理
+    /** 触发 scrcpy 预览:先尝试停止,成功则返回 stop;否则启动并返回 start(主进程可能提前 resolve,exitCode 为 null 按成功处理) */
     async triggerPreview() {
-        const stopResult = await window.electronAPI.runNodejsScript('adb/screenshot', 'stop')
-        const stopOk = stopResult.exitCode === 0 || stopResult.exitCode === null
-        if (stopOk && stopResult.stdout) {
-            try {
-                const json = JSON.parse(stopResult.stdout)
-                if (json.success === true) {
-                    return { action: 'stop', success: true }
-                }
-            } catch (_) {}
-        }
-
         const startResult = await window.electronAPI.runNodejsScript('adb/screenshot', 'start', this.ipAddress)
-        const startOk = startResult.exitCode === 0 || startResult.exitCode === null
-        if (startOk && startResult.stdout) {
-            try {
-                const json = JSON.parse(startResult.stdout)
-                return { action: 'start', success: json.success === true }
-            } catch (_) {}
-        }
-
-        return { action: 'start', success: false }
     }
 
+    /** 从 adb 移除该设备,返回是否成功 */
     async remove() {
         const result = await window.electronAPI.runNodejsScript('adb-remove', this.ipAddress)
         return result.exitCode === 0

+ 9 - 0
src/page/device/connect-item/connect-item.jsx

@@ -20,6 +20,15 @@ function ConnectItem({ ipAddress, selected=false, executionStatus = null, onSele
     }
   }, [])
 
+  useEffect(() => {
+    if (!isPreviewing) return
+    const timer = setInterval(async () => {
+      const { running } = await window.electronAPI.checkScrcpyRunning()
+      if (!running) setIsPreviewing(false)
+    }, 2000)
+    return () => clearInterval(timer)
+  }, [isPreviewing])
+
   return (
     <div className="connect-item-container">
 

+ 4 - 0
src/page/device/connect-item/connect-item.scss

@@ -81,14 +81,18 @@ $font-size-scale: 1.5;  // 字体缩放系数,调整此值可改变字体大
         justify-content: space-around;
 
         .preview-btn {
+            min-width: 4.5em;
             width: 60%;
             height: 100%;
+            flex-shrink: 0;
             @include flex-center;
 
             color: #000000;
             font-weight: bold;
             border-radius: 10px;
             @include highlight-btn(#00fb43);
+            white-space: nowrap;
+            line-height: 1.2;
         }    
         
         .status-indicator {

+ 1 - 1
static/scrcpy-pid.json

@@ -1 +1 @@
-{"pid":13232}
+{"pid":9468}