android.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.AndroidDevice = exports.Android = void 0;
  6. var _utilsBundle = require("../../utilsBundle");
  7. var _events = require("events");
  8. var _fs = _interopRequireDefault(require("fs"));
  9. var _os = _interopRequireDefault(require("os"));
  10. var _path = _interopRequireDefault(require("path"));
  11. var _utils = require("../../utils");
  12. var _fileUtils = require("../../utils/fileUtils");
  13. var _browserContext = require("../browserContext");
  14. var _progress = require("../progress");
  15. var _crBrowser = require("../chromium/crBrowser");
  16. var _helper = require("../helper");
  17. var _transport = require("../../protocol/transport");
  18. var _debugLogger = require("../../common/debugLogger");
  19. var _processLauncher = require("../../utils/processLauncher");
  20. var _timeoutSettings = require("../../common/timeoutSettings");
  21. var _instrumentation = require("../instrumentation");
  22. var _chromiumSwitches = require("../chromium/chromiumSwitches");
  23. var _registry = require("../registry");
  24. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  25. /**
  26. * Copyright Microsoft Corporation. All rights reserved.
  27. *
  28. * Licensed under the Apache License, Version 2.0 (the "License");
  29. * you may not use this file except in compliance with the License.
  30. * You may obtain a copy of the License at
  31. *
  32. * http://www.apache.org/licenses/LICENSE-2.0
  33. *
  34. * Unless required by applicable law or agreed to in writing, software
  35. * distributed under the License is distributed on an "AS IS" BASIS,
  36. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  37. * See the License for the specific language governing permissions and
  38. * limitations under the License.
  39. */
  40. const ARTIFACTS_FOLDER = _path.default.join(_os.default.tmpdir(), 'playwright-artifacts-');
  41. class Android extends _instrumentation.SdkObject {
  42. constructor(parent, backend) {
  43. super(parent, 'android');
  44. this._backend = void 0;
  45. this._devices = new Map();
  46. this._timeoutSettings = void 0;
  47. this._backend = backend;
  48. this._timeoutSettings = new _timeoutSettings.TimeoutSettings();
  49. }
  50. setDefaultTimeout(timeout) {
  51. this._timeoutSettings.setDefaultTimeout(timeout);
  52. }
  53. async devices(options) {
  54. const devices = (await this._backend.devices(options)).filter(d => d.status === 'device');
  55. const newSerials = new Set();
  56. for (const d of devices) {
  57. newSerials.add(d.serial);
  58. if (this._devices.has(d.serial)) continue;
  59. const device = await AndroidDevice.create(this, d, options);
  60. this._devices.set(d.serial, device);
  61. }
  62. for (const d of this._devices.keys()) {
  63. if (!newSerials.has(d)) this._devices.delete(d);
  64. }
  65. return [...this._devices.values()];
  66. }
  67. _deviceClosed(device) {
  68. this._devices.delete(device.serial);
  69. }
  70. }
  71. exports.Android = Android;
  72. class AndroidDevice extends _instrumentation.SdkObject {
  73. constructor(android, backend, model, options) {
  74. super(android, 'android-device');
  75. this._backend = void 0;
  76. this.model = void 0;
  77. this.serial = void 0;
  78. this._options = void 0;
  79. this._driverPromise = void 0;
  80. this._lastId = 0;
  81. this._callbacks = new Map();
  82. this._pollingWebViews = void 0;
  83. this._timeoutSettings = void 0;
  84. this._webViews = new Map();
  85. this._browserConnections = new Set();
  86. this._android = void 0;
  87. this._isClosed = false;
  88. this._android = android;
  89. this._backend = backend;
  90. this.model = model;
  91. this.serial = backend.serial;
  92. this._options = options;
  93. this._timeoutSettings = new _timeoutSettings.TimeoutSettings(android._timeoutSettings);
  94. }
  95. static async create(android, backend, options) {
  96. await backend.init();
  97. const model = await backend.runCommand('shell:getprop ro.product.model');
  98. const device = new AndroidDevice(android, backend, model.toString().trim(), options);
  99. await device._init();
  100. return device;
  101. }
  102. async _init() {
  103. await this._refreshWebViews();
  104. const poll = () => {
  105. this._pollingWebViews = setTimeout(() => this._refreshWebViews().then(poll).catch(() => {
  106. this.close().catch(() => {});
  107. }), 500);
  108. };
  109. poll();
  110. }
  111. setDefaultTimeout(timeout) {
  112. this._timeoutSettings.setDefaultTimeout(timeout);
  113. }
  114. async shell(command) {
  115. const result = await this._backend.runCommand(`shell:${command}`);
  116. await this._refreshWebViews();
  117. return result;
  118. }
  119. async open(command) {
  120. return await this._backend.open(`${command}`);
  121. }
  122. async screenshot() {
  123. return await this._backend.runCommand(`shell:screencap -p`);
  124. }
  125. async _driver() {
  126. if (this._isClosed) return;
  127. if (!this._driverPromise) this._driverPromise = this._installDriver();
  128. return this._driverPromise;
  129. }
  130. async _installDriver() {
  131. (0, _utilsBundle.debug)('pw:android')('Stopping the old driver');
  132. await this.shell(`am force-stop com.microsoft.playwright.androiddriver`);
  133. // uninstall and install driver on every execution
  134. if (!this._options.omitDriverInstall) {
  135. (0, _utilsBundle.debug)('pw:android')('Uninstalling the old driver');
  136. await this.shell(`cmd package uninstall com.microsoft.playwright.androiddriver`);
  137. await this.shell(`cmd package uninstall com.microsoft.playwright.androiddriver.test`);
  138. (0, _utilsBundle.debug)('pw:android')('Installing the new driver');
  139. const executable = _registry.registry.findExecutable('android');
  140. const packageManagerCommand = (0, _utils.getPackageManagerExecCommand)();
  141. for (const file of ['android-driver.apk', 'android-driver-target.apk']) {
  142. const fullName = _path.default.join(executable.directory, file);
  143. if (!_fs.default.existsSync(fullName)) throw new Error(`Please install Android driver apk using '${packageManagerCommand} playwright install android'`);
  144. await this.installApk(await _fs.default.promises.readFile(fullName));
  145. }
  146. } else {
  147. (0, _utilsBundle.debug)('pw:android')('Skipping the driver installation');
  148. }
  149. (0, _utilsBundle.debug)('pw:android')('Starting the new driver');
  150. this.shell('am instrument -w com.microsoft.playwright.androiddriver.test/androidx.test.runner.AndroidJUnitRunner').catch(e => (0, _utilsBundle.debug)('pw:android')(e));
  151. const socket = await this._waitForLocalAbstract('playwright_android_driver_socket');
  152. const transport = new _transport.PipeTransport(socket, socket, socket, 'be');
  153. transport.onmessage = message => {
  154. const response = JSON.parse(message);
  155. const {
  156. id,
  157. result,
  158. error
  159. } = response;
  160. const callback = this._callbacks.get(id);
  161. if (!callback) return;
  162. if (error) callback.reject(new Error(error));else callback.fulfill(result);
  163. this._callbacks.delete(id);
  164. };
  165. return transport;
  166. }
  167. async _waitForLocalAbstract(socketName) {
  168. let socket;
  169. (0, _utilsBundle.debug)('pw:android')(`Polling the socket localabstract:${socketName}`);
  170. while (!socket) {
  171. try {
  172. socket = await this._backend.open(`localabstract:${socketName}`);
  173. } catch (e) {
  174. await new Promise(f => setTimeout(f, 250));
  175. }
  176. }
  177. (0, _utilsBundle.debug)('pw:android')(`Connected to localabstract:${socketName}`);
  178. return socket;
  179. }
  180. async send(method, params = {}) {
  181. // Patch the timeout in!
  182. params.timeout = this._timeoutSettings.timeout(params);
  183. const driver = await this._driver();
  184. if (!driver) throw new Error('Device is closed');
  185. const id = ++this._lastId;
  186. const result = new Promise((fulfill, reject) => this._callbacks.set(id, {
  187. fulfill,
  188. reject
  189. }));
  190. driver.send(JSON.stringify({
  191. id,
  192. method,
  193. params
  194. }));
  195. return result;
  196. }
  197. async close() {
  198. if (this._isClosed) return;
  199. this._isClosed = true;
  200. if (this._pollingWebViews) clearTimeout(this._pollingWebViews);
  201. for (const connection of this._browserConnections) await connection.close();
  202. if (this._driverPromise) {
  203. const driver = await this._driver();
  204. driver === null || driver === void 0 ? void 0 : driver.close();
  205. }
  206. await this._backend.close();
  207. this._android._deviceClosed(this);
  208. this.emit(AndroidDevice.Events.Close);
  209. }
  210. async launchBrowser(pkg = 'com.android.chrome', options) {
  211. (0, _utilsBundle.debug)('pw:android')('Force-stopping', pkg);
  212. await this._backend.runCommand(`shell:am force-stop ${pkg}`);
  213. const socketName = (0, _utils.isUnderTest)() ? 'webview_devtools_remote_playwright_test' : 'playwright_' + (0, _utils.createGuid)() + '_devtools_remote';
  214. const commandLine = this._defaultArgs(options, socketName).join(' ');
  215. (0, _utilsBundle.debug)('pw:android')('Starting', pkg, commandLine);
  216. // encode commandLine to base64 to avoid issues (bash encoding) with special characters
  217. await this._backend.runCommand(`shell:echo "${Buffer.from(commandLine).toString('base64')}" | base64 -d > /data/local/tmp/chrome-command-line`);
  218. await this._backend.runCommand(`shell:am start -a android.intent.action.VIEW -d about:blank ${pkg}`);
  219. const browserContext = await this._connectToBrowser(socketName, options);
  220. await this._backend.runCommand(`shell:rm /data/local/tmp/chrome-command-line`);
  221. return browserContext;
  222. }
  223. _defaultArgs(options, socketName) {
  224. const chromeArguments = ['_', '--disable-fre', '--no-default-browser-check', `--remote-debugging-socket-name=${socketName}`, ..._chromiumSwitches.chromiumSwitches, ...this._innerDefaultArgs(options)];
  225. return chromeArguments;
  226. }
  227. _innerDefaultArgs(options) {
  228. const {
  229. args = [],
  230. proxy
  231. } = options;
  232. const chromeArguments = [];
  233. if (proxy) {
  234. chromeArguments.push(`--proxy-server=${proxy.server}`);
  235. const proxyBypassRules = [];
  236. if (proxy.bypass) proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
  237. if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>')) proxyBypassRules.push('<-loopback>');
  238. if (proxyBypassRules.length > 0) chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`);
  239. }
  240. chromeArguments.push(...args);
  241. return chromeArguments;
  242. }
  243. async connectToWebView(socketName) {
  244. const webView = this._webViews.get(socketName);
  245. if (!webView) throw new Error('WebView has been closed');
  246. return await this._connectToBrowser(socketName);
  247. }
  248. async _connectToBrowser(socketName, options = {}) {
  249. const socket = await this._waitForLocalAbstract(socketName);
  250. const androidBrowser = new AndroidBrowser(this, socket);
  251. await androidBrowser._init();
  252. this._browserConnections.add(androidBrowser);
  253. const artifactsDir = await _fs.default.promises.mkdtemp(ARTIFACTS_FOLDER);
  254. const cleanupArtifactsDir = async () => {
  255. const errors = await (0, _fileUtils.removeFolders)([artifactsDir]);
  256. for (let i = 0; i < (errors || []).length; ++i) (0, _utilsBundle.debug)('pw:android')(`exception while removing ${artifactsDir}: ${errors[i]}`);
  257. };
  258. _processLauncher.gracefullyCloseSet.add(cleanupArtifactsDir);
  259. socket.on('close', async () => {
  260. _processLauncher.gracefullyCloseSet.delete(cleanupArtifactsDir);
  261. cleanupArtifactsDir().catch(e => (0, _utilsBundle.debug)('pw:android')(`could not cleanup artifacts dir: ${e}`));
  262. });
  263. const browserOptions = {
  264. name: 'clank',
  265. isChromium: true,
  266. slowMo: 0,
  267. persistent: {
  268. ...options,
  269. noDefaultViewport: true
  270. },
  271. artifactsDir,
  272. downloadsPath: artifactsDir,
  273. tracesDir: artifactsDir,
  274. browserProcess: new ClankBrowserProcess(androidBrowser),
  275. proxy: options.proxy,
  276. protocolLogger: _helper.helper.debugProtocolLogger(),
  277. browserLogsCollector: new _debugLogger.RecentLogsCollector(),
  278. originalLaunchOptions: {}
  279. };
  280. (0, _browserContext.validateBrowserContextOptions)(options, browserOptions);
  281. const browser = await _crBrowser.CRBrowser.connect(this.attribution.playwright, androidBrowser, browserOptions);
  282. const controller = new _progress.ProgressController((0, _instrumentation.serverSideCallMetadata)(), this);
  283. const defaultContext = browser._defaultContext;
  284. await controller.run(async progress => {
  285. await defaultContext._loadDefaultContextAsIs(progress);
  286. });
  287. return defaultContext;
  288. }
  289. webViews() {
  290. return [...this._webViews.values()];
  291. }
  292. async installApk(content, options) {
  293. const args = options && options.args ? options.args : ['-r', '-t', '-S'];
  294. (0, _utilsBundle.debug)('pw:android')('Opening install socket');
  295. const installSocket = await this._backend.open(`shell:cmd package install ${args.join(' ')} ${content.length}`);
  296. (0, _utilsBundle.debug)('pw:android')('Writing driver bytes: ' + content.length);
  297. await installSocket.write(content);
  298. const success = await new Promise(f => installSocket.on('data', f));
  299. (0, _utilsBundle.debug)('pw:android')('Written driver bytes: ' + success);
  300. installSocket.close();
  301. }
  302. async push(content, path, mode = 0o644) {
  303. const socket = await this._backend.open(`sync:`);
  304. const sendHeader = async (command, length) => {
  305. const buffer = Buffer.alloc(command.length + 4);
  306. buffer.write(command, 0);
  307. buffer.writeUInt32LE(length, command.length);
  308. await socket.write(buffer);
  309. };
  310. const send = async (command, data) => {
  311. await sendHeader(command, data.length);
  312. await socket.write(data);
  313. };
  314. await send('SEND', Buffer.from(`${path},${mode}`));
  315. const maxChunk = 65535;
  316. for (let i = 0; i < content.length; i += maxChunk) await send('DATA', content.slice(i, i + maxChunk));
  317. await sendHeader('DONE', Date.now() / 1000 | 0);
  318. const result = await new Promise(f => socket.once('data', f));
  319. const code = result.slice(0, 4).toString();
  320. if (code !== 'OKAY') throw new Error('Could not push: ' + code);
  321. socket.close();
  322. }
  323. async _refreshWebViews() {
  324. // possible socketName, eg: webview_devtools_remote_32327, webview_devtools_remote_32327_zeus, webview_devtools_remote_zeus
  325. const sockets = (await this._backend.runCommand(`shell:cat /proc/net/unix | grep webview_devtools_remote`)).toString().split('\n');
  326. if (this._isClosed) return;
  327. const socketNames = new Set();
  328. for (const line of sockets) {
  329. const matchSocketName = line.match(/[^@]+@(.*?webview_devtools_remote_?.*)/);
  330. if (!matchSocketName) continue;
  331. const socketName = matchSocketName[1];
  332. socketNames.add(socketName);
  333. if (this._webViews.has(socketName)) continue;
  334. // possible line: 0000000000000000: 00000002 00000000 00010000 0001 01 5841881 @webview_devtools_remote_zeus
  335. // the result: match[1] = ''
  336. const match = line.match(/[^@]+@.*?webview_devtools_remote_?(\d*)/);
  337. let pid = -1;
  338. if (match && match[1]) pid = +match[1];
  339. const pkg = await this._extractPkg(pid);
  340. if (this._isClosed) return;
  341. const webView = {
  342. pid,
  343. pkg,
  344. socketName
  345. };
  346. this._webViews.set(socketName, webView);
  347. this.emit(AndroidDevice.Events.WebViewAdded, webView);
  348. }
  349. for (const p of this._webViews.keys()) {
  350. if (!socketNames.has(p)) {
  351. this._webViews.delete(p);
  352. this.emit(AndroidDevice.Events.WebViewRemoved, p);
  353. }
  354. }
  355. }
  356. async _extractPkg(pid) {
  357. let pkg = '';
  358. if (pid === -1) return pkg;
  359. const procs = (await this._backend.runCommand(`shell:ps -A | grep ${pid}`)).toString().split('\n');
  360. for (const proc of procs) {
  361. const match = proc.match(/[^\s]+\s+(\d+).*$/);
  362. if (!match) continue;
  363. pkg = proc.substring(proc.lastIndexOf(' ') + 1);
  364. }
  365. return pkg;
  366. }
  367. }
  368. exports.AndroidDevice = AndroidDevice;
  369. AndroidDevice.Events = {
  370. WebViewAdded: 'webViewAdded',
  371. WebViewRemoved: 'webViewRemoved',
  372. Close: 'close'
  373. };
  374. class AndroidBrowser extends _events.EventEmitter {
  375. constructor(device, socket) {
  376. super();
  377. this.device = void 0;
  378. this._socket = void 0;
  379. this._receiver = void 0;
  380. this._waitForNextTask = (0, _utils.makeWaitForNextTask)();
  381. this.onmessage = void 0;
  382. this.onclose = void 0;
  383. this.setMaxListeners(0);
  384. this.device = device;
  385. this._socket = socket;
  386. this._socket.on('close', () => {
  387. this._waitForNextTask(() => {
  388. if (this.onclose) this.onclose();
  389. });
  390. });
  391. this._receiver = new _utilsBundle.wsReceiver();
  392. this._receiver.on('message', message => {
  393. this._waitForNextTask(() => {
  394. if (this.onmessage) this.onmessage(JSON.parse(message));
  395. });
  396. });
  397. }
  398. async _init() {
  399. await this._socket.write(Buffer.from(`GET /devtools/browser HTTP/1.1\r
  400. Upgrade: WebSocket\r
  401. Connection: Upgrade\r
  402. Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r
  403. Sec-WebSocket-Version: 13\r
  404. \r
  405. `));
  406. // HTTP Upgrade response.
  407. await new Promise(f => this._socket.once('data', f));
  408. // Start sending web frame to receiver.
  409. this._socket.on('data', data => this._receiver._write(data, 'binary', () => {}));
  410. }
  411. async send(s) {
  412. await this._socket.write(encodeWebFrame(JSON.stringify(s)));
  413. }
  414. async close() {
  415. this._socket.close();
  416. }
  417. }
  418. function encodeWebFrame(data) {
  419. return _utilsBundle.wsSender.frame(Buffer.from(data), {
  420. opcode: 1,
  421. mask: true,
  422. fin: true,
  423. readOnly: true
  424. })[0];
  425. }
  426. class ClankBrowserProcess {
  427. constructor(browser) {
  428. this._browser = void 0;
  429. this.onclose = void 0;
  430. this._browser = browser;
  431. }
  432. async kill() {}
  433. async close() {
  434. await this._browser.close();
  435. }
  436. }