170 lines
5.2 KiB
TypeScript
170 lines
5.2 KiB
TypeScript
|
|
/**
|
||
|
|
* Copyright Microsoft Corporation. All rights reserved.
|
||
|
|
*
|
||
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||
|
|
* you may not use this file except in compliance with the License.
|
||
|
|
* You may obtain a copy of the License at
|
||
|
|
*
|
||
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||
|
|
*
|
||
|
|
* Unless required by applicable law or agreed to in writing, software
|
||
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
|
|
* See the License for the specific language governing permissions and
|
||
|
|
* limitations under the License.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import * as debug from 'debug';
|
||
|
|
import { EventEmitter } from 'events';
|
||
|
|
import * as stream from 'stream';
|
||
|
|
import * as ws from 'ws';
|
||
|
|
import { makeWaitForNextTask } from '../../utils/utils';
|
||
|
|
|
||
|
|
export interface Backend {
|
||
|
|
devices(): Promise<DeviceBackend[]>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface DeviceBackend {
|
||
|
|
close(): Promise<void>;
|
||
|
|
init(): Promise<void>;
|
||
|
|
runCommand(command: string): Promise<string>;
|
||
|
|
open(command: string): Promise<SocketBackend>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface SocketBackend extends EventEmitter {
|
||
|
|
write(data: Buffer): Promise<void>;
|
||
|
|
close(): Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
export class AndroidClient {
|
||
|
|
backend: Backend;
|
||
|
|
|
||
|
|
constructor(backend: Backend) {
|
||
|
|
this.backend = backend;
|
||
|
|
}
|
||
|
|
|
||
|
|
async devices(): Promise<AndroidDevice[]> {
|
||
|
|
const devices = await this.backend.devices();
|
||
|
|
return devices.map(b => new AndroidDevice(b));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export class AndroidDevice {
|
||
|
|
readonly backend: DeviceBackend;
|
||
|
|
private _model: string | undefined;
|
||
|
|
|
||
|
|
constructor(backend: DeviceBackend) {
|
||
|
|
this.backend = backend;
|
||
|
|
}
|
||
|
|
|
||
|
|
async init() {
|
||
|
|
await this.backend.init();
|
||
|
|
this._model = await this.backend.runCommand('shell:getprop ro.product.model');
|
||
|
|
}
|
||
|
|
|
||
|
|
async close() {
|
||
|
|
await this.backend.close();
|
||
|
|
}
|
||
|
|
|
||
|
|
async launchBrowser(packageName: string): Promise<AndroidBrowser> {
|
||
|
|
debug('pw:android')('Force-stopping', packageName);
|
||
|
|
await this.backend.runCommand(`shell:am force-stop ${packageName}`);
|
||
|
|
const hasDefaultSocket = !!(await this.backend.runCommand(`shell:cat /proc/net/unix | grep chrome_devtools_remote$`));
|
||
|
|
debug('pw:android')('Starting', packageName);
|
||
|
|
await this.backend.runCommand(`shell:am start -n ${packageName}/com.google.android.apps.chrome.Main about:blank`);
|
||
|
|
let pid = 0;
|
||
|
|
debug('pw:android')('Polling pid for', packageName);
|
||
|
|
while (!pid) {
|
||
|
|
const ps = (await this.backend.runCommand(`shell:ps -A | grep ${packageName}`)).split('\n');
|
||
|
|
const proc = ps.find(line => line.endsWith(packageName));
|
||
|
|
if (proc)
|
||
|
|
pid = +proc.replace(/\s+/g, ' ').split(' ')[1];
|
||
|
|
await new Promise(f => setTimeout(f, 100));
|
||
|
|
}
|
||
|
|
debug('pw:android')('PID=' + pid);
|
||
|
|
const socketName = hasDefaultSocket ? `chrome_devtools_remote_${pid}` : 'chrome_devtools_remote';
|
||
|
|
debug('pw:android')('Polling for socket', socketName);
|
||
|
|
while (true) {
|
||
|
|
const net = await this.backend.runCommand(`shell:cat /proc/net/unix | grep ${socketName}$`);
|
||
|
|
if (net)
|
||
|
|
break;
|
||
|
|
await new Promise(f => setTimeout(f, 100));
|
||
|
|
}
|
||
|
|
debug('pw:android')('Got the socket, connecting');
|
||
|
|
const browser = new AndroidBrowser(this, packageName, socketName, pid);
|
||
|
|
await browser._open();
|
||
|
|
return browser;
|
||
|
|
}
|
||
|
|
|
||
|
|
model(): string | undefined {
|
||
|
|
return this._model;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export class AndroidBrowser extends EventEmitter {
|
||
|
|
readonly device: AndroidDevice;
|
||
|
|
readonly socketName: string;
|
||
|
|
readonly pid: number;
|
||
|
|
private _socket: SocketBackend | undefined;
|
||
|
|
private _receiver: stream.Writable;
|
||
|
|
private _waitForNextTask = makeWaitForNextTask();
|
||
|
|
onmessage?: (message: any) => void;
|
||
|
|
onclose?: () => void;
|
||
|
|
private _packageName: string;
|
||
|
|
|
||
|
|
constructor(device: AndroidDevice, packageName: string, socketName: string, pid: number) {
|
||
|
|
super();
|
||
|
|
this._packageName = packageName;
|
||
|
|
this.device = device;
|
||
|
|
this.socketName = socketName;
|
||
|
|
this.pid = pid;
|
||
|
|
this._receiver = new (ws as any).Receiver() as stream.Writable;
|
||
|
|
this._receiver.on('message', message => {
|
||
|
|
this._waitForNextTask(() => {
|
||
|
|
if (this.onmessage)
|
||
|
|
this.onmessage(JSON.parse(message));
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async _open() {
|
||
|
|
this._socket = await this.device.backend.open(`localabstract:${this.socketName}`);
|
||
|
|
this._socket.on('close', () => {
|
||
|
|
this._waitForNextTask(() => {
|
||
|
|
if (this.onclose)
|
||
|
|
this.onclose();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
await this._socket.write(Buffer.from(`GET /devtools/browser HTTP/1.1\r
|
||
|
|
Upgrade: WebSocket\r
|
||
|
|
Connection: Upgrade\r
|
||
|
|
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
|
||
|
|
Sec-WebSocket-Version: 13\r
|
||
|
|
\r
|
||
|
|
`));
|
||
|
|
// HTTP Upgrade response.
|
||
|
|
await new Promise(f => this._socket!.once('data', f));
|
||
|
|
|
||
|
|
// Start sending web frame to receiver.
|
||
|
|
this._socket.on('data', data => this._receiver._write(data, 'binary', () => {}));
|
||
|
|
}
|
||
|
|
|
||
|
|
async send(s: any) {
|
||
|
|
await this._socket!.write(encodeWebFrame(JSON.stringify(s)));
|
||
|
|
}
|
||
|
|
|
||
|
|
async close() {
|
||
|
|
await this._socket!.close();
|
||
|
|
await this.device.backend.runCommand(`shell:am force-stop ${this._packageName}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function encodeWebFrame(data: string): Buffer {
|
||
|
|
return (ws as any).Sender.frame(Buffer.from(data), {
|
||
|
|
opcode: 1,
|
||
|
|
mask: true,
|
||
|
|
fin: true,
|
||
|
|
readOnly: true
|
||
|
|
})[0];
|
||
|
|
}
|