@u4/adbkit
Version:
A Typescript client for the Android Debug Bridge.
530 lines • 21.4 kB
JavaScript
import { EventEmitter } from 'node:events';
import { Buffer } from 'node:buffer';
import fs from 'node:fs';
import PromiseDuplex from 'promise-duplex';
import ThirdUtils from '../ThirdUtils.js';
import * as STF from './STFServiceModel.js';
// import * as STFAg from "./STFAgentModel.js";
import protobufjs from 'protobufjs';
import STFServiceBuf from './STFServiceBuf.js';
import Utils from '../../utils.js';
// const debug = Debug('STFService');
const PKG = 'jp.co.cyberagent.stf';
export default class STFService extends EventEmitter {
constructor(client, options = {}) {
super();
this.client = client;
this.on = (event, listener) => super.on(event, listener);
this.off = (event, listener) => super.off(event, listener);
this.once = (event, listener) => super.once(event, listener);
this.emit = (event, ...args) => super.emit(event, ...args);
this._cachedApkPath = '';
/**
* esponce callback hooks
*/
this.responseHook = {};
/**
* request id counter [1..0xFFFFFF]
*/
this.reqCnt = 1;
this.config = {
timeout: 15000,
noInstall: false,
...options,
};
this._maxContact = new Promise((resolve) => this.setMaxContact = resolve);
this._width = new Promise((resolve) => this.setWidth = resolve);
this._height = new Promise((resolve) => this.setHeight = resolve);
this._maxPressure = new Promise((resolve) => this.setMaxPressure = resolve);
}
get maxContact() { return this._maxContact; }
get width() { return this._width; }
get height() { return this._height; }
get maxPressure() { return this._maxPressure; }
/**
* find the APK and install it
*/
async installApk(version) {
const apk = ThirdUtils.getResourcePath(`STFService_${version}.apk`);
try {
await fs.promises.stat(apk);
}
catch (e) {
throw Error(`can not find APK bin/STFService_${version}.apk Err: ${JSON.stringify(e)}`);
}
this._cachedApkPath = '';
return this.client.install(apk);
}
/**
*
* @returns get agent setup path
*/
async getApkPath() {
if (this._cachedApkPath)
return this._cachedApkPath;
/**
* locate the installed apk file
*/
let setupPath = (await this.client.execOut(`pm path ${PKG}`, 'utf8')).trim();
if (!setupPath.startsWith('package:')) {
return '';
// throw new Error(`Failed to find ${PKG} package path`);
}
setupPath = setupPath.substring(8);
this._cachedApkPath = setupPath;
return setupPath;
}
/**
* get the current installed Agent version number
* @returns 'MISSING' if not installed, 'OK' if expected, 'MISMATCH' if version differ
*/
async checkVersion(version) {
const setupPath = await this.getApkPath();
const getVersion = `export CLASSPATH='${setupPath}'; exec app_process /system/bin '${PKG}.Agent' --version 2>/dev/null`;
const currentVersion = await this.client.execOut(getVersion, 'utf8');
if (!currentVersion)
return 'MISSING';
if (currentVersion.trim() !== version) {
return 'MISMATCH';
}
return 'OK';
}
/**
* start agent
*/
async startAgent() {
const setupPath = await this.getApkPath();
const startAgent = `export CLASSPATH='${setupPath}'; exec app_process /system/bin '${PKG}.Agent' 2>&1`;
const agentProcess = new PromiseDuplex(await this.client.exec(startAgent));
const result = await Utils.waitforText(agentProcess, /|Address already in use/, 10000);
if (result.includes("@stfagent"))
return; // started
// console.log(`${this.client.serial} stfagent already running`);
// debug only
// ThirdUtils.dumpReadable(agentProcess, 'STFagent');
}
/**
* start long running service and keep the duplex opened
*/
async startService() {
const props = await this.client.getProperties();
const action = `${PKG}.ACTION_START`;
const component = `${PKG}/.Service`;
const sdkLevel = parseInt(props['ro.build.version.sdk']);
const startServiceCmd = (sdkLevel < 26) ? 'startservice' : 'start-foreground-service';
const duplex = new PromiseDuplex(await this.client.shell(`am ${startServiceCmd} --user 0 -a '${action}' -n '${component}'`));
await Utils.waitforReadable(duplex);
const msg = await duplex.setEncoding('utf8').readAll();
if (msg.includes('Error')) {
throw Error(msg.trim());
}
}
/**
* uninstall the service
*/
async uninstall() {
await this.client.uninstall(PKG);
await this.client.execOut('rm -f /data/local/tmp/minicap /data/local/tmp/minicap.so /data/local/tmp/minitouch /data/local/tmp/minirev');
return true;
}
async start() {
this.protoSrv = await STFServiceBuf.get();
if (!this.config.noInstall) {
const versionStatus = await this.checkVersion('2.4.9');
if (versionStatus === 'MISMATCH') {
await this.uninstall();
}
if (versionStatus !== 'OK') {
await this.installApk('2.4.9');
}
}
await this.startService();
await this.startAgent();
this.servicesSocket = await this.client.openLocal2('localabstract:stfservice');
this.servicesSocket.once('close').then(() => this.stop());
void this.startServiceStream().catch((e) => { console.log('Service failed', e); this.stop(); });
return this;
}
/**
* get minitouch duplex, if not connected open connexion
*/
async getMinitouchSocket() {
if (this._minitouchagent)
return this._minitouchagent;
this._minitouchagent = this.client.openLocal2('localabstract:minitouchagent');
const socket = await this._minitouchagent;
socket.once('close').then(() => {
this._minitouchagent = undefined;
// console.log('getMinitouchSocket just closed');
});
void this.startMinitouchStream(socket).catch(() => { socket.destroy(); });
return socket;
}
async getAgentSocket() {
if (this._agentSocket)
return this._agentSocket;
this._agentSocket = this.client.openLocal2('localabstract:stfagent');
const socket = await this._agentSocket;
socket.once('close').then(() => {
this._agentSocket = undefined;
// console.log('agentSocket just closed');
});
void this.startAgentStream(socket).catch(() => { socket.destroy(); });
return this._agentSocket;
}
async startServiceStream() {
let buffer = null;
for (;;) {
await Utils.waitforReadable(this.servicesSocket);
if (!this.servicesSocket)
return;
const next = await this.servicesSocket.read();
if (!next)
continue;
if (buffer) {
buffer = Buffer.concat([buffer, next]);
}
else {
buffer = next;
}
while (buffer) {
const reader = protobufjs.Reader.create(buffer);
const envelopLen = reader.uint32();
const bufLen = envelopLen + reader.pos;
// need mode data to complet envelop
if (buffer.length < envelopLen)
break;
let chunk;
if (bufLen === buffer.length) {
// chunk len match Envelop len should speedup parsing, depending on nodeJS internal Buffer implementation, need to check Buffer.subarray implementation
chunk = buffer.subarray(reader.pos);
buffer = null;
}
else {
chunk = buffer.subarray(reader.pos, bufLen);
buffer = buffer.subarray(bufLen);
}
try {
const eventObj = this.protoSrv.readEnvelope(chunk);
const { id, message } = eventObj;
if (id) {
const resolv = this.responseHook[id];
if (resolv) {
delete this.responseHook[id];
resolv(message);
}
else {
console.error(`STFService RCV response to unknown QueryId:${id} Type:${eventObj.type}`);
}
continue;
}
switch (eventObj.type) {
case STF.MessageTypeMap.EVENT_AIRPLANE_MODE:
this.emit("airplaneMode", this.protoSrv.read.AirplaneModeEvent(message));
break;
case STF.MessageTypeMap.EVENT_BATTERY:
this.emit("battery", this.protoSrv.read.BatteryEvent(message));
break;
case STF.MessageTypeMap.EVENT_CONNECTIVITY:
this.emit("connectivity", this.protoSrv.read.ConnectivityEvent(message));
break;
case STF.MessageTypeMap.EVENT_ROTATION:
this.emit("rotation", this.protoSrv.read.RotationEvent(message));
break;
case STF.MessageTypeMap.EVENT_PHONE_STATE:
this.emit("phoneState", this.protoSrv.read.PhoneStateEvent(message));
break;
case STF.MessageTypeMap.EVENT_BROWSER_PACKAGE:
this.emit("browerPackage", this.protoSrv.read.BrowserPackageEvent(message));
break;
default: console.error(`STFService Response Type (${eventObj.type}) is not implemented`);
}
}
catch (e) {
if (chunk)
console.error(chunk.toString('hex'));
console.error(e);
}
}
await Utils.delay(0);
}
}
/**
* RCV banne:
* v 1
* ^ %d %d %d %d DEFAULT_MAX_CONTACTS, width, height, DEFAULT_MAX_PRESSURE;
* @param socket
*/
async startMinitouchStream(socket) {
socket.setEncoding('ascii');
let data = '';
for (;;) {
await Utils.waitforReadable(socket);
const chunk = await socket.read();
if (!chunk)
return;
data = data + chunk;
for (;;) {
const p = data.indexOf('\n');
if (p >= 0)
break;
const line = data.substring(0, p);
data = data.substring(p + 1);
if (line.startsWith('v 1'))
continue;
if (line.startsWith('^')) {
const [, mc, w, h, mp] = line.split(/ /);
this.setMaxContact(Number(mc));
this.setWidth(Number(w));
this.setHeight(Number(h));
this.setMaxPressure(Number(mp));
continue;
}
console.error('minitouchSocket RCV chunk len:', line);
}
await Utils.delay(0);
}
}
async startAgentStream(socket) {
for (;;) {
await Utils.waitforReadable(socket);
const chunk = await socket.read();
if (chunk) {
console.log('agentSocket RCV chunk len:', chunk.length, chunk.toString('hex').substring(0, 80));
}
else {
return;
}
await Utils.delay(0);
}
}
pushService(type, message, requestReader) {
const id = (this.reqCnt + 1) | 0xFFFFFF;
this.reqCnt = id;
const envelope = { type, message: message, id };
let pReject;
const promise = new Promise((resolve, reject) => {
pReject = reject;
this.responseHook[id] = (message) => {
if (requestReader) {
const conv = requestReader(message);
resolve(conv);
}
else {
resolve();
}
};
});
const buf = this.protoSrv.write.Envelope(envelope);
if (!this.servicesSocket)
throw Error('servicesSocket is not open');
this.servicesSocket.write(buf);
const timeout = Utils.delay(this.config.timeout).then(() => {
if (this.responseHook[id]) {
delete this.responseHook[id];
pReject(Error('timeout'));
}
});
Promise.race([promise, timeout]);
return promise;
}
/**
* Generic method to push message to agent
*/
async pushAgent(type, message) {
const envelope = { type, message: message };
// const buf = this.protoAgent.write.Envelope(envelope)
const buf = this.protoSrv.write.Envelope(envelope);
const socket = await this.getAgentSocket();
return socket.write(buf);
}
////////////////////////////
// public methods
getAccounts(type) {
const message = this.protoSrv.write.GetAccountsRequest({ type });
return this.pushService(STF.MessageTypeMap.GET_ACCOUNTS, message, this.protoSrv.read.GetAccountsResponse);
}
getBrowsers(req = {}) {
const message = this.protoSrv.write.GetBrowsersRequest(req);
return this.pushService(STF.MessageTypeMap.GET_BROWSERS, message, this.protoSrv.read.GetBrowsersResponse);
}
getClipboard(type = STF.ClipboardTypeMap.TEXT) {
const message = this.protoSrv.write.GetClipboardRequest({ type });
return this.pushService(STF.MessageTypeMap.GET_CLIPBOARD, message, this.protoSrv.read.GetClipboardResponse);
}
getDisplay(id = 0) {
const message = this.protoSrv.write.GetDisplayRequest({ id });
return this.pushService(STF.MessageTypeMap.GET_DISPLAY, message, this.protoSrv.read.GetDisplayResponse);
}
getProperties(properties) {
const message = this.protoSrv.write.GetPropertiesRequest({ properties });
return this.pushService(STF.MessageTypeMap.GET_PROPERTIES, message, this.protoSrv.read.GetPropertiesResponse);
}
getRingerMode(req = {}) {
const message = this.protoSrv.write.GetRingerModeRequest(req);
return this.pushService(STF.MessageTypeMap.GET_RINGER_MODE, message, this.protoSrv.read.GetRingerModeResponse);
}
getSdStatus(req = {}) {
const message = this.protoSrv.write.GetSdStatusRequest(req);
return this.pushService(STF.MessageTypeMap.GET_SD_STATUS, message, this.protoSrv.read.GetSdStatusResponse);
}
// invalid response send by the service
// public async getVersion(): Promise<STF.GetVersionResponse> {
// const message = this.proto.write.GetVersionRequest();
// return this.pushEnvelop<STF.GetVersionResponse>(STF.MessageType.GET_VERSION, message })
// }
getWifiStatus(req = {}) {
const message = this.protoSrv.write.GetWifiStatusRequest(req);
return this.pushService(STF.MessageTypeMap.GET_WIFI_STATUS, message, this.protoSrv.read.GetWifiStatusResponse);
}
getBluetoothStatus(req = {}) {
const message = this.protoSrv.write.GetBluetoothStatusRequest(req);
return this.pushService(STF.MessageTypeMap.GET_BLUETOOTH_STATUS, message, this.protoSrv.read.GetBluetoothStatusResponse);
}
getRootStatus(req = {}) {
const message = this.protoSrv.write.GetRootStatusRequest(req);
return this.pushService(STF.MessageTypeMap.GET_ROOT_STATUS, message, this.protoSrv.read.GetRootStatusResponse);
}
setClipboard(req) {
const message = this.protoSrv.write.SetClipboardRequest(req);
return this.pushService(STF.MessageTypeMap.SET_CLIPBOARD, message, this.protoSrv.read.SetClipboardResponse);
}
setKeyguardState(req) {
const message = this.protoSrv.write.SetKeyguardStateRequest(req);
return this.pushService(STF.MessageTypeMap.SET_KEYGUARD_STATE, message, this.protoSrv.read.SetKeyguardStateResponse);
}
setRingerMode(req) {
const message = this.protoSrv.write.SetRingerModeRequest(req);
return this.pushService(STF.MessageTypeMap.SET_RINGER_MODE, message, this.protoSrv.read.SetRingerModeResponse);
}
setRotationRequest(req) {
const message = this.protoSrv.write.SetRotationRequest(req);
return this.pushService(STF.MessageTypeMap.SET_ROTATION, message);
}
setWakeLock(req) {
const message = this.protoSrv.write.SetWakeLockRequest(req);
return this.pushService(STF.MessageTypeMap.SET_WAKE_LOCK, message, this.protoSrv.read.GetWifiStatusResponse);
}
setWifiEnabledRequest(req) {
const message = this.protoSrv.write.SetWifiEnabledRequest(req);
return this.pushService(STF.MessageTypeMap.SET_WIFI_ENABLED, message, this.protoSrv.read.SetWifiEnabledResponse);
}
setBluetoothEnabledRequest(req) {
const message = this.protoSrv.write.SetBluetoothEnabledRequest(req);
return this.pushService(STF.MessageTypeMap.SET_BLUETOOTH_ENABLED, message, this.protoSrv.read.SetBluetoothEnabledResponse);
}
setMasterMute(req) {
const message = this.protoSrv.write.SetMasterMuteRequest(req);
const ret = this.pushService(STF.MessageTypeMap.SET_MASTER_MUTE, message, this.protoSrv.read.SetMasterMuteResponse);
return ret;
}
// Agents
doKeyEvent(req) {
const message = this.protoSrv.write.KeyEventRequest(req);
return this.pushAgent(STF.MessageTypeMap.DO_KEYEVENT, message);
}
doType(req) {
const message = this.protoSrv.write.DoTypeRequest(req);
return this.pushAgent(STF.MessageTypeMap.DO_TYPE, message);
}
doWake(req) {
const message = this.protoSrv.write.DoWakeRequest(req);
return this.pushAgent(STF.MessageTypeMap.DO_WAKE, message);
}
setRotation(req) {
const message = this.protoSrv.write.SetRotationRequest(req);
return this.pushAgent(STF.MessageTypeMap.SET_ROTATION, message);
}
/**
* Send commit minitouch events
*/
async commit() {
const cmd = `c\n`;
const s = await this.getMinitouchSocket();
return s.write(cmd, 'ascii');
}
/**
* Send move minitouch events
*/
async move(x, y, contact = 0, pressure = 0) {
const cmd = `m ${contact | 0} ${x | 0} ${y | 0} ${pressure | 0}\n`;
const s = await this.getMinitouchSocket();
return s.write(cmd, 'ascii');
}
/**
* Send press down minitouch events
*/
async down(x, y, contact = 0, pressure = 0) {
const cmd = `d ${contact} ${x | 0} ${y | 0} ${pressure | 0}\n`;
const s = await this.getMinitouchSocket();
return s.write(cmd, 'ascii');
}
/**
* Send press up minitouch events
*/
async up(contact = 0) {
const cmd = `u ${contact | 0}\n`;
const s = await this.getMinitouchSocket();
return s.write(cmd, 'ascii');
}
/**
* Send move + commit minitouch events
*/
async moveCommit(x, y, contact = 0, pressure = 0) {
const cmd = `m ${contact | 0} ${x | 0} ${y | 0} ${pressure | 0}\nc\n`;
const s = await this.getMinitouchSocket();
return s.write(cmd, 'ascii');
}
/**
* Send press down + commit minitouch events
*/
async downCommit(x, y, contact = 0, pressure = 0) {
const cmd = `d ${contact} ${x | 0} ${y | 0} ${pressure | 0}\nc\n`;
const s = await this.getMinitouchSocket();
return s.write(cmd, 'ascii');
}
/**
* Send press up + commit minitouch events
*/
async upCommit(contact = 0) {
const cmd = `u ${contact | 0}\nc\n`;
const s = await this.getMinitouchSocket();
return s.write(cmd, 'ascii');
}
/**
* Send wait instruction minitouch events
*/
async wait(time) {
const cmd = `w ${time}\n`;
const s = await this.getMinitouchSocket();
return s.write(cmd, 'ascii');
}
/**
* stop the service
*/
stop() {
let close = false;
if (this.servicesSocket) {
this.servicesSocket.destroy();
this.servicesSocket = undefined;
close = true;
}
if (this._agentSocket) {
this._agentSocket.then(a => a.destroy());
this._agentSocket = undefined;
close = true;
}
if (this._minitouchagent) {
this._minitouchagent.then(a => a.destroy());
this._minitouchagent = undefined;
close = true;
}
if (close)
this.emit('disconnect');
return close;
}
isRunning() {
return this.servicesSocket !== null;
}
}
//# sourceMappingURL=STFService.js.map