chore: add adb-based connectivity (#4375)
This commit is contained in:
parent
06c8881dad
commit
28f6547d67
|
|
@ -47,28 +47,26 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
||||||
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
|
||||||
env: options.env ? envObjectToArray(options.env) : undefined,
|
env: options.env ? envObjectToArray(options.env) : undefined,
|
||||||
});
|
});
|
||||||
return new BrowserServerImpl(this._browserType, browser, options.port);
|
return new BrowserServerImpl(browser, options.port);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BrowserServerImpl extends EventEmitter implements BrowserServer {
|
export class BrowserServerImpl extends EventEmitter implements BrowserServer {
|
||||||
private _server: ws.Server;
|
private _server: ws.Server;
|
||||||
private _browserType: BrowserType;
|
|
||||||
private _browser: Browser;
|
private _browser: Browser;
|
||||||
private _wsEndpoint: string;
|
private _wsEndpoint: string;
|
||||||
private _process: ChildProcess;
|
private _process: ChildProcess;
|
||||||
|
|
||||||
constructor(browserType: BrowserType, browser: Browser, port: number = 0) {
|
constructor(browser: Browser, port: number = 0) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
this._browserType = browserType;
|
|
||||||
this._browser = browser;
|
this._browser = browser;
|
||||||
|
|
||||||
const token = createGuid();
|
const token = createGuid();
|
||||||
this._server = new ws.Server({ port });
|
this._server = new ws.Server({ port });
|
||||||
const address = this._server.address();
|
const address = this._server.address();
|
||||||
this._wsEndpoint = typeof address === 'string' ? `${address}/${token}` : `ws://127.0.0.1:${address.port}/${token}`;
|
this._wsEndpoint = typeof address === 'string' ? `${address}/${token}` : `ws://127.0.0.1:${address.port}/${token}`;
|
||||||
this._process = browser._options.browserProcess.process;
|
this._process = browser._options.browserProcess.process!;
|
||||||
|
|
||||||
this._server.on('connection', (socket: ws, req) => {
|
this._server.on('connection', (socket: ws, req) => {
|
||||||
if (req.url !== '/' + token) {
|
if (req.url !== '/' + token) {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
|
||||||
readonly chromium: BrowserType;
|
readonly chromium: BrowserType;
|
||||||
readonly firefox: BrowserType;
|
readonly firefox: BrowserType;
|
||||||
readonly webkit: BrowserType;
|
readonly webkit: BrowserType;
|
||||||
|
readonly _clank: BrowserType;
|
||||||
readonly devices: Devices;
|
readonly devices: Devices;
|
||||||
readonly selectors: Selectors;
|
readonly selectors: Selectors;
|
||||||
readonly errors: { TimeoutError: typeof TimeoutError };
|
readonly errors: { TimeoutError: typeof TimeoutError };
|
||||||
|
|
@ -45,6 +46,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
|
||||||
this.chromium = BrowserType.from(initializer.chromium);
|
this.chromium = BrowserType.from(initializer.chromium);
|
||||||
this.firefox = BrowserType.from(initializer.firefox);
|
this.firefox = BrowserType.from(initializer.firefox);
|
||||||
this.webkit = BrowserType.from(initializer.webkit);
|
this.webkit = BrowserType.from(initializer.webkit);
|
||||||
|
this._clank = BrowserType.from(initializer.clank);
|
||||||
if (initializer.electron)
|
if (initializer.electron)
|
||||||
(this as any).electron = Electron.from(initializer.electron);
|
(this as any).electron = Electron.from(initializer.electron);
|
||||||
this.devices = {};
|
this.devices = {};
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
|
||||||
.map(([name, descriptor]) => ({ name, descriptor }));
|
.map(([name, descriptor]) => ({ name, descriptor }));
|
||||||
super(scope, playwright, 'Playwright', {
|
super(scope, playwright, 'Playwright', {
|
||||||
chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
|
chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
|
||||||
|
clank: new BrowserTypeDispatcher(scope, playwright.clank),
|
||||||
firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
|
firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
|
||||||
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
|
||||||
electron: electron ? new ElectronDispatcher(scope, electron) : undefined,
|
electron: electron ? new ElectronDispatcher(scope, electron) : undefined,
|
||||||
|
|
|
||||||
|
|
@ -92,6 +92,7 @@ export type SerializedError = {
|
||||||
// ----------- Playwright -----------
|
// ----------- Playwright -----------
|
||||||
export type PlaywrightInitializer = {
|
export type PlaywrightInitializer = {
|
||||||
chromium: BrowserTypeChannel,
|
chromium: BrowserTypeChannel,
|
||||||
|
clank: BrowserTypeChannel,
|
||||||
firefox: BrowserTypeChannel,
|
firefox: BrowserTypeChannel,
|
||||||
webkit: BrowserTypeChannel,
|
webkit: BrowserTypeChannel,
|
||||||
electron?: ElectronChannel,
|
electron?: ElectronChannel,
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ Playwright:
|
||||||
|
|
||||||
initializer:
|
initializer:
|
||||||
chromium: BrowserType
|
chromium: BrowserType
|
||||||
|
clank: BrowserType
|
||||||
firefox: BrowserType
|
firefox: BrowserType
|
||||||
webkit: BrowserType
|
webkit: BrowserType
|
||||||
electron: Electron?
|
electron: Electron?
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ import { ChildProcess } from 'child_process';
|
||||||
|
|
||||||
export interface BrowserProcess {
|
export interface BrowserProcess {
|
||||||
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
|
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
|
||||||
process: ChildProcess;
|
process?: ChildProcess;
|
||||||
kill(): Promise<void>;
|
kill(): Promise<void>;
|
||||||
close(): Promise<void>;
|
close(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -118,6 +118,10 @@ export class CRBrowser extends Browser {
|
||||||
return this._version;
|
return this._version;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isClank(): boolean {
|
||||||
|
return this._options.name === 'clank';
|
||||||
|
}
|
||||||
|
|
||||||
_onAttachedToTarget({targetInfo, sessionId, waitingForDebugger}: Protocol.Target.attachedToTargetPayload) {
|
_onAttachedToTarget({targetInfo, sessionId, waitingForDebugger}: Protocol.Target.attachedToTargetPayload) {
|
||||||
if (targetInfo.type === 'browser')
|
if (targetInfo.type === 'browser')
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -380,7 +380,9 @@ class FrameSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _initialize(hasUIWindow: boolean) {
|
async _initialize(hasUIWindow: boolean) {
|
||||||
if (hasUIWindow && !this._crPage._browserContext._options.noDefaultViewport) {
|
if (hasUIWindow &&
|
||||||
|
!this._crPage._browserContext._browser.isClank() &&
|
||||||
|
!this._crPage._browserContext._options.noDefaultViewport) {
|
||||||
const { windowId } = await this._client.send('Browser.getWindowForTarget');
|
const { windowId } = await this._client.send('Browser.getWindowForTarget');
|
||||||
this._windowId = windowId;
|
this._windowId = windowId;
|
||||||
}
|
}
|
||||||
|
|
@ -825,6 +827,8 @@ class FrameSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateViewport(): Promise<void> {
|
async _updateViewport(): Promise<void> {
|
||||||
|
if (this._crPage._browserContext._browser.isClank())
|
||||||
|
return;
|
||||||
assert(this._isMainFrame());
|
assert(this._isMainFrame());
|
||||||
const options = this._crPage._browserContext._options;
|
const options = this._crPage._browserContext._options;
|
||||||
const viewportSize = this._page._state.viewportSize;
|
const viewportSize = this._page._state.viewportSize;
|
||||||
|
|
@ -863,6 +867,8 @@ class FrameSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _updateEmulateMedia(initial: boolean): Promise<void> {
|
async _updateEmulateMedia(initial: boolean): Promise<void> {
|
||||||
|
if (this._crPage._browserContext._browser.isClank())
|
||||||
|
return;
|
||||||
const colorScheme = this._page._state.colorScheme || this._crPage._browserContext._options.colorScheme || 'light';
|
const colorScheme = this._page._state.colorScheme || this._crPage._browserContext._options.colorScheme || 'light';
|
||||||
const features = colorScheme ? [{ name: 'prefers-color-scheme', value: colorScheme }] : [];
|
const features = colorScheme ? [{ name: 'prefers-color-scheme', value: colorScheme }] : [];
|
||||||
await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features });
|
await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features });
|
||||||
|
|
|
||||||
169
src/server/clank/android.ts
Normal file
169
src/server/clank/android.ts
Normal file
|
|
@ -0,0 +1,169 @@
|
||||||
|
/**
|
||||||
|
* 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];
|
||||||
|
}
|
||||||
156
src/server/clank/backendAdb.ts
Normal file
156
src/server/clank/backendAdb.ts
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
/**
|
||||||
|
* 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 assert from 'assert';
|
||||||
|
import * as debug from 'debug';
|
||||||
|
import * as net from 'net';
|
||||||
|
import { EventEmitter } from 'ws';
|
||||||
|
import { Backend, DeviceBackend, SocketBackend } from './android';
|
||||||
|
|
||||||
|
export class AdbBackend implements Backend {
|
||||||
|
async devices(): Promise<DeviceBackend[]> {
|
||||||
|
const result = await runCommand('host:devices');
|
||||||
|
const lines = result.toString().trim().split('\n');
|
||||||
|
const serials = lines.map(line => line.split('\t')[0]);
|
||||||
|
return serials.map(serial => new AdbDevice(serial));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AdbDevice implements DeviceBackend {
|
||||||
|
readonly serial: string;
|
||||||
|
|
||||||
|
constructor(serial: string) {
|
||||||
|
this.serial = serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
}
|
||||||
|
|
||||||
|
runCommand(command: string): Promise<string> {
|
||||||
|
return runCommand(command, this.serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
async open(command: string): Promise<SocketBackend> {
|
||||||
|
const result = await open(command, this.serial);
|
||||||
|
result.becomeSocket();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommand(command: string, serial?: string): Promise<string> {
|
||||||
|
debug('pw:adb:runCommand')(command, serial);
|
||||||
|
const socket = new BufferedSocketWrapper(net.createConnection({ port: 5037 }));
|
||||||
|
if (serial) {
|
||||||
|
await socket.write(encodeMessage(`host:transport:${serial}`));
|
||||||
|
const status = await socket.read(4);
|
||||||
|
assert(status.toString() === 'OKAY', status.toString());
|
||||||
|
}
|
||||||
|
await socket.write(encodeMessage(command));
|
||||||
|
const status = await socket.read(4);
|
||||||
|
assert(status.toString() === 'OKAY', status.toString());
|
||||||
|
if (!command.startsWith('shell:')) {
|
||||||
|
const remainingLength = parseInt((await socket.read(4)).toString(), 16);
|
||||||
|
return (await socket.read(remainingLength)).toString();
|
||||||
|
}
|
||||||
|
return (await socket.readAll()).toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function open(command: string, serial?: string): Promise<BufferedSocketWrapper> {
|
||||||
|
const socket = new BufferedSocketWrapper(net.createConnection({ port: 5037 }));
|
||||||
|
if (serial) {
|
||||||
|
await socket.write(encodeMessage(`host:transport:${serial}`));
|
||||||
|
const status = await socket.read(4);
|
||||||
|
assert(status.toString() === 'OKAY', status.toString());
|
||||||
|
}
|
||||||
|
await socket.write(encodeMessage(command));
|
||||||
|
const status = await socket.read(4);
|
||||||
|
assert(status.toString() === 'OKAY', status.toString());
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeMessage(message: string): Buffer {
|
||||||
|
let lenHex = (message.length).toString(16);
|
||||||
|
lenHex = '0'.repeat(4 - lenHex.length) + lenHex;
|
||||||
|
return Buffer.from(lenHex + message);
|
||||||
|
}
|
||||||
|
|
||||||
|
class BufferedSocketWrapper extends EventEmitter implements SocketBackend {
|
||||||
|
private _socket: net.Socket;
|
||||||
|
private _buffer = Buffer.from([]);
|
||||||
|
private _isSocket = false;
|
||||||
|
private _notifyReader: (() => void) | undefined;
|
||||||
|
private _connectPromise: Promise<void>;
|
||||||
|
private _isClosed = false;
|
||||||
|
|
||||||
|
constructor(socket: net.Socket) {
|
||||||
|
super();
|
||||||
|
this._socket = socket;
|
||||||
|
this._connectPromise = new Promise(f => this._socket.on('connect', f));
|
||||||
|
this._socket.on('data', data => {
|
||||||
|
debug('pw:android:adb:data')(data.toString());
|
||||||
|
if (this._isSocket) {
|
||||||
|
this.emit('data', data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._buffer = Buffer.concat([this._buffer, data]);
|
||||||
|
if (this._notifyReader)
|
||||||
|
this._notifyReader();
|
||||||
|
});
|
||||||
|
this._socket.on('close', () => {
|
||||||
|
this._isClosed = true;
|
||||||
|
if (this._notifyReader)
|
||||||
|
this._notifyReader();
|
||||||
|
this.emit('close');
|
||||||
|
});
|
||||||
|
this._socket.on('error', error => this.emit('error', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
async write(data: Buffer) {
|
||||||
|
debug('pw:android:adb:send')(data.toString());
|
||||||
|
await this._connectPromise;
|
||||||
|
await new Promise(f => this._socket.write(data, f));
|
||||||
|
}
|
||||||
|
|
||||||
|
async close() {
|
||||||
|
debug('pw:android:adb')('Close');
|
||||||
|
this._socket.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
async read(length: number): Promise<Buffer> {
|
||||||
|
await this._connectPromise;
|
||||||
|
assert(!this._isSocket, 'Can not read by length in socket mode');
|
||||||
|
while (this._buffer.length < length)
|
||||||
|
await new Promise(f => this._notifyReader = f);
|
||||||
|
const result = this._buffer.slice(0, length);
|
||||||
|
this._buffer = this._buffer.slice(length);
|
||||||
|
debug('pw:android:adb:recv')(result.toString());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async readAll(): Promise<Buffer> {
|
||||||
|
while (!this._isClosed)
|
||||||
|
await new Promise(f => this._notifyReader = f);
|
||||||
|
return this._buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
becomeSocket() {
|
||||||
|
assert(!this._buffer.length);
|
||||||
|
this._isSocket = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/server/clank/clank.ts
Normal file
99
src/server/clank/clank.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
/**
|
||||||
|
* Copyright 2017 Google Inc. All rights reserved.
|
||||||
|
* Modifications copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* 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 { BrowserType } from '../browserType';
|
||||||
|
import { Browser, BrowserOptions, BrowserProcess } from '../browser';
|
||||||
|
import * as types from '../types';
|
||||||
|
import { normalizeProxySettings, validateBrowserContextOptions } from '../browserContext';
|
||||||
|
import { Progress } from '../progress';
|
||||||
|
import { ConnectionTransport } from '../transport';
|
||||||
|
import { Env } from '../processLauncher';
|
||||||
|
import { CRBrowser } from '../chromium/crBrowser';
|
||||||
|
import { AndroidBrowser, AndroidClient, AndroidDevice } from './android';
|
||||||
|
import { AdbBackend } from './backendAdb';
|
||||||
|
|
||||||
|
export class Clank extends BrowserType {
|
||||||
|
async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, userDataDir?: string): Promise<Browser> {
|
||||||
|
options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined;
|
||||||
|
if ((options as any).__testHookBeforeCreateBrowser)
|
||||||
|
await (options as any).__testHookBeforeCreateBrowser();
|
||||||
|
|
||||||
|
// const client = new AndroidClient(new UsbBackend());
|
||||||
|
const client = new AndroidClient(new AdbBackend());
|
||||||
|
const device = (await client.devices())[0];
|
||||||
|
await device.init();
|
||||||
|
const adbBrowser = await device.launchBrowser(options.executablePath || 'com.android.chrome'); // com.chrome.canary
|
||||||
|
const transport = adbBrowser;
|
||||||
|
|
||||||
|
const browserOptions: BrowserOptions = {
|
||||||
|
name: 'clank',
|
||||||
|
slowMo: options.slowMo,
|
||||||
|
persistent,
|
||||||
|
headful: !options.headless,
|
||||||
|
downloadsPath: undefined,
|
||||||
|
browserProcess: new ClankBrowserProcess(device, adbBrowser),
|
||||||
|
proxy: options.proxy,
|
||||||
|
};
|
||||||
|
if (persistent)
|
||||||
|
validateBrowserContextOptions(persistent, browserOptions);
|
||||||
|
|
||||||
|
const browser = await this._connectToTransport(transport, browserOptions);
|
||||||
|
// We assume no control when using custom arguments, and do not prepare the default context in that case.
|
||||||
|
if (persistent && !options.ignoreAllDefaultArgs)
|
||||||
|
await browser._defaultContext!._loadDefaultContext(progress);
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
_defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_connectToTransport(transport: ConnectionTransport, options: BrowserOptions): Promise<Browser> {
|
||||||
|
return CRBrowser.connect(transport, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
_amendEnvironment(env: Env, userDataDir: string, executable: string, browserArguments: string[]): Env {
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rewriteStartupError(error: Error): Error {
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
_attemptToGracefullyCloseBrowser(transport: ConnectionTransport): void {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClankBrowserProcess implements BrowserProcess {
|
||||||
|
private _device: AndroidDevice;
|
||||||
|
private _browser: AndroidBrowser;
|
||||||
|
|
||||||
|
constructor(device: AndroidDevice, browser: AndroidBrowser) {
|
||||||
|
this._device = device;
|
||||||
|
this._browser = browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
|
||||||
|
|
||||||
|
async kill(): Promise<void> {
|
||||||
|
}
|
||||||
|
|
||||||
|
async close(): Promise<void> {
|
||||||
|
await this._browser.close();
|
||||||
|
await this._device.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Chromium } from './chromium/chromium';
|
import { Chromium } from './chromium/chromium';
|
||||||
|
import { Clank } from './clank/clank';
|
||||||
import { WebKit } from './webkit/webkit';
|
import { WebKit } from './webkit/webkit';
|
||||||
import { Firefox } from './firefox/firefox';
|
import { Firefox } from './firefox/firefox';
|
||||||
import * as browserPaths from '../utils/browserPaths';
|
import * as browserPaths from '../utils/browserPaths';
|
||||||
|
|
@ -23,6 +24,7 @@ import { serverSelectors } from './selectors';
|
||||||
export class Playwright {
|
export class Playwright {
|
||||||
readonly selectors = serverSelectors;
|
readonly selectors = serverSelectors;
|
||||||
readonly chromium: Chromium;
|
readonly chromium: Chromium;
|
||||||
|
readonly clank: Clank;
|
||||||
readonly firefox: Firefox;
|
readonly firefox: Firefox;
|
||||||
readonly webkit: WebKit;
|
readonly webkit: WebKit;
|
||||||
|
|
||||||
|
|
@ -35,5 +37,11 @@ export class Playwright {
|
||||||
|
|
||||||
const webkit = browsers.find(browser => browser.name === 'webkit');
|
const webkit = browsers.find(browser => browser.name === 'webkit');
|
||||||
this.webkit = new WebKit(packagePath, webkit!);
|
this.webkit = new WebKit(packagePath, webkit!);
|
||||||
|
|
||||||
|
this.clank = new Clank(packagePath, {
|
||||||
|
name: 'clank',
|
||||||
|
revision: '0',
|
||||||
|
download: false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ const DL_OPEN_LIBRARIES = {
|
||||||
chromium: [],
|
chromium: [],
|
||||||
webkit: ['libGLESv2.so.2', 'libx264.so'],
|
webkit: ['libGLESv2.so.2', 'libx264.so'],
|
||||||
firefox: [],
|
firefox: [],
|
||||||
|
clank: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
async function validateDependencies(browserPath: string, browser: BrowserDescriptor) {
|
async function validateDependencies(browserPath: string, browser: BrowserDescriptor) {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import * as path from 'path';
|
||||||
import { getUbuntuVersionSync } from './ubuntuVersion';
|
import { getUbuntuVersionSync } from './ubuntuVersion';
|
||||||
import { getFromENV } from './utils';
|
import { getFromENV } from './utils';
|
||||||
|
|
||||||
export type BrowserName = 'chromium'|'webkit'|'firefox';
|
export type BrowserName = 'chromium'|'webkit'|'firefox'|'clank';
|
||||||
export type BrowserPlatform = 'win32'|'win64'|'mac10.13'|'mac10.14'|'mac10.15'|'mac11.0'|'ubuntu18.04'|'ubuntu20.04';
|
export type BrowserPlatform = 'win32'|'win64'|'mac10.13'|'mac10.14'|'mac10.15'|'mac11.0'|'ubuntu18.04'|'ubuntu20.04';
|
||||||
export type BrowserDescriptor = {
|
export type BrowserDescriptor = {
|
||||||
name: BrowserName,
|
name: BrowserName,
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ it('should scope context handles', async ({browser, server}) => {
|
||||||
] },
|
] },
|
||||||
{ _guid: 'BrowserType', objects: [] },
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
{ _guid: 'BrowserType', objects: [] },
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
{ _guid: 'Playwright', objects: [] },
|
{ _guid: 'Playwright', objects: [] },
|
||||||
{ _guid: 'Selectors', objects: [] },
|
{ _guid: 'Selectors', objects: [] },
|
||||||
{ _guid: 'Electron', objects: [] },
|
{ _guid: 'Electron', objects: [] },
|
||||||
|
|
@ -57,6 +58,7 @@ it('should scope context handles', async ({browser, server}) => {
|
||||||
await expectScopeState(browser, {
|
await expectScopeState(browser, {
|
||||||
_guid: '',
|
_guid: '',
|
||||||
objects: [
|
objects: [
|
||||||
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
{ _guid: 'BrowserType', objects: [] },
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
{ _guid: 'BrowserType', objects: [] },
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
{ _guid: 'BrowserType', objects: [
|
{ _guid: 'BrowserType', objects: [
|
||||||
|
|
@ -91,6 +93,7 @@ it('should scope CDPSession handles', (test, { browserName }) => {
|
||||||
] },
|
] },
|
||||||
{ _guid: 'BrowserType', objects: [] },
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
{ _guid: 'BrowserType', objects: [] },
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
{ _guid: 'Playwright', objects: [] },
|
{ _guid: 'Playwright', objects: [] },
|
||||||
{ _guid: 'Selectors', objects: [] },
|
{ _guid: 'Selectors', objects: [] },
|
||||||
{ _guid: 'Electron', objects: [] },
|
{ _guid: 'Electron', objects: [] },
|
||||||
|
|
@ -109,6 +112,7 @@ it('should scope CDPSession handles', (test, { browserName }) => {
|
||||||
] },
|
] },
|
||||||
{ _guid: 'BrowserType', objects: [] },
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
{ _guid: 'BrowserType', objects: [] },
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
{ _guid: 'Playwright', objects: [] },
|
{ _guid: 'Playwright', objects: [] },
|
||||||
{ _guid: 'Selectors', objects: [] },
|
{ _guid: 'Selectors', objects: [] },
|
||||||
{ _guid: 'Electron', objects: [] },
|
{ _guid: 'Electron', objects: [] },
|
||||||
|
|
@ -129,6 +133,7 @@ it('should scope browser handles', async ({browserType, browserOptions}) => {
|
||||||
{ _guid: 'Browser', objects: [] },
|
{ _guid: 'Browser', objects: [] },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
{ _guid: 'Playwright', objects: [] },
|
{ _guid: 'Playwright', objects: [] },
|
||||||
{ _guid: 'Selectors', objects: [] },
|
{ _guid: 'Selectors', objects: [] },
|
||||||
{ _guid: 'Electron', objects: [] },
|
{ _guid: 'Electron', objects: [] },
|
||||||
|
|
@ -152,6 +157,7 @@ it('should scope browser handles', async ({browserType, browserOptions}) => {
|
||||||
},
|
},
|
||||||
{ _guid: 'BrowserType', objects: [] },
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
{ _guid: 'BrowserType', objects: [] },
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
|
{ _guid: 'BrowserType', objects: [] },
|
||||||
{ _guid: 'Playwright', objects: [] },
|
{ _guid: 'Playwright', objects: [] },
|
||||||
{ _guid: 'Selectors', objects: [] },
|
{ _guid: 'Selectors', objects: [] },
|
||||||
{ _guid: 'Electron', objects: [] },
|
{ _guid: 'Electron', objects: [] },
|
||||||
|
|
|
||||||
|
|
@ -110,10 +110,11 @@ DEPS['src/server/common/'] = [];
|
||||||
// Strict dependencies for injected code.
|
// Strict dependencies for injected code.
|
||||||
DEPS['src/server/injected/'] = ['src/server/common/'];
|
DEPS['src/server/injected/'] = ['src/server/common/'];
|
||||||
|
|
||||||
// Electron uses chromium internally.
|
// Electron and Clank use chromium internally.
|
||||||
DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
|
DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
|
||||||
|
DEPS['src/server/clank/'] = [...DEPS['src/server/'], 'src/server/chromium/'];
|
||||||
|
|
||||||
DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/'];
|
DEPS['src/server/playwright.ts'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/server/webkit/', 'src/server/firefox/', 'src/server/clank/'];
|
||||||
DEPS['src/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerImpl.ts'] = ['src/**'];
|
DEPS['src/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerImpl.ts'] = ['src/**'];
|
||||||
|
|
||||||
// Tracing is a client/server plugin, nothing should depend on it.
|
// Tracing is a client/server plugin, nothing should depend on it.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue