sandbox_device
為箱子租一台 Android 模擬器或 iPhone 模擬器,顯示在箱子畫面上;也能列出箱子的裝置,或還掉一台。
| 輸入 | 輸出 |
|---|---|
id、action(attach、list、release、reconnect 或 restart)、platform(android 或 ios)、model、osVersion、deviceId |
裝置的 deviceId、在箱子裡的 serial、model、osVersion、status;attach 另外回 bootTimings 與 egressIp;list 回這個箱子的 devices 與裝置主機的 hosts;reconnect 回那台裝置並附 reconnected: true,restart 附 restarted: true |
attach 替箱子租一支跑在另外機器上的手機,等它開機完成才回:platform: "android" 是 Android 模擬器,platform: "ios" 是 Mac 上的 iPhone 模擬器。畫面在箱子的虛擬螢幕上,所以 sandbox_shot、錄影、接手與 box 的 sandbox_review 都看得到,人也能在那裡點、打字。一個箱子最多同時接 4 台。release 還一台;停掉箱子會全部還回去。裝置開著的期間按分鐘計費。手機跟著箱子凍結與恢復(箱子狀態):箱子 10 分鐘沒有使用時,先凍手機、再凍箱子;凍著的手機不佔裝置格、不計費。Android 模擬器連記憶體一起存下來,開回來就在原本的畫面;iPhone 模擬器是關機,裝好的 App、資料與 Mac 上的 /work 都留著,App 會重新啟動。恢復箱子的那次呼叫會先在同一台主機把手機開回來,接回同一個 serial 再執行(Android 約 35 秒、iPhone 約 50 秒,兩支同時開回來)。那台主機當下沒有空格時,手機照舊凍著(list 顯示 status: "frozen"),箱子還有人在用(10 分鐘內有呼叫)時一有空格就會接回來;release 會丟掉它。list 不會喚醒凍結的箱子。box 的交件在等人時也不會讓它留著:手機照一般閒置規則跟箱子一起凍結、一起回收,人打開交件時會把它跟箱子一起叫醒。那個平台的裝置全被租走時,attach 會失敗並回 all N <platform> device slots are in use; try again later or release a device with sandbox_device release。裝置主機沒有的 osVersion 會在開機前就回 400:osVersion <v> is not available: the <platform> device host has <version>; attach without osVersion to use it,不會默默換成別的版本。等開機的時候,進度訊息會說手機開到哪一步(iPhone 是建租約使用者、建模擬器、第一次開機、預熱 Safari),開好後結果的 bootTimings 列出每一步花了多久。結果的 egressIp 是手機自己上網的出口 IP:跟箱子的出口不同,IP 白名單與地區判斷要對它看(手機裡沒有 curl,在箱子裡 curl 拿到的是箱子的 IP)。
list 回 { "devices": [...], "hosts": [...] }。devices 是這個箱子的手機與它們的 status(ready、frozen、offline、restarting 之類),不是單純 ready 的附一句 note;最近一小時內壞掉的也會列出,status: "failed" 並附 reason(模擬器一直當掉就會這樣;請重新 attach 一台)。hosts 說每個平台有沒有裝置主機 online、還有幾個 freeSlots、開得出哪些 osVersions(osVersion 給其中一個,不給就是預設那一版),以及手機上網的出口 egressIps;一台都不在線時附 lastHeartbeatAt 與 note:那個平台的 attach 會失敗到主機回來為止,這是 ParallelSandbox 這邊的事,不是你的專案的問題,測試計畫先別靠那支手機,或晚點再試。花時間為手機建置之前先看一下;帶 toolchains: ["android"] 開箱時,若沒有 Android 主機在線,sandbox_start 的 next 也會說。
要人試 Android 版、又借不到模擬器時,把 APK 本身交給人裝在自己的手機上。sandbox_review 沒有檔案的入口,所以當成網頁交:把 APK 放進一個資料夾,旁邊放一個連到它的 index.html(<a href="app-debug.apk" download>安裝</a>),在背景起一個伺服器(cd /work/apk && python3 -m http.server 8090,background: true),再用 open: "web"、port: 8090 呼叫 sandbox_review,在 what 寫明用 Android 手機打開、下載 APK、Android 詢問時允許瀏覽器安裝 App。debug 版可以這樣裝;手機上已經有同一個 App 的商店版時,簽章不同,要先解除安裝那一個。
Android 裝置在箱子裡的 adb 連不到時會自己回來。隧道斷了,箱子約 20 秒內發現並重接,沒接回來就每 30 秒再試,直到接回來;模擬器本身當掉、或連著一分鐘不回 adb,裝置主機會在原地把同一台模擬器重開,serial、裝好的 App 與資料都留著(冷開機,約一分鐘),箱子再接回去。這段時間 list 會列成 status: "offline" 並附它的 adb 狀態(箱子自己接回來過的附 autoReconnects),或 status: "restarting";重開過的附 restarts、restartedAt 與 restartReason。sandbox_device { "id": "<id>", "action": "reconnect", "deviceId": "<deviceId>" } 立刻重接,回傳那台裝置並附 reconnected: true。15 分鐘內第四次當掉的模擬器不再重開,會列成 failed:常常是 app 本身把它弄當的,先在它掛掉前看 adb logcat,再 attach 一台新的。
sandbox_device { "id": "<id>", "action": "restart", "deviceId": "<deviceId>" } 在原地重開一支手機,開好才回(restarted: true),serial、裝好的 App 與資料都不變;開著的 App 會重新啟動。Android 模擬器是重開機(adb reboot,箱子接著重接、等開完機、叫醒螢幕);iPhone 模擬器是關機再開機,重開才生效的設定(例如 simctl ui {udid} appearance dark)這時套上,關不掉的系統對話框也一併清掉。這個動作加上之前開的箱子,會回說它的映像不能重開裝置。
Android(約 35 秒):沒給 model 時是 pixel_9 機型的 Android 15(也可以指定,例如 pixel_7)。在箱子裡 adb devices 看得到,名稱就是回傳的 serial(emulator-5554,第二台 emulator-5556);交件或接手時人也能在上面點、打字。
箱子裡有 artemis-adb,是確定性的 ADB 工具,裡面沒有任何模型:hierarchy --out 檔案(畫面上元素的 JSON 陣列,有 text、content-desc、resource-id、package、parsed_bounds;點元素就點 parsed_bounds 的中心)、tap X Y、long-press X Y、swipe X1 Y1 X2 Y2、type X Y 文字、clear-text X Y、erase、back、key KEYCODE、launch 套件名、stop 套件名、open 網址、screenshot --out 檔案。
adb reverse tcp:埠 tcp:埠 穿得過隧道:手機上的 localhost:埠 就是箱子裡的那個埠,箱子裡的 dev server 可以像 Metro 接 React Native 那樣接到 app。debug 版、或呼叫了 WebView.setWebContentsDebuggingEnabled(true) 的混合式 app(Capacitor、Cordova、Ionic)會把 WebView 開給 Chrome DevTools:psbx-webview <package> [埠] 把那個 app 的 WebView 轉到箱子的 127.0.0.1:<埠>(預設 9222),並印出每一頁的 webSocketDebuggerUrl(只打 psbx-webview 列出有 WebView 可接的 app,psbx-webview chrome 是 Chrome 本身)。用 raw CDP、chrome-remote-interface 或 Puppeteer 的 connect({ browserWSEndpoint: <那個網址> }) 接頁面的網址;Playwright 的 connectOverCDP 接 WebView 會失敗:Browser.setDownloadBehavior: Browser context management is not supported。adb exec-out screencap -p > /work/phone.png 存下手機全解析度畫面。
iOS:裝了 Xcode 26.5 的 Apple 晶片 Mac 上的 iPhone 模擬器。沒給 model 時是 iPhone 17 Pro、iOS 26.5,也可以給其他模擬器機型名稱,例如 "iPhone 17"、"iPhone Air"、"iPhone 17 Pro Max" 或 "iPad Pro 11-inch (M5)"。Mac 上有哪些 iOS 版本看 list 的 hosts[].osVersions;不給 osVersion 就是最新的那一版。iPhone 的 attach 要 1 到 4 分鐘,大部分是新模擬器的第一次開機。serial 是模擬器的 UDID。模擬器屬於只給這次租約用的 macOS 使用者,裝置還掉時連同裡面的東西一起刪掉,Mac 上那份 /work 副本和在那裡建置出來的東西也一起沒了:重新 attach 之後要重新同步、重新建置。
iOS 沒有 adb,Xcode 也只能在 Mac 上跑,所以 app 是在 Mac 那邊建置。箱子的 PATH 上會有 psbx-ios,以及轉給它的 xcodebuild、simctl 包裝程式,用 sandbox_exec 執行。每次呼叫都是一趟到 Mac 的 SSH(第一次約 3 秒,之後不到 1 秒)。箱子本身不需要裝工具鏈,也不需要加大 size。
- 每個
psbx-ios指令都在 Mac 上這次租約的work/資料夾裡跑,那是psbx-ios sync填進去的箱子/work副本,路徑一樣:箱子的/work/app在 Mac 上就是work/app(psbx-ios pwd印出這個資料夾的完整路徑)。所以其他指令的路徑都相對於/work:寫xcodebuild -project app/Hello.xcodeproj ...或-workspace app/ios/App/App.xcworkspace,不要寫/work/app/...這種箱子路徑;箱子裡的cd對那邊沒有作用。 psbx-ios sync [路徑]把箱子裡的這個路徑(預設整個/work;相對路徑從你在箱子裡的目錄算起,/work以外的路徑用它最後一段的名稱)複製到 Mac 上的同一個位置,結尾有沒有斜線都一樣,做完印出 Mac 上的完整路徑,過程中也印進度;大的(例如node_modules)用sandbox_exec加background: true跑。箱子裡已經沒有的東西,Mac 那邊也會刪掉,所以箱子裡刪掉的檔案那邊也會刪掉,只有在 Mac 上建出來或錄下來的不動:任何一層的dd/與DerivedData/資料夾,以及那個路徑最上層的build/、*.mp4、*.mov,都留在 Mac 上,也不會送過去。psbx-ios pull 路徑 [目的地]把 Mac 的work/裡的檔案或資料夾複製回箱子(目的地預設/work/),拿只有 Mac 才做得出來的東西。xcodebuild -project app/Hello.xcodeproj -scheme Hello -destination "platform=iOS Simulator,id={udid}" -derivedDataPath dd CODE_SIGN_IDENTITY=- CODE_SIGN_STYLE=Manual build替租到的模擬器建置(小的 SwiftUI app 約 6 秒;{udid}會換成這台模擬器的 id,寫<serial>也行)。產物留在 Mac 上,這個例子是dd/Build/Products/Debug-iphonesimulator/Hello.app;會回到箱子的只有每個指令的輸出(以及你psbx-ios pull回來的)。在箱子裡ls看不到它,psbx-ios ls dd/Build/Products/Debug-iphonesimulator看得到。CODE_SIGN_IDENTITY=- CODE_SIGN_STYLE=Manual(ad hoc 簽章,不需要憑證)要留著:用CODE_SIGNING_ALLOWED=NO建的 app 跑得起來,但啟動畫面會一直是黑的,因為模擬器不接受沒簽章的 launch storyboard。psbx-ios xcodegen 資料夾 [參數]在 Mac 上的work/資料夾跑 XcodeGen(預設generate),用它的project.yml產生.xcodeproj,新做的 app 不用手寫專案檔:psbx-ios sync app,再psbx-ios xcodegen app。psbx-ios install dd/Build/Products/Debug-iphonesimulator/Hello.app(Mac 上的路徑;給了不存在的路徑,例如箱子的路徑,它會說路徑從 Mac 的work/算起、用psbx-ios ls找)、psbx-ios launch BUNDLE_ID、psbx-ios terminate BUNDLE_ID,以及存下全解析度畫面的psbx-ios screenshot /work/phone.png(或> /work/phone.png)(iPhone 17 Pro 是 1206 × 2622)。BUNDLE_ID是 target 的PRODUCT_BUNDLE_IDENTIFIER:xcodebuild -project app/Hello.xcodeproj -scheme Hello -showBuildSettings | grep PRODUCT_BUNDLE_IDENTIFIER。simctl 參數在 Mac 上跑xcrun simctl 參數,其中的{udid}換成這台模擬器,例如simctl openurl {udid} myapp://orders/42。psbx-ios xcrun 參數可以跑其他 Xcode 工具,例如psbx-ios xcrun --sdk iphonesimulator swiftc ...。psbx-ios boot、psbx-ios shutdown、psbx-ios ls、psbx-ios pwd、下面的輸入指令,以及列出全部的psbx-ios help也可以用;psbx-ios後面接其他字一律以結束碼 126 拒絕,訊息是psbx-lease: 不允許的指令: <那個字>。simctl privacy {udid} grant photos BUNDLE_ID(還有location、contacts、calendar、microphone等simctl privacy列出的服務)可以先答應權限請求;通知不在其中,請在那個對話框點「允許」(psbx-ios tap會等它滑進來停好),或讓測試版不要請求。simctl ui {udid} appearance dark要在裝置restart之後才套到 app 上。- Capacitor 或 React Native app 的 Xcode 專案用相對路徑連到
node_modules裡的套件:要同步同時包含ios/與node_modules/的那個資料夾(psbx-ios sync myapp),不然套件的相依關係解不出來。 - Flutter app 的 iOS 版用 Mac 上的 Flutter 建,版本跟箱子的
flutter工具鏈一樣:psbx-ios sync app之後,psbx-ios flutter app build ios --simulator會在 Mac 的app/裡跑flutter build ios --simulator,再用psbx-ios install app/build/ios/iphonesimulator/Runner.app裝到模擬器。一次租約第一次跑psbx-ios flutter會多花幾秒,替這次租約複製一份 SDK。要 CocoaPods 的插件也可以:Mac 上每個psbx-ios動詞的 PATH 都有 CocoaPods 與 Node。 - React Native、Expo 與 Capacitor 6 以前用 CocoaPods:同步同時包含
ios/與node_modules/的資料夾,跑psbx-ios pod myapp/ios install(在 Mac 的myapp/ios跑 CocoaPods;Podfile 與 Xcode 打包 JavaScript 那一步要的 Node 也在),再用xcodebuild建.xcworkspace。React Native 要用-configuration Release建:debug 版會去 Metro 拿 JavaScript,模擬器連不到箱子裡的 Metro。Expo 先在箱子裡跑npx expo prebuild --platform ios。node_modules同步到 Mac 很慢(剛建好的 React Native app 324 MB 約 4 分半),同步一次之後只同步改過的部分。接著 iPhone 時箱子裡的xcodebuild會轉到 Mac,所以在箱子裡跑npx cap sync ios也會叫到它,並印出App.xcodeproj不存在;網頁資產照樣有複製,之後psbx-ios pod與xcodebuild -workspace都能正常建。 - 不能做發佈用的簽章:Mac 上沒有簽章憑證,App Store 或 TestFlight 的 archive、上傳 App Store Connect 都在使用者自己的 Mac 上做。這台 Mac 也只跑 iOS 模擬器:macOS 桌面 app 在這裡測不了(租約沒有 GUI session)。
- 箱子沒辦法用 Safari 的 Web Inspector 打開 app 的 WebView。要看它印出來的東西,在
background: true的sandbox_exec裡用simctl launch --console --terminate-running-process {udid} BUNDLE_ID啟動 app:app 的 stdout 與 stderr(Capacitor 的每一行console.log都寫在這裡)會進那個指令的 log,直到 app 結束。或是從頁面把訊息送到箱子裡的伺服器。 attach回來之前會先開關一次 Mobile Safari,第一次真的開它時不會卡在黑畫面;頁面本身載得慢時畫面上就只有那一頁,不確定時用psbx-ios screenshot看。Vite dev server 會送出幾百個獨立的模組,Mobile Safari 經箱子網址載它們比 Android 的 Chrome 慢得多(停在啟動畫面 50 秒以上):要在 iPhone 上試網頁 app,先vite build,再提供建好的檔案(vite preview)。
箱子的畫面上是模擬器的即時影像,每秒約 6 到 10 格。交件或接手時人在上面點、滑、打字(打字支援中文與 emoji 等 Unicode 文字)。你也能這樣操作,座標用 point(像素除以倍率;iPhone 17 Pro 是 402 × 874):
psbx-ios tap X Y點一下。它會先等畫面停下來(最多 2 秒),不會點在轉場中、或還在滑進來的系統對話框上,再用跟人點畫面一樣的方式送出觸控,對話框擋不住。psbx-ios swipe X1 Y1 X2 Y2 [秒]滑動,沒給秒數是 0.3 秒。printf 'hello 中文測試🙂' | psbx-ios type把 stdin 的文字打進目前選取的欄位,中文與 emoji 也行:有 ASCII 以外字元的文字會自己改走模擬器裡的 Unicode helper。psbx-ios unicode則只走那個 helper。- 在
tap、swipe、type、unicode、key、button後面加--wait [毫秒],會等畫面連續這麼久(預設 500)沒變才回,接著的sandbox_shot或psbx-ios screenshot拍到的就是做完的樣子,不是前一格(經 Unicode helper 打字時,指令回來時字還沒上畫面):psbx-ios tap 200 400 --wait、printf '你好' | psbx-ios type --wait 800。它會印出等了多久;遇到動畫或影片,多等 5 秒照樣回。psbx-ios settle [毫秒]只等不做。游標閃爍不算變化。 - 按鍵與打字遇到畫面正在轉場會重試,每次之間等畫面停下來;還是失敗時,訊息會說可能被系統對話框(通知權限、Safari 提示)擋住,先
sandbox_shot看一下。 psbx-ios key HID碼按鍵:40 Enter、42 Backspace、43 Tab、41 Escape。psbx-ios button home按實體按鍵:home、lock、side-button、siri、apple-pay。psbx-ios size印出螢幕的像素寬高與倍率(1206 2622 3.000000)。
要到某個畫面,深層連結(simctl openurl)或 UI 測試 target(xcodebuild test)還是比較快;用 sandbox_shot 或 psbx-ios screenshot 確認。要抓很短的變化(例如啟動畫面交接到第一個畫面),psbx-ios record 秒數 檔案 以模擬器原本的影格率錄 1 到 120 秒,存成箱子裡的 mp4(psbx-ios record 5 /work/launch.mp4,再用 ffmpeg -i /work/launch.mp4 -vf fps=20 /work/f_%03d.png 拆成單張);psbx-ios screenshot 一張約 0.7 秒,來不及。sandbox_shot 的 record 錄的是箱子畫面,也就是每秒約 10 格的即時影像。
要給人看原生 app,先自己點過、打過字(Android 用 artemis-adb tap/type,iOS 用 psbx-ios tap/type,再 sandbox_shot),讓它停在起始畫面、在裝置上開著,以 open: "box" 呼叫 sandbox_review。卡片打開箱子的畫面,人在上面自己點、打字試用原生 app。交件不會讓任何東西保持醒著:裝置照一般閒置規則跟箱子一起凍結、一起回收,人打開交件時跟著箱子開回來。
所有工具與主題都列在工具參考。