mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
281 lines
9.3 KiB
TypeScript
281 lines
9.3 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 * as types from '../types';
|
||
|
import { EventEmitter } from 'events';
|
||
|
import * as fs from 'fs';
|
||
|
import * as stream from 'stream';
|
||
|
import * as util from 'util';
|
||
|
import * as ws from 'ws';
|
||
|
import { createGuid, makeWaitForNextTask } from '../../utils/utils';
|
||
|
import { BrowserOptions, BrowserProcess } from '../browser';
|
||
|
import { BrowserContext, validateBrowserContextOptions } from '../browserContext';
|
||
|
import { ProgressController } from '../progress';
|
||
|
import { CRBrowser } from '../chromium/crBrowser';
|
||
|
import { helper } from '../helper';
|
||
|
import { Transport } from '../../protocol/transport';
|
||
|
import { RecentLogsCollector } from '../../utils/debugLogger';
|
||
|
|
||
|
const readFileAsync = util.promisify(fs.readFile);
|
||
|
|
||
|
export interface Backend {
|
||
|
devices(): Promise<DeviceBackend[]>;
|
||
|
}
|
||
|
|
||
|
export interface DeviceBackend {
|
||
|
serial: string;
|
||
|
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 Android {
|
||
|
private _backend: Backend;
|
||
|
|
||
|
constructor(backend: Backend) {
|
||
|
this._backend = backend;
|
||
|
}
|
||
|
|
||
|
async devices(): Promise<AndroidDevice[]> {
|
||
|
const devices = await this._backend.devices();
|
||
|
return await Promise.all(devices.map(d => AndroidDevice.create(d)));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
export class AndroidDevice {
|
||
|
readonly _backend: DeviceBackend;
|
||
|
readonly model: string;
|
||
|
readonly serial: string;
|
||
|
private _driverPromise: Promise<Transport> | undefined;
|
||
|
private _lastId = 0;
|
||
|
private _callbacks = new Map<number, { fulfill: (result: any) => void, reject: (error: Error) => void }>();
|
||
|
|
||
|
constructor(backend: DeviceBackend, model: string) {
|
||
|
this._backend = backend;
|
||
|
this.model = model;
|
||
|
this.serial = backend.serial;
|
||
|
}
|
||
|
|
||
|
static async create(backend: DeviceBackend): Promise<AndroidDevice> {
|
||
|
await backend.init();
|
||
|
const model = await backend.runCommand('shell:getprop ro.product.model');
|
||
|
return new AndroidDevice(backend, model);
|
||
|
}
|
||
|
|
||
|
async shell(command: string): Promise<string> {
|
||
|
return await this._backend.runCommand(`shell:${command}`);
|
||
|
}
|
||
|
|
||
|
private async _driver(): Promise<Transport> {
|
||
|
if (this._driverPromise)
|
||
|
return this._driverPromise;
|
||
|
let callback: any;
|
||
|
this._driverPromise = new Promise(f => callback = f);
|
||
|
|
||
|
debug('pw:android')('Stopping the old driver');
|
||
|
await this.shell(`am force-stop com.microsoft.playwright.androiddriver`);
|
||
|
|
||
|
debug('pw:android')('Uninstalling the old driver');
|
||
|
await this.shell(`cmd package uninstall com.microsoft.playwright.androiddriver`);
|
||
|
await this.shell(`cmd package uninstall com.microsoft.playwright.androiddriver.test`);
|
||
|
|
||
|
debug('pw:android')('Installing the new driver');
|
||
|
for (const file of ['android-driver.apk', 'android-driver-target.apk']) {
|
||
|
const driverFile = await readFileAsync(require.resolve(`../../../bin/${file}`));
|
||
|
const installSocket = await this._backend.open(`shell:cmd package install -r -t -S ${driverFile.length}`);
|
||
|
debug('pw:android')('Writing driver bytes: ' + driverFile.length);
|
||
|
await installSocket.write(driverFile);
|
||
|
const success = await new Promise(f => installSocket.on('data', f));
|
||
|
debug('pw:android')('Written driver bytes: ' + success);
|
||
|
}
|
||
|
|
||
|
debug('pw:android')('Starting the new driver');
|
||
|
this.shell(`am instrument -w com.microsoft.playwright.androiddriver.test/androidx.test.runner.AndroidJUnitRunner`);
|
||
|
|
||
|
debug('pw:android')('Polling the socket');
|
||
|
let socket;
|
||
|
while (!socket) {
|
||
|
try {
|
||
|
socket = await this._backend.open(`localabstract:playwright_android_driver_socket`);
|
||
|
} catch (e) {
|
||
|
await new Promise(f => setTimeout(f, 100));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
debug('pw:android')('Connected to driver');
|
||
|
const transport = new Transport(socket, socket, socket, 'be');
|
||
|
transport.onmessage = message => {
|
||
|
const response = JSON.parse(message);
|
||
|
const { id, result, error } = response;
|
||
|
const callback = this._callbacks.get(id);
|
||
|
if (!callback)
|
||
|
return;
|
||
|
if (error)
|
||
|
callback.reject(new Error(error));
|
||
|
else
|
||
|
callback.fulfill(result);
|
||
|
this._callbacks.delete(id);
|
||
|
};
|
||
|
|
||
|
callback(transport);
|
||
|
return this._driverPromise;
|
||
|
}
|
||
|
|
||
|
async send(method: string, params: any): Promise<any> {
|
||
|
const driver = await this._driver();
|
||
|
const id = ++this._lastId;
|
||
|
const result = new Promise((fulfill, reject) => this._callbacks.set(id, { fulfill, reject }));
|
||
|
driver.send(JSON.stringify({ id, method, params }));
|
||
|
return result;
|
||
|
}
|
||
|
|
||
|
async close() {
|
||
|
const driver = await this._driver();
|
||
|
driver.close();
|
||
|
await this._backend.close();
|
||
|
}
|
||
|
|
||
|
async launchBrowser(packageName: string = 'com.android.chrome', options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
|
||
|
debug('pw:android')('Force-stopping', packageName);
|
||
|
await this._backend.runCommand(`shell:am force-stop ${packageName}`);
|
||
|
|
||
|
const socketName = createGuid();
|
||
|
const commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`;
|
||
|
debug('pw:android')('Starting', packageName, commandLine);
|
||
|
await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`);
|
||
|
await this._backend.runCommand(`shell:am start -n ${packageName}/com.google.android.apps.chrome.Main about:blank`);
|
||
|
|
||
|
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 androidBrowser = new AndroidBrowser(this, packageName, socketName);
|
||
|
await androidBrowser._open();
|
||
|
|
||
|
const browserOptions: BrowserOptions = {
|
||
|
name: 'clank',
|
||
|
slowMo: 0,
|
||
|
persistent: { ...options, noDefaultViewport: true },
|
||
|
downloadsPath: undefined,
|
||
|
browserProcess: new ClankBrowserProcess(androidBrowser),
|
||
|
proxy: options.proxy,
|
||
|
protocolLogger: helper.debugProtocolLogger(),
|
||
|
browserLogsCollector: new RecentLogsCollector()
|
||
|
};
|
||
|
validateBrowserContextOptions(options, browserOptions);
|
||
|
|
||
|
const browser = await CRBrowser.connect(androidBrowser, browserOptions);
|
||
|
const controller = new ProgressController();
|
||
|
await controller.run(async progress => {
|
||
|
await browser._defaultContext!._loadDefaultContext(progress);
|
||
|
});
|
||
|
return browser._defaultContext!;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class AndroidBrowser extends EventEmitter {
|
||
|
readonly device: AndroidDevice;
|
||
|
readonly socketName: string;
|
||
|
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) {
|
||
|
super();
|
||
|
this._packageName = packageName;
|
||
|
this.device = device;
|
||
|
this.socketName = socketName;
|
||
|
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];
|
||
|
}
|
||
|
|
||
|
class ClankBrowserProcess implements BrowserProcess {
|
||
|
private _browser: AndroidBrowser;
|
||
|
|
||
|
constructor(browser: AndroidBrowser) {
|
||
|
this._browser = browser;
|
||
|
}
|
||
|
|
||
|
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
|
||
|
|
||
|
async kill(): Promise<void> {
|
||
|
}
|
||
|
|
||
|
async close(): Promise<void> {
|
||
|
await this._browser.close();
|
||
|
}
|
||
|
}
|