Skip to content

Commit 28f6547

Browse files
authored
chore: add adb-based connectivity (#4375)
1 parent 06c8881 commit 28f6547

File tree

16 files changed

+463
-10
lines changed

16 files changed

+463
-10
lines changed

src/browserServerImpl.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,28 +47,26 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
4747
ignoreAllDefaultArgs: !!options.ignoreDefaultArgs && !Array.isArray(options.ignoreDefaultArgs),
4848
env: options.env ? envObjectToArray(options.env) : undefined,
4949
});
50-
return new BrowserServerImpl(this._browserType, browser, options.port);
50+
return new BrowserServerImpl(browser, options.port);
5151
}
5252
}
5353

5454
export class BrowserServerImpl extends EventEmitter implements BrowserServer {
5555
private _server: ws.Server;
56-
private _browserType: BrowserType;
5756
private _browser: Browser;
5857
private _wsEndpoint: string;
5958
private _process: ChildProcess;
6059

61-
constructor(browserType: BrowserType, browser: Browser, port: number = 0) {
60+
constructor(browser: Browser, port: number = 0) {
6261
super();
6362

64-
this._browserType = browserType;
6563
this._browser = browser;
6664

6765
const token = createGuid();
6866
this._server = new ws.Server({ port });
6967
const address = this._server.address();
7068
this._wsEndpoint = typeof address === 'string' ? `${address}/${token}` : `ws://127.0.0.1:${address.port}/${token}`;
71-
this._process = browser._options.browserProcess.process;
69+
this._process = browser._options.browserProcess.process!;
7270

7371
this._server.on('connection', (socket: ws, req) => {
7472
if (req.url !== '/' + token) {

src/client/playwright.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
3636
readonly chromium: BrowserType;
3737
readonly firefox: BrowserType;
3838
readonly webkit: BrowserType;
39+
readonly _clank: BrowserType;
3940
readonly devices: Devices;
4041
readonly selectors: Selectors;
4142
readonly errors: { TimeoutError: typeof TimeoutError };
@@ -45,6 +46,7 @@ export class Playwright extends ChannelOwner<channels.PlaywrightChannel, channel
4546
this.chromium = BrowserType.from(initializer.chromium);
4647
this.firefox = BrowserType.from(initializer.firefox);
4748
this.webkit = BrowserType.from(initializer.webkit);
49+
this._clank = BrowserType.from(initializer.clank);
4850
if (initializer.electron)
4951
(this as any).electron = Electron.from(initializer.electron);
5052
this.devices = {};

src/dispatchers/playwrightDispatcher.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class PlaywrightDispatcher extends Dispatcher<Playwright, channels.Playwr
3030
.map(([name, descriptor]) => ({ name, descriptor }));
3131
super(scope, playwright, 'Playwright', {
3232
chromium: new BrowserTypeDispatcher(scope, playwright.chromium),
33+
clank: new BrowserTypeDispatcher(scope, playwright.clank),
3334
firefox: new BrowserTypeDispatcher(scope, playwright.firefox),
3435
webkit: new BrowserTypeDispatcher(scope, playwright.webkit),
3536
electron: electron ? new ElectronDispatcher(scope, electron) : undefined,

src/protocol/channels.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export type SerializedError = {
9292
// ----------- Playwright -----------
9393
export type PlaywrightInitializer = {
9494
chromium: BrowserTypeChannel,
95+
clank: BrowserTypeChannel,
9596
firefox: BrowserTypeChannel,
9697
webkit: BrowserTypeChannel,
9798
electron?: ElectronChannel,

src/protocol/protocol.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ Playwright:
130130

131131
initializer:
132132
chromium: BrowserType
133+
clank: BrowserType
133134
firefox: BrowserType
134135
webkit: BrowserType
135136
electron: Electron?

src/server/browser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { ChildProcess } from 'child_process';
2424

2525
export interface BrowserProcess {
2626
onclose: ((exitCode: number | null, signal: string | null) => void) | undefined;
27-
process: ChildProcess;
27+
process?: ChildProcess;
2828
kill(): Promise<void>;
2929
close(): Promise<void>;
3030
}

src/server/chromium/crBrowser.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,10 @@ export class CRBrowser extends Browser {
118118
return this._version;
119119
}
120120

121+
isClank(): boolean {
122+
return this._options.name === 'clank';
123+
}
124+
121125
_onAttachedToTarget({targetInfo, sessionId, waitingForDebugger}: Protocol.Target.attachedToTargetPayload) {
122126
if (targetInfo.type === 'browser')
123127
return;

src/server/chromium/crPage.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,7 +380,9 @@ class FrameSession {
380380
}
381381

382382
async _initialize(hasUIWindow: boolean) {
383-
if (hasUIWindow && !this._crPage._browserContext._options.noDefaultViewport) {
383+
if (hasUIWindow &&
384+
!this._crPage._browserContext._browser.isClank() &&
385+
!this._crPage._browserContext._options.noDefaultViewport) {
384386
const { windowId } = await this._client.send('Browser.getWindowForTarget');
385387
this._windowId = windowId;
386388
}
@@ -825,6 +827,8 @@ class FrameSession {
825827
}
826828

827829
async _updateViewport(): Promise<void> {
830+
if (this._crPage._browserContext._browser.isClank())
831+
return;
828832
assert(this._isMainFrame());
829833
const options = this._crPage._browserContext._options;
830834
const viewportSize = this._page._state.viewportSize;
@@ -863,6 +867,8 @@ class FrameSession {
863867
}
864868

865869
async _updateEmulateMedia(initial: boolean): Promise<void> {
870+
if (this._crPage._browserContext._browser.isClank())
871+
return;
866872
const colorScheme = this._page._state.colorScheme || this._crPage._browserContext._options.colorScheme || 'light';
867873
const features = colorScheme ? [{ name: 'prefers-color-scheme', value: colorScheme }] : [];
868874
await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features });

src/server/clank/android.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* Copyright Microsoft Corporation. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as debug from 'debug';
18+
import { EventEmitter } from 'events';
19+
import * as stream from 'stream';
20+
import * as ws from 'ws';
21+
import { makeWaitForNextTask } from '../../utils/utils';
22+
23+
export interface Backend {
24+
devices(): Promise<DeviceBackend[]>;
25+
}
26+
27+
export interface DeviceBackend {
28+
close(): Promise<void>;
29+
init(): Promise<void>;
30+
runCommand(command: string): Promise<string>;
31+
open(command: string): Promise<SocketBackend>;
32+
}
33+
34+
export interface SocketBackend extends EventEmitter {
35+
write(data: Buffer): Promise<void>;
36+
close(): Promise<void>;
37+
}
38+
39+
export class AndroidClient {
40+
backend: Backend;
41+
42+
constructor(backend: Backend) {
43+
this.backend = backend;
44+
}
45+
46+
async devices(): Promise<AndroidDevice[]> {
47+
const devices = await this.backend.devices();
48+
return devices.map(b => new AndroidDevice(b));
49+
}
50+
}
51+
52+
export class AndroidDevice {
53+
readonly backend: DeviceBackend;
54+
private _model: string | undefined;
55+
56+
constructor(backend: DeviceBackend) {
57+
this.backend = backend;
58+
}
59+
60+
async init() {
61+
await this.backend.init();
62+
this._model = await this.backend.runCommand('shell:getprop ro.product.model');
63+
}
64+
65+
async close() {
66+
await this.backend.close();
67+
}
68+
69+
async launchBrowser(packageName: string): Promise<AndroidBrowser> {
70+
debug('pw:android')('Force-stopping', packageName);
71+
await this.backend.runCommand(`shell:am force-stop ${packageName}`);
72+
const hasDefaultSocket = !!(await this.backend.runCommand(`shell:cat /proc/net/unix | grep chrome_devtools_remote$`));
73+
debug('pw:android')('Starting', packageName);
74+
await this.backend.runCommand(`shell:am start -n ${packageName}/com.google.android.apps.chrome.Main about:blank`);
75+
let pid = 0;
76+
debug('pw:android')('Polling pid for', packageName);
77+
while (!pid) {
78+
const ps = (await this.backend.runCommand(`shell:ps -A | grep ${packageName}`)).split('\n');
79+
const proc = ps.find(line => line.endsWith(packageName));
80+
if (proc)
81+
pid = +proc.replace(/\s+/g, ' ').split(' ')[1];
82+
await new Promise(f => setTimeout(f, 100));
83+
}
84+
debug('pw:android')('PID=' + pid);
85+
const socketName = hasDefaultSocket ? `chrome_devtools_remote_${pid}` : 'chrome_devtools_remote';
86+
debug('pw:android')('Polling for socket', socketName);
87+
while (true) {
88+
const net = await this.backend.runCommand(`shell:cat /proc/net/unix | grep ${socketName}$`);
89+
if (net)
90+
break;
91+
await new Promise(f => setTimeout(f, 100));
92+
}
93+
debug('pw:android')('Got the socket, connecting');
94+
const browser = new AndroidBrowser(this, packageName, socketName, pid);
95+
await browser._open();
96+
return browser;
97+
}
98+
99+
model(): string | undefined {
100+
return this._model;
101+
}
102+
}
103+
104+
export class AndroidBrowser extends EventEmitter {
105+
readonly device: AndroidDevice;
106+
readonly socketName: string;
107+
readonly pid: number;
108+
private _socket: SocketBackend | undefined;
109+
private _receiver: stream.Writable;
110+
private _waitForNextTask = makeWaitForNextTask();
111+
onmessage?: (message: any) => void;
112+
onclose?: () => void;
113+
private _packageName: string;
114+
115+
constructor(device: AndroidDevice, packageName: string, socketName: string, pid: number) {
116+
super();
117+
this._packageName = packageName;
118+
this.device = device;
119+
this.socketName = socketName;
120+
this.pid = pid;
121+
this._receiver = new (ws as any).Receiver() as stream.Writable;
122+
this._receiver.on('message', message => {
123+
this._waitForNextTask(() => {
124+
if (this.onmessage)
125+
this.onmessage(JSON.parse(message));
126+
});
127+
});
128+
}
129+
130+
async _open() {
131+
this._socket = await this.device.backend.open(`localabstract:${this.socketName}`);
132+
this._socket.on('close', () => {
133+
this._waitForNextTask(() => {
134+
if (this.onclose)
135+
this.onclose();
136+
});
137+
});
138+
await this._socket.write(Buffer.from(`GET /devtools/browser HTTP/1.1\r
139+
Upgrade: WebSocket\r
140+
Connection: Upgrade\r
141+
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
142+
Sec-WebSocket-Version: 13\r
143+
\r
144+
`));
145+
// HTTP Upgrade response.
146+
await new Promise(f => this._socket!.once('data', f));
147+
148+
// Start sending web frame to receiver.
149+
this._socket.on('data', data => this._receiver._write(data, 'binary', () => {}));
150+
}
151+
152+
async send(s: any) {
153+
await this._socket!.write(encodeWebFrame(JSON.stringify(s)));
154+
}
155+
156+
async close() {
157+
await this._socket!.close();
158+
await this.device.backend.runCommand(`shell:am force-stop ${this._packageName}`);
159+
}
160+
}
161+
162+
function encodeWebFrame(data: string): Buffer {
163+
return (ws as any).Sender.frame(Buffer.from(data), {
164+
opcode: 1,
165+
mask: true,
166+
fin: true,
167+
readOnly: true
168+
})[0];
169+
}

0 commit comments

Comments
 (0)