2020-12-09 15:06:57 -08:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
|
2022-04-18 16:50:25 -08:00
|
|
|
import { debug } from '../../utilsBundle';
|
2022-04-06 13:57:14 -08:00
|
|
|
import type * as types from '../types';
|
2020-12-09 15:06:57 -08:00
|
|
|
import { EventEmitter } from 'events';
|
2021-02-11 06:36:15 -08:00
|
|
|
import fs from 'fs';
|
2022-02-24 06:57:32 +01:00
|
|
|
import os from 'os';
|
|
|
|
import path from 'path';
|
2022-04-06 13:57:14 -08:00
|
|
|
import type * as stream from 'stream';
|
2022-04-19 12:28:05 -08:00
|
|
|
import { wsReceiver, wsSender } from '../../utilsBundle';
|
2022-04-09 02:52:16 +08:00
|
|
|
import { createGuid, makeWaitForNextTask, isUnderTest } from '../../utils';
|
2022-04-07 19:18:22 -08:00
|
|
|
import { removeFolders } from '../../utils/fileUtils';
|
2022-04-06 13:57:14 -08:00
|
|
|
import type { BrowserOptions, BrowserProcess, PlaywrightOptions } from '../browser';
|
|
|
|
import type { BrowserContext } from '../browserContext';
|
|
|
|
import { validateBrowserContextOptions } from '../browserContext';
|
2020-12-09 15:06:57 -08:00
|
|
|
import { ProgressController } from '../progress';
|
|
|
|
import { CRBrowser } from '../chromium/crBrowser';
|
|
|
|
import { helper } from '../helper';
|
2022-02-02 21:26:45 -08:00
|
|
|
import { PipeTransport } from '../../protocol/transport';
|
2022-04-07 13:36:13 -08:00
|
|
|
import { RecentLogsCollector } from '../../common/debugLogger';
|
2022-02-24 06:57:32 +01:00
|
|
|
import { gracefullyCloseSet } from '../../utils/processLauncher';
|
2022-04-07 13:36:13 -08:00
|
|
|
import { TimeoutSettings } from '../../common/timeoutSettings';
|
2022-04-06 13:57:14 -08:00
|
|
|
import type { AndroidWebView } from '../../protocol/channels';
|
2022-03-17 17:27:33 -08:00
|
|
|
import { SdkObject, serverSideCallMetadata } from '../instrumentation';
|
2020-12-09 15:06:57 -08:00
|
|
|
|
2022-02-24 06:57:32 +01:00
|
|
|
const ARTIFACTS_FOLDER = path.join(os.tmpdir(), 'playwright-artifacts-');
|
|
|
|
|
2020-12-09 15:06:57 -08:00
|
|
|
export interface Backend {
|
2022-03-05 00:57:25 +05:30
|
|
|
devices(options: types.AndroidDeviceOptions): Promise<DeviceBackend[]>;
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface DeviceBackend {
|
|
|
|
serial: string;
|
2020-12-10 16:37:18 -08:00
|
|
|
status: string;
|
2021-07-15 12:12:09 +05:30
|
|
|
close(): Promise<void>;
|
2020-12-09 15:06:57 -08:00
|
|
|
init(): Promise<void>;
|
2021-02-09 14:44:48 -08:00
|
|
|
runCommand(command: string): Promise<Buffer>;
|
|
|
|
open(command: string): Promise<SocketBackend>;
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 14:44:48 -08:00
|
|
|
export interface SocketBackend extends EventEmitter {
|
2021-04-20 23:03:56 -07:00
|
|
|
guid: string;
|
2020-12-09 15:06:57 -08:00
|
|
|
write(data: Buffer): Promise<void>;
|
2021-03-22 09:59:39 -07:00
|
|
|
close(): void;
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 09:00:00 -08:00
|
|
|
export class Android extends SdkObject {
|
2020-12-09 15:06:57 -08:00
|
|
|
private _backend: Backend;
|
2020-12-11 23:35:25 -08:00
|
|
|
private _devices = new Map<string, AndroidDevice>();
|
2020-12-09 17:15:24 -08:00
|
|
|
readonly _timeoutSettings: TimeoutSettings;
|
2021-01-29 16:00:56 -08:00
|
|
|
readonly _playwrightOptions: PlaywrightOptions;
|
2020-12-09 15:06:57 -08:00
|
|
|
|
2021-02-08 16:02:49 -08:00
|
|
|
constructor(backend: Backend, playwrightOptions: PlaywrightOptions) {
|
2021-04-20 23:03:56 -07:00
|
|
|
super(playwrightOptions.rootSdkObject, 'android');
|
2020-12-09 15:06:57 -08:00
|
|
|
this._backend = backend;
|
2021-01-29 16:00:56 -08:00
|
|
|
this._playwrightOptions = playwrightOptions;
|
2020-12-09 17:15:24 -08:00
|
|
|
this._timeoutSettings = new TimeoutSettings();
|
|
|
|
}
|
|
|
|
|
|
|
|
setDefaultTimeout(timeout: number) {
|
|
|
|
this._timeoutSettings.setDefaultTimeout(timeout);
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
|
|
|
|
2022-03-05 00:57:25 +05:30
|
|
|
async devices(options: types.AndroidDeviceOptions): Promise<AndroidDevice[]> {
|
|
|
|
const devices = (await this._backend.devices(options)).filter(d => d.status === 'device');
|
2020-12-11 23:35:25 -08:00
|
|
|
const newSerials = new Set<string>();
|
|
|
|
for (const d of devices) {
|
|
|
|
newSerials.add(d.serial);
|
|
|
|
if (this._devices.has(d.serial))
|
|
|
|
continue;
|
2022-04-03 07:00:38 +08:00
|
|
|
const device = await AndroidDevice.create(this, d, options);
|
2020-12-11 23:35:25 -08:00
|
|
|
this._devices.set(d.serial, device);
|
|
|
|
}
|
|
|
|
for (const d of this._devices.keys()) {
|
|
|
|
if (!newSerials.has(d))
|
|
|
|
this._devices.delete(d);
|
|
|
|
}
|
|
|
|
return [...this._devices.values()];
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
2020-12-12 01:18:32 -08:00
|
|
|
|
|
|
|
_deviceClosed(device: AndroidDevice) {
|
|
|
|
this._devices.delete(device.serial);
|
|
|
|
}
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
|
|
|
|
2021-02-09 09:00:00 -08:00
|
|
|
export class AndroidDevice extends SdkObject {
|
2020-12-09 15:06:57 -08:00
|
|
|
readonly _backend: DeviceBackend;
|
|
|
|
readonly model: string;
|
|
|
|
readonly serial: string;
|
2022-04-03 07:00:38 +08:00
|
|
|
private _options: types.AndroidDeviceOptions;
|
2022-02-02 21:26:45 -08:00
|
|
|
private _driverPromise: Promise<PipeTransport> | undefined;
|
2020-12-09 15:06:57 -08:00
|
|
|
private _lastId = 0;
|
|
|
|
private _callbacks = new Map<number, { fulfill: (result: any) => void, reject: (error: Error) => void }>();
|
2020-12-09 17:15:24 -08:00
|
|
|
private _pollingWebViews: NodeJS.Timeout | undefined;
|
|
|
|
readonly _timeoutSettings: TimeoutSettings;
|
2022-04-09 02:52:16 +08:00
|
|
|
private _webViews = new Map<string, AndroidWebView>();
|
2020-12-09 17:15:24 -08:00
|
|
|
|
|
|
|
static Events = {
|
|
|
|
WebViewAdded: 'webViewAdded',
|
|
|
|
WebViewRemoved: 'webViewRemoved',
|
2020-12-12 01:18:32 -08:00
|
|
|
Closed: 'closed'
|
2020-12-09 17:15:24 -08:00
|
|
|
};
|
|
|
|
|
|
|
|
private _browserConnections = new Set<AndroidBrowser>();
|
2020-12-12 01:18:32 -08:00
|
|
|
private _android: Android;
|
|
|
|
private _isClosed = false;
|
2020-12-09 15:06:57 -08:00
|
|
|
|
2022-04-03 07:00:38 +08:00
|
|
|
constructor(android: Android, backend: DeviceBackend, model: string, options: types.AndroidDeviceOptions) {
|
2021-04-20 23:03:56 -07:00
|
|
|
super(android, 'android-device');
|
2020-12-12 01:18:32 -08:00
|
|
|
this._android = android;
|
2020-12-09 15:06:57 -08:00
|
|
|
this._backend = backend;
|
|
|
|
this.model = model;
|
|
|
|
this.serial = backend.serial;
|
2022-04-03 07:00:38 +08:00
|
|
|
this._options = options;
|
2020-12-09 17:15:24 -08:00
|
|
|
this._timeoutSettings = new TimeoutSettings(android._timeoutSettings);
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
|
|
|
|
2022-04-03 07:00:38 +08:00
|
|
|
static async create(android: Android, backend: DeviceBackend, options: types.AndroidDeviceOptions): Promise<AndroidDevice> {
|
2020-12-09 15:06:57 -08:00
|
|
|
await backend.init();
|
2021-02-09 14:44:48 -08:00
|
|
|
const model = await backend.runCommand('shell:getprop ro.product.model');
|
2022-04-03 07:00:38 +08:00
|
|
|
const device = new AndroidDevice(android, backend, model.toString().trim(), options);
|
2020-12-09 17:15:24 -08:00
|
|
|
await device._init();
|
|
|
|
return device;
|
|
|
|
}
|
|
|
|
|
|
|
|
async _init() {
|
|
|
|
await this._refreshWebViews();
|
|
|
|
const poll = () => {
|
|
|
|
this._pollingWebViews = setTimeout(() => this._refreshWebViews().then(poll).catch(() => {}), 500);
|
|
|
|
};
|
|
|
|
poll();
|
|
|
|
}
|
|
|
|
|
|
|
|
setDefaultTimeout(timeout: number) {
|
|
|
|
this._timeoutSettings.setDefaultTimeout(timeout);
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
|
|
|
|
2020-12-13 08:33:35 -08:00
|
|
|
async shell(command: string): Promise<Buffer> {
|
2021-02-09 14:44:48 -08:00
|
|
|
const result = await this._backend.runCommand(`shell:${command}`);
|
2020-12-10 16:37:18 -08:00
|
|
|
await this._refreshWebViews();
|
|
|
|
return result;
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
|
|
|
|
2020-12-12 18:36:38 +01:00
|
|
|
async open(command: string): Promise<SocketBackend> {
|
2021-02-09 14:44:48 -08:00
|
|
|
return await this._backend.open(`${command}`);
|
2020-12-12 18:36:38 +01:00
|
|
|
}
|
|
|
|
|
2020-12-13 23:20:13 -08:00
|
|
|
async screenshot(): Promise<Buffer> {
|
2021-02-09 14:44:48 -08:00
|
|
|
return await this._backend.runCommand(`shell:screencap -p`);
|
2020-12-13 23:20:13 -08:00
|
|
|
}
|
|
|
|
|
2022-02-02 21:26:45 -08:00
|
|
|
private async _driver(): Promise<PipeTransport> {
|
2020-12-14 18:39:14 -08:00
|
|
|
if (!this._driverPromise)
|
|
|
|
this._driverPromise = this._installDriver();
|
|
|
|
return this._driverPromise;
|
|
|
|
}
|
2020-12-09 15:06:57 -08:00
|
|
|
|
2022-02-02 21:26:45 -08:00
|
|
|
private async _installDriver(): Promise<PipeTransport> {
|
2020-12-09 15:06:57 -08:00
|
|
|
debug('pw:android')('Stopping the old driver');
|
|
|
|
await this.shell(`am force-stop com.microsoft.playwright.androiddriver`);
|
|
|
|
|
2022-04-03 07:00:38 +08:00
|
|
|
// uninstall and install driver on every excution
|
|
|
|
if (!this._options.omitDriverInstall) {
|
|
|
|
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'])
|
|
|
|
await this.installApk(await fs.promises.readFile(require.resolve(`../../../bin/${file}`)));
|
|
|
|
} else {
|
|
|
|
debug('pw:android')('Skipping the driver installation');
|
|
|
|
}
|
2020-12-09 15:06:57 -08:00
|
|
|
|
|
|
|
debug('pw:android')('Starting the new driver');
|
2021-03-22 09:59:39 -07:00
|
|
|
this.shell('am instrument -w com.microsoft.playwright.androiddriver.test/androidx.test.runner.AndroidJUnitRunner').catch(e => debug('pw:android')(e));
|
2020-12-13 22:33:00 -08:00
|
|
|
const socket = await this._waitForLocalAbstract('playwright_android_driver_socket');
|
2022-02-02 21:26:45 -08:00
|
|
|
const transport = new PipeTransport(socket, socket, socket, 'be');
|
2020-12-09 15:06:57 -08:00
|
|
|
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);
|
|
|
|
};
|
2020-12-14 18:39:14 -08:00
|
|
|
return transport;
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
|
|
|
|
2020-12-13 22:33:00 -08:00
|
|
|
private async _waitForLocalAbstract(socketName: string): Promise<SocketBackend> {
|
|
|
|
let socket: SocketBackend | undefined;
|
|
|
|
debug('pw:android')(`Polling the socket localabstract:${socketName}`);
|
|
|
|
while (!socket) {
|
|
|
|
try {
|
2021-02-09 14:44:48 -08:00
|
|
|
socket = await this._backend.open(`localabstract:${socketName}`);
|
2020-12-13 22:33:00 -08:00
|
|
|
} catch (e) {
|
|
|
|
await new Promise(f => setTimeout(f, 250));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
debug('pw:android')(`Connected to localabstract:${socketName}`);
|
|
|
|
return socket;
|
|
|
|
}
|
|
|
|
|
2020-12-14 18:39:14 -08:00
|
|
|
async send(method: string, params: any = {}): Promise<any> {
|
|
|
|
// Patch the timeout in!
|
|
|
|
params.timeout = this._timeoutSettings.timeout(params);
|
2020-12-09 15:06:57 -08:00
|
|
|
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() {
|
2020-12-12 01:18:32 -08:00
|
|
|
this._isClosed = true;
|
2020-12-09 17:15:24 -08:00
|
|
|
if (this._pollingWebViews)
|
|
|
|
clearTimeout(this._pollingWebViews);
|
|
|
|
for (const connection of this._browserConnections)
|
|
|
|
await connection.close();
|
|
|
|
if (this._driverPromise) {
|
|
|
|
const driver = await this._driver();
|
|
|
|
driver.close();
|
|
|
|
}
|
2020-12-09 15:06:57 -08:00
|
|
|
await this._backend.close();
|
2020-12-12 01:18:32 -08:00
|
|
|
this._android._deviceClosed(this);
|
|
|
|
this.emit(AndroidDevice.Events.Closed);
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
|
|
|
|
2021-02-11 17:46:54 -08:00
|
|
|
async launchBrowser(pkg: string = 'com.android.chrome', options: types.BrowserContextOptions): Promise<BrowserContext> {
|
2020-12-09 17:15:24 -08:00
|
|
|
debug('pw:android')('Force-stopping', pkg);
|
2021-02-09 14:44:48 -08:00
|
|
|
await this._backend.runCommand(`shell:am force-stop ${pkg}`);
|
2022-04-09 02:52:16 +08:00
|
|
|
const socketName = isUnderTest() ? 'webview_devtools_remote_playwright_test' : ('playwright-' + createGuid());
|
2020-12-09 15:06:57 -08:00
|
|
|
const commandLine = `_ --disable-fre --no-default-browser-check --no-first-run --remote-debugging-socket-name=${socketName}`;
|
2020-12-09 17:15:24 -08:00
|
|
|
debug('pw:android')('Starting', pkg, commandLine);
|
2021-02-09 14:44:48 -08:00
|
|
|
await this._backend.runCommand(`shell:echo "${commandLine}" > /data/local/tmp/chrome-command-line`);
|
|
|
|
await this._backend.runCommand(`shell:am start -n ${pkg}/com.google.android.apps.chrome.Main about:blank`);
|
2020-12-09 17:15:24 -08:00
|
|
|
return await this._connectToBrowser(socketName, options);
|
|
|
|
}
|
|
|
|
|
2022-04-09 02:52:16 +08:00
|
|
|
async connectToWebView(socketName: string): Promise<BrowserContext> {
|
|
|
|
const webView = this._webViews.get(socketName);
|
2020-12-09 17:15:24 -08:00
|
|
|
if (!webView)
|
|
|
|
throw new Error('WebView has been closed');
|
2022-04-09 02:52:16 +08:00
|
|
|
return await this._connectToBrowser(socketName);
|
2020-12-09 17:15:24 -08:00
|
|
|
}
|
|
|
|
|
2021-08-20 21:32:21 +02:00
|
|
|
private async _connectToBrowser(socketName: string, options: types.BrowserContextOptions = {}): Promise<BrowserContext> {
|
2020-12-13 22:33:00 -08:00
|
|
|
const socket = await this._waitForLocalAbstract(socketName);
|
|
|
|
const androidBrowser = new AndroidBrowser(this, socket);
|
|
|
|
await androidBrowser._init();
|
2020-12-09 17:15:24 -08:00
|
|
|
this._browserConnections.add(androidBrowser);
|
2020-12-09 15:06:57 -08:00
|
|
|
|
2022-02-24 06:57:32 +01:00
|
|
|
const artifactsDir = await fs.promises.mkdtemp(ARTIFACTS_FOLDER);
|
|
|
|
const cleanupArtifactsDir = async () => {
|
|
|
|
const errors = await removeFolders([artifactsDir]);
|
|
|
|
for (let i = 0; i < (errors || []).length; ++i)
|
|
|
|
debug('pw:android')(`exception while removing ${artifactsDir}: ${errors[i]}`);
|
|
|
|
};
|
|
|
|
gracefullyCloseSet.add(cleanupArtifactsDir);
|
|
|
|
socket.on('close', async () => {
|
|
|
|
gracefullyCloseSet.delete(cleanupArtifactsDir);
|
|
|
|
cleanupArtifactsDir().catch(e => debug('pw:android')(`could not cleanup artifacts dir: ${e}`));
|
|
|
|
});
|
2020-12-09 15:06:57 -08:00
|
|
|
const browserOptions: BrowserOptions = {
|
2021-01-29 16:00:56 -08:00
|
|
|
...this._android._playwrightOptions,
|
2020-12-09 15:06:57 -08:00
|
|
|
name: 'clank',
|
2021-01-13 12:08:14 -08:00
|
|
|
isChromium: true,
|
2020-12-09 15:06:57 -08:00
|
|
|
slowMo: 0,
|
|
|
|
persistent: { ...options, noDefaultViewport: true },
|
2022-02-24 06:57:32 +01:00
|
|
|
artifactsDir,
|
|
|
|
downloadsPath: artifactsDir,
|
|
|
|
tracesDir: artifactsDir,
|
2020-12-09 15:06:57 -08:00
|
|
|
browserProcess: new ClankBrowserProcess(androidBrowser),
|
|
|
|
proxy: options.proxy,
|
|
|
|
protocolLogger: helper.debugProtocolLogger(),
|
|
|
|
browserLogsCollector: new RecentLogsCollector()
|
|
|
|
};
|
|
|
|
validateBrowserContextOptions(options, browserOptions);
|
|
|
|
|
2021-02-08 16:02:49 -08:00
|
|
|
const browser = await CRBrowser.connect(androidBrowser, browserOptions);
|
2022-03-17 17:27:33 -08:00
|
|
|
const controller = new ProgressController(serverSideCallMetadata(), this);
|
2020-12-17 18:28:49 -08:00
|
|
|
const defaultContext = browser._defaultContext!;
|
2020-12-09 15:06:57 -08:00
|
|
|
await controller.run(async progress => {
|
2020-12-17 18:28:49 -08:00
|
|
|
await defaultContext._loadDefaultContextAsIs(progress);
|
2020-12-09 15:06:57 -08:00
|
|
|
});
|
2020-12-17 18:28:49 -08:00
|
|
|
return defaultContext;
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
2020-12-09 17:15:24 -08:00
|
|
|
|
|
|
|
webViews(): AndroidWebView[] {
|
|
|
|
return [...this._webViews.values()];
|
|
|
|
}
|
|
|
|
|
2020-12-12 18:36:38 +01:00
|
|
|
async installApk(content: Buffer, options?: { args?: string[] }): Promise<void> {
|
|
|
|
const args = options && options.args ? options.args : ['-r', '-t', '-S'];
|
|
|
|
debug('pw:android')('Opening install socket');
|
2021-02-09 14:44:48 -08:00
|
|
|
const installSocket = await this._backend.open(`shell:cmd package install ${args.join(' ')} ${content.length}`);
|
2020-12-12 18:36:38 +01:00
|
|
|
debug('pw:android')('Writing driver bytes: ' + content.length);
|
|
|
|
await installSocket.write(content);
|
|
|
|
const success = await new Promise(f => installSocket.on('data', f));
|
|
|
|
debug('pw:android')('Written driver bytes: ' + success);
|
2021-03-22 09:59:39 -07:00
|
|
|
installSocket.close();
|
2020-12-12 18:36:38 +01:00
|
|
|
}
|
|
|
|
|
2020-12-13 22:00:37 -08:00
|
|
|
async push(content: Buffer, path: string, mode = 0o644): Promise<void> {
|
2021-02-09 14:44:48 -08:00
|
|
|
const socket = await this._backend.open(`sync:`);
|
2020-12-13 22:00:37 -08:00
|
|
|
const sendHeader = async (command: string, length: number) => {
|
|
|
|
const buffer = Buffer.alloc(command.length + 4);
|
|
|
|
buffer.write(command, 0);
|
|
|
|
buffer.writeUInt32LE(length, command.length);
|
|
|
|
await socket.write(buffer);
|
|
|
|
};
|
|
|
|
const send = async (command: string, data: Buffer) => {
|
|
|
|
await sendHeader(command, data.length);
|
|
|
|
await socket.write(data);
|
|
|
|
};
|
|
|
|
await send('SEND', Buffer.from(`${path},${mode}`));
|
|
|
|
const maxChunk = 65535;
|
|
|
|
for (let i = 0; i < content.length; i += maxChunk)
|
|
|
|
await send('DATA', content.slice(i, i + maxChunk));
|
|
|
|
await sendHeader('DONE', (Date.now() / 1000) | 0);
|
|
|
|
const result = await new Promise<Buffer>(f => socket.once('data', f));
|
|
|
|
const code = result.slice(0, 4).toString();
|
|
|
|
if (code !== 'OKAY')
|
|
|
|
throw new Error('Could not push: ' + code);
|
2021-03-22 09:59:39 -07:00
|
|
|
socket.close();
|
2020-12-13 22:00:37 -08:00
|
|
|
}
|
|
|
|
|
2020-12-09 17:15:24 -08:00
|
|
|
private async _refreshWebViews() {
|
2022-04-09 02:52:16 +08:00
|
|
|
// possible socketName, eg: webview_devtools_remote_32327, webview_devtools_remote_32327_zeus, webview_devtools_remote_zeus
|
2021-02-09 14:44:48 -08:00
|
|
|
const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n');
|
2020-12-12 01:18:32 -08:00
|
|
|
if (this._isClosed)
|
|
|
|
return;
|
2020-12-09 17:15:24 -08:00
|
|
|
|
2022-04-09 02:52:16 +08:00
|
|
|
const socketNames = new Set<string>();
|
2020-12-09 17:15:24 -08:00
|
|
|
for (const line of sockets) {
|
2022-04-09 02:52:16 +08:00
|
|
|
const matchSocketName = line.match(/[^@]+@(.*?webview_devtools_remote_?.*)/);
|
|
|
|
if (!matchSocketName)
|
2020-12-09 17:15:24 -08:00
|
|
|
continue;
|
2022-04-09 02:52:16 +08:00
|
|
|
|
|
|
|
const socketName = matchSocketName[1];
|
|
|
|
socketNames.add(socketName);
|
|
|
|
if (this._webViews.has(socketName))
|
2020-12-09 17:15:24 -08:00
|
|
|
continue;
|
|
|
|
|
2022-04-09 02:52:16 +08:00
|
|
|
// possible line: 0000000000000000: 00000002 00000000 00010000 0001 01 5841881 @webview_devtools_remote_zeus
|
|
|
|
// the result: match[1] = ''
|
|
|
|
const match = line.match(/[^@]+@.*?webview_devtools_remote_?(\d*)/);
|
|
|
|
let pid = -1;
|
|
|
|
if (match && match[1])
|
|
|
|
pid = +match[1];
|
|
|
|
|
|
|
|
const pkg = await this._extractPkg(pid);
|
2020-12-12 01:18:32 -08:00
|
|
|
if (this._isClosed)
|
|
|
|
return;
|
2022-04-09 02:52:16 +08:00
|
|
|
|
|
|
|
const webView = { pid, pkg, socketName };
|
|
|
|
this._webViews.set(socketName, webView);
|
2020-12-09 17:15:24 -08:00
|
|
|
this.emit(AndroidDevice.Events.WebViewAdded, webView);
|
|
|
|
}
|
|
|
|
for (const p of this._webViews.keys()) {
|
2022-04-09 02:52:16 +08:00
|
|
|
if (!socketNames.has(p)) {
|
2020-12-09 17:15:24 -08:00
|
|
|
this._webViews.delete(p);
|
|
|
|
this.emit(AndroidDevice.Events.WebViewRemoved, p);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-04-09 02:52:16 +08:00
|
|
|
|
|
|
|
private async _extractPkg(pid: number) {
|
|
|
|
let pkg = '';
|
|
|
|
if (pid === -1)
|
|
|
|
return pkg;
|
|
|
|
|
|
|
|
const procs = (await this._backend.runCommand(`shell:ps -A | grep ${pid}`)).toString().split('\n');
|
|
|
|
for (const proc of procs) {
|
|
|
|
const match = proc.match(/[^\s]+\s+(\d+).*$/);
|
|
|
|
if (!match)
|
|
|
|
continue;
|
|
|
|
pkg = proc.substring(proc.lastIndexOf(' ') + 1);
|
|
|
|
}
|
|
|
|
return pkg;
|
|
|
|
}
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
class AndroidBrowser extends EventEmitter {
|
|
|
|
readonly device: AndroidDevice;
|
2020-12-13 22:33:00 -08:00
|
|
|
private _socket: SocketBackend;
|
2020-12-09 15:06:57 -08:00
|
|
|
private _receiver: stream.Writable;
|
|
|
|
private _waitForNextTask = makeWaitForNextTask();
|
|
|
|
onmessage?: (message: any) => void;
|
|
|
|
onclose?: () => void;
|
|
|
|
|
2020-12-13 22:33:00 -08:00
|
|
|
constructor(device: AndroidDevice, socket: SocketBackend) {
|
2020-12-09 15:06:57 -08:00
|
|
|
super();
|
2021-02-03 13:53:09 -08:00
|
|
|
this.setMaxListeners(0);
|
2020-12-09 15:06:57 -08:00
|
|
|
this.device = device;
|
2020-12-13 22:33:00 -08:00
|
|
|
this._socket = socket;
|
|
|
|
this._socket.on('close', () => {
|
|
|
|
this._waitForNextTask(() => {
|
|
|
|
if (this.onclose)
|
|
|
|
this.onclose();
|
|
|
|
});
|
|
|
|
});
|
2022-04-19 12:28:05 -08:00
|
|
|
this._receiver = new wsReceiver() as stream.Writable;
|
2020-12-09 15:06:57 -08:00
|
|
|
this._receiver.on('message', message => {
|
|
|
|
this._waitForNextTask(() => {
|
|
|
|
if (this.onmessage)
|
|
|
|
this.onmessage(JSON.parse(message));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-12-13 22:33:00 -08:00
|
|
|
async _init() {
|
2020-12-09 15:06:57 -08:00
|
|
|
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() {
|
2021-03-22 09:59:39 -07:00
|
|
|
this._socket!.close();
|
2020-12-09 15:06:57 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function encodeWebFrame(data: string): Buffer {
|
2022-04-19 12:28:05 -08:00
|
|
|
return wsSender.frame(Buffer.from(data), {
|
2020-12-09 15:06:57 -08:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
2022-04-09 02:52:16 +08:00
|
|
|
|
|
|
|
|