Три способа работать с ADB из кода:
Способы принципиально разные, и от выбора зависит вся архитектура.
Первый: запускать бинарник adb как внешний процесс. Просто и надежно, но на каждый вызов тратится время на запуск процесса (10-50 мс), а вывод приходится парсить как текст.
Второй: общаться с adb-сервером напрямую по его TCP-протоколу на порту 5037 (мы трогали его в главах 4 и 19). Бинарник нужен один раз, чтобы поднять сервер командой
Код: Выделить всё
adb start-serverТретий: реализовать сам протокол ADB и говорить с adbd на устройстве напрямую, вообще без установленного adb. Нужны RSA-ключи для авторизации, зато ноль внешних зависимостей. Так работает adb-shell.
Python, обертки над subprocess:
Базовый и самый живучий вариант. Минимальная обертка, которую я таскаю из проекта в проект:
Код: Выделить всё
import subprocess
def adb(*args, serial=None, timeout=30):
cmd = ["adb"]
if serial:
cmd += ["-s", serial]
cmd += list(args)
res = subprocess.run(cmd, capture_output=True, text=True,
encoding="utf-8", errors="replace",
timeout=timeout)
if res.returncode != 0:
raise RuntimeError(f"{' '.join(cmd)}: {res.stderr.strip()}")
return res.stdout
print(adb("shell", "getprop", "ro.build.version.release",
serial="R58M42ABCDE").strip())
Код: Выделить всё
15Код: Выделить всё
encoding="utf-8"Код: Выделить всё
shell=TrueКод: Выделить всё
adb shellБинарные данные тяните через exec-out, а не shell:
Код: Выделить всё
png = subprocess.run(["adb", "exec-out", "screencap", "-p"],
capture_output=True, timeout=30).stdout
open("screen.png", "wb").write(png)
Библиотека ppadb говорит с adb-сервером по сокету 5037:
Код: Выделить всё
pip install pure-python-adbКод: Выделить всё
from ppadb.client import Client as AdbClient
client = AdbClient(host="127.0.0.1", port=5037)
print(client.version()) # 41, версия протокола сервера
for device in client.devices():
print(device.serial, device.shell("getprop ro.product.model").strip())
device = client.device("R58M42ABCDE")
device.install("app-debug.apk")
with open("screen.png", "wb") as f:
f.write(device.screencap())
Код: Выделить всё
41
R58M42ABCDE SM-A546E
emulator-5554 sdk_gphone64_x86_64
Код: Выделить всё
adb start-serverКод: Выделить всё
import adbutils
d = adbutils.adb.device() # первое устройство в списке
print(d.shell("getprop ro.product.model"))
Эта библиотека реализует протокол ADB сама и подключается к adbd напрямую, без бинарника и сервера. На ней построена интеграция Android TV в Home Assistant. Для авторизации нужны те же RSA-ключи, что описаны в главе 3, проще всего переиспользовать ключ хоста:
Код: Выделить всё
pip install adb-shellКод: Выделить всё
import os
from adb_shell.adb_device import AdbDeviceTcp
from adb_shell.auth.sign_pythonrsa import PythonRSASigner
key = os.path.expanduser("~/.android/adbkey")
with open(key) as f:
priv = f.read()
with open(key + ".pub") as f:
pub = f.read()
signer = PythonRSASigner(pub, priv)
device = AdbDeviceTcp("192.168.1.42", 5555, default_transport_timeout_s=9.0)
device.connect(rsa_keys=[signer], auth_timeout_s=10.0)
print(device.shell("getprop ro.build.version.sdk").strip()) # 35
device.push("config.json", "/sdcard/Download/config.json")
device.close()
Код: Выделить всё
adb tcpip 5555Код: Выделить всё
pip install adb-shell[usb]JavaScript и Node.js, adbkit:
Оригинальный пакет adbkit от OpenSTF заброшен, живой форк поддерживает команда DeviceFarmer, ставьте именно его:
Код: Выделить всё
npm install @devicefarmer/adbkitКод: Выделить всё
const { Adb } = require('@devicefarmer/adbkit');
const client = Adb.createClient(); // сервер на 127.0.0.1:5037
async function main() {
const devices = await client.listDevices();
for (const d of devices) {
const device = client.getDevice(d.id);
const stream = await device.shell('getprop ro.build.version.release');
const out = await Adb.util.readAll(stream);
console.log(`${d.id}: Android ${out.toString().trim()}`);
}
}
main();
Код: Выделить всё
R58M42ABCDE: Android 15
emulator-5554: Android 14
Код: Выделить всё
const tracker = await client.trackDevices();
tracker.on('add', d => console.log('подключено:', d.id));
tracker.on('remove', d => console.log('отвалилось:', d.id));
Код: Выделить всё
const device = client.getDevice('R58M42ABCDE');
await device.install('app-debug.apk');
const png = await device.screencap();
png.pipe(require('fs').createWriteStream('screen.png'));
Десктопные утилиты для Android (GUI-обертки над scrcpy, менеджеры прошивок) часто пишут на Electron. Правила простые. Весь код с adb живет в главном процессе, рендерер получает данные через IPC, включать nodeIntegration ради adb нельзя. Бинарники platform-tools под каждую ОС кладут в extraResources сборщика (electron-builder) и ищут относительно ресурсов приложения:
Код: Выделить всё
const { app } = require('electron');
const { execFile } = require('child_process');
const path = require('path');
const adbPath = app.isPackaged
? path.join(process.resourcesPath, 'platform-tools', 'adb')
: 'adb';
function adbDevices() {
return new Promise((resolve, reject) => {
execFile(adbPath, ['devices', '-l'], (err, stdout) =>
err ? reject(err) : resolve(stdout));
});
}
Код: Выделить всё
adb kill-serverJava и Kotlin, внешний процесс:
В JVM-мире штатный путь, ProcessBuilder. Главная ловушка: если не вычитывать stdout, процесс встанет насмерть, когда заполнится пайп (около 64 КБ). Поэтому сначала читаем поток до конца, потом waitFor:
Код: Выделить всё
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
public class AdbRunner {
public static String run(String... args) throws Exception {
String[] cmd = new String[args.length + 1];
cmd[0] = "adb";
System.arraycopy(args, 0, cmd, 1, args.length);
ProcessBuilder pb = new ProcessBuilder(cmd);
pb.redirectErrorStream(true);
Process p = pb.start();
String out = new String(p.getInputStream().readAllBytes(),
StandardCharsets.UTF_8);
if (!p.waitFor(30, TimeUnit.SECONDS)) {
p.destroyForcibly();
throw new RuntimeException("adb не ответил за 30 секунд");
}
return out;
}
}
Код: Выделить всё
import java.util.concurrent.TimeUnit
fun adb(vararg args: String, timeoutSec: Long = 30): String {
val proc = ProcessBuilder("adb", *args)
.redirectErrorStream(true)
.start()
val out = proc.inputStream.bufferedReader(Charsets.UTF_8).readText()
if (!proc.waitFor(timeoutSec, TimeUnit.SECONDS)) {
proc.destroyForcibly()
error("adb не ответил за $timeoutSec секунд")
}
return out.trim()
}
fun main() {
println(adb("-s", "R58M42ABCDE", "shell", "dumpsys", "battery"))
}
REST-обертки и фермы устройств:
Когда устройства висят на отдельной машине (а в командах СНГ это обычно сервер в офисе с USB-хабом), удобно поднять над ними HTTP API. Самодельная обертка на Flask с жестким белым списком действий:
Код: Выделить всё
from flask import Flask, jsonify, abort
import subprocess
app = Flask(__name__)
ALLOWED = {
"battery": ["shell", "dumpsys", "battery"],
"packages": ["shell", "pm", "list", "packages", "-3"],
}
@app.route("/device/<serial>/<action>")
def run_action(serial, action):
if action not in ALLOWED:
abort(404)
res = subprocess.run(["adb", "-s", serial] + ALLOWED[action],
capture_output=True, text=True, timeout=30)
return jsonify(stdout=res.stdout, code=res.returncode)
Для ферм посерьезнее есть STF (Smartphone Test Farm). Оригинальный OpenSTF заброшен, развивается форк DeviceFarmer/stf, разворачивается через Docker и дает веб-интерфейс с управлением устройством из браузера плюс REST API. Токен берется в настройках профиля (Keys, Access Tokens). Типовой цикл аренды устройства:
Код: Выделить всё
curl -s -H "Authorization: Bearer $STF_TOKEN" \
https://stf.lan/api/v1/devices | jq '.devices[] | {serial, model, present}'
curl -s -X POST -H "Authorization: Bearer $STF_TOKEN" \
-H "Content-Type: application/json" \
-d '{"serial": "R58M42ABCDE"}' \
https://stf.lan/api/v1/user/devices
curl -s -X POST -H "Authorization: Bearer $STF_TOKEN" \
https://stf.lan/api/v1/user/devices/R58M42ABCDE/remoteConnect
Код: Выделить всё
{"success": true, "remoteConnectUrl": "stf.lan:7404"}Код: Выделить всё
adb connect stf.lan:7404
adb -s stf.lan:7404 shell getprop ro.product.model
Код: Выделить всё
curl -s -X DELETE -H "Authorization: Bearer $STF_TOKEN" \
https://stf.lan/api/v1/user/devices/R58M42ABCDE