chromium.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.Chromium = void 0;
  6. var _fs = _interopRequireDefault(require("fs"));
  7. var _os = _interopRequireDefault(require("os"));
  8. var _path = _interopRequireDefault(require("path"));
  9. var _crBrowser = require("./crBrowser");
  10. var _processLauncher = require("../../utils/processLauncher");
  11. var _crConnection = require("./crConnection");
  12. var _browserType = require("../browserType");
  13. var _transport = require("../transport");
  14. var _crDevTools = require("./crDevTools");
  15. var _browser = require("../browser");
  16. var _network = require("../../utils/network");
  17. var _userAgent = require("../../utils/userAgent");
  18. var _ascii = require("../../utils/ascii");
  19. var _utils = require("../../utils");
  20. var _fileUtils = require("../../utils/fileUtils");
  21. var _debugLogger = require("../../common/debugLogger");
  22. var _progress = require("../progress");
  23. var _timeoutSettings = require("../../common/timeoutSettings");
  24. var _helper = require("../helper");
  25. var _registry = require("../registry");
  26. var _manualPromise = require("../../utils/manualPromise");
  27. var _browserContext = require("../browserContext");
  28. var _chromiumSwitches = require("./chromiumSwitches");
  29. function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
  30. /**
  31. * Copyright 2017 Google Inc. All rights reserved.
  32. * Modifications copyright (c) Microsoft Corporation.
  33. *
  34. * Licensed under the Apache License, Version 2.0 (the "License");
  35. * you may not use this file except in compliance with the License.
  36. * You may obtain a copy of the License at
  37. *
  38. * http://www.apache.org/licenses/LICENSE-2.0
  39. *
  40. * Unless required by applicable law or agreed to in writing, software
  41. * distributed under the License is distributed on an "AS IS" BASIS,
  42. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  43. * See the License for the specific language governing permissions and
  44. * limitations under the License.
  45. */
  46. const ARTIFACTS_FOLDER = _path.default.join(_os.default.tmpdir(), 'playwright-artifacts-');
  47. class Chromium extends _browserType.BrowserType {
  48. constructor(parent) {
  49. super(parent, 'chromium');
  50. this._devtools = void 0;
  51. if ((0, _utils.debugMode)()) this._devtools = this._createDevTools();
  52. }
  53. async connectOverCDP(metadata, endpointURL, options, timeout) {
  54. const controller = new _progress.ProgressController(metadata, this);
  55. controller.setLogName('browser');
  56. return controller.run(async progress => {
  57. return await this._connectOverCDPInternal(progress, endpointURL, options);
  58. }, _timeoutSettings.TimeoutSettings.timeout({
  59. timeout
  60. }));
  61. }
  62. async _connectOverCDPInternal(progress, endpointURL, options, onClose) {
  63. let headersMap;
  64. if (options.headers) headersMap = (0, _utils.headersArrayToObject)(options.headers, false);
  65. if (!headersMap) headersMap = {
  66. 'User-Agent': (0, _userAgent.getUserAgent)()
  67. };else if (headersMap && !Object.keys(headersMap).some(key => key.toLowerCase() === 'user-agent')) headersMap['User-Agent'] = (0, _userAgent.getUserAgent)();
  68. const artifactsDir = await _fs.default.promises.mkdtemp(ARTIFACTS_FOLDER);
  69. const wsEndpoint = await urlToWSEndpoint(progress, endpointURL);
  70. progress.throwIfAborted();
  71. const chromeTransport = await _transport.WebSocketTransport.connect(progress, wsEndpoint, headersMap);
  72. const cleanedUp = new _manualPromise.ManualPromise();
  73. const doCleanup = async () => {
  74. await (0, _fileUtils.removeFolders)([artifactsDir]);
  75. await (onClose === null || onClose === void 0 ? void 0 : onClose());
  76. cleanedUp.resolve();
  77. };
  78. const doClose = async () => {
  79. await chromeTransport.closeAndWait();
  80. await cleanedUp;
  81. };
  82. const browserProcess = {
  83. close: doClose,
  84. kill: doClose
  85. };
  86. const persistent = {
  87. noDefaultViewport: true
  88. };
  89. const browserOptions = {
  90. slowMo: options.slowMo,
  91. name: 'chromium',
  92. isChromium: true,
  93. persistent,
  94. browserProcess,
  95. protocolLogger: _helper.helper.debugProtocolLogger(),
  96. browserLogsCollector: new _debugLogger.RecentLogsCollector(),
  97. artifactsDir,
  98. downloadsPath: options.downloadsPath || artifactsDir,
  99. tracesDir: options.tracesDir || artifactsDir,
  100. // On Windows context level proxies only work, if there isn't a global proxy
  101. // set. This is currently a bug in the CR/Windows networking stack. By
  102. // passing an arbitrary value we disable the check in PW land which warns
  103. // users in normal (launch/launchServer) mode since otherwise connectOverCDP
  104. // does not work at all with proxies on Windows.
  105. proxy: {
  106. server: 'per-context'
  107. },
  108. originalLaunchOptions: {}
  109. };
  110. (0, _browserContext.validateBrowserContextOptions)(persistent, browserOptions);
  111. progress.throwIfAborted();
  112. const browser = await _crBrowser.CRBrowser.connect(this.attribution.playwright, chromeTransport, browserOptions);
  113. browser._isCollocatedWithServer = false;
  114. browser.on(_browser.Browser.Events.Disconnected, doCleanup);
  115. return browser;
  116. }
  117. _createDevTools() {
  118. // TODO: this is totally wrong when using channels.
  119. const directory = _registry.registry.findExecutable('chromium').directory;
  120. return directory ? new _crDevTools.CRDevTools(_path.default.join(directory, 'devtools-preferences.json')) : undefined;
  121. }
  122. async _connectToTransport(transport, options) {
  123. let devtools = this._devtools;
  124. if (options.__testHookForDevTools) {
  125. devtools = this._createDevTools();
  126. await options.__testHookForDevTools(devtools);
  127. }
  128. return _crBrowser.CRBrowser.connect(this.attribution.playwright, transport, options, devtools);
  129. }
  130. _doRewriteStartupLog(error) {
  131. if (!error.logs) return error;
  132. if (error.logs.includes('Missing X server')) error.logs = '\n' + (0, _ascii.wrapInASCIIBox)(_browserType.kNoXServerRunningError, 1);
  133. // These error messages are taken from Chromium source code as of July, 2020:
  134. // https://github.com/chromium/chromium/blob/70565f67e79f79e17663ad1337dc6e63ee207ce9/content/browser/zygote_host/zygote_host_impl_linux.cc
  135. if (!error.logs.includes('crbug.com/357670') && !error.logs.includes('No usable sandbox!') && !error.logs.includes('crbug.com/638180')) return error;
  136. error.logs = [`Chromium sandboxing failed!`, `================================`, `To workaround sandboxing issues, do either of the following:`, ` - (preferred): Configure environment to support sandboxing: https://playwright.dev/docs/troubleshooting`, ` - (alternative): Launch Chromium without sandbox using 'chromiumSandbox: false' option`, `================================`, ``].join('\n');
  137. return error;
  138. }
  139. _amendEnvironment(env, userDataDir, executable, browserArguments) {
  140. return env;
  141. }
  142. _attemptToGracefullyCloseBrowser(transport) {
  143. const message = {
  144. method: 'Browser.close',
  145. id: _crConnection.kBrowserCloseMessageId,
  146. params: {}
  147. };
  148. transport.send(message);
  149. }
  150. async _launchWithSeleniumHub(progress, hubUrl, options) {
  151. await this._createArtifactDirs(options);
  152. if (!hubUrl.endsWith('/')) hubUrl = hubUrl + '/';
  153. const args = this._innerDefaultArgs(options);
  154. args.push('--remote-debugging-port=0');
  155. const isEdge = options.channel && options.channel.startsWith('msedge');
  156. let desiredCapabilities = {
  157. 'browserName': isEdge ? 'MicrosoftEdge' : 'chrome',
  158. [isEdge ? 'ms:edgeOptions' : 'goog:chromeOptions']: {
  159. args
  160. }
  161. };
  162. if (process.env.SELENIUM_REMOTE_CAPABILITIES) {
  163. const remoteCapabilities = parseSeleniumRemoteParams({
  164. name: 'capabilities',
  165. value: process.env.SELENIUM_REMOTE_CAPABILITIES
  166. }, progress);
  167. if (remoteCapabilities) desiredCapabilities = {
  168. ...desiredCapabilities,
  169. ...remoteCapabilities
  170. };
  171. }
  172. let headers = {};
  173. if (process.env.SELENIUM_REMOTE_HEADERS) {
  174. const remoteHeaders = parseSeleniumRemoteParams({
  175. name: 'headers',
  176. value: process.env.SELENIUM_REMOTE_HEADERS
  177. }, progress);
  178. if (remoteHeaders) headers = remoteHeaders;
  179. }
  180. progress.log(`<selenium> connecting to ${hubUrl}`);
  181. const response = await (0, _network.fetchData)({
  182. url: hubUrl + 'session',
  183. method: 'POST',
  184. headers: {
  185. 'Content-Type': 'application/json; charset=utf-8',
  186. ...headers
  187. },
  188. data: JSON.stringify({
  189. capabilities: {
  190. alwaysMatch: desiredCapabilities
  191. }
  192. }),
  193. timeout: progress.timeUntilDeadline()
  194. }, seleniumErrorHandler);
  195. const value = JSON.parse(response).value;
  196. const sessionId = value.sessionId;
  197. progress.log(`<selenium> connected to sessionId=${sessionId}`);
  198. const disconnectFromSelenium = async () => {
  199. progress.log(`<selenium> disconnecting from sessionId=${sessionId}`);
  200. await (0, _network.fetchData)({
  201. url: hubUrl + 'session/' + sessionId,
  202. method: 'DELETE',
  203. headers
  204. }).catch(error => progress.log(`<error disconnecting from selenium>: ${error}`));
  205. progress.log(`<selenium> disconnected from sessionId=${sessionId}`);
  206. _processLauncher.gracefullyCloseSet.delete(disconnectFromSelenium);
  207. };
  208. _processLauncher.gracefullyCloseSet.add(disconnectFromSelenium);
  209. try {
  210. const capabilities = value.capabilities;
  211. let endpointURL;
  212. if (capabilities['se:cdp']) {
  213. // Selenium 4 - use built-in CDP websocket proxy.
  214. progress.log(`<selenium> using selenium v4`);
  215. const endpointURLString = addProtocol(capabilities['se:cdp']);
  216. endpointURL = new URL(endpointURLString);
  217. if (endpointURL.hostname === 'localhost' || endpointURL.hostname === '127.0.0.1') endpointURL.hostname = new URL(hubUrl).hostname;
  218. progress.log(`<selenium> retrieved endpoint ${endpointURL.toString()} for sessionId=${sessionId}`);
  219. } else {
  220. // Selenium 3 - resolve target node IP to use instead of localhost ws url.
  221. progress.log(`<selenium> using selenium v3`);
  222. const maybeChromeOptions = capabilities['goog:chromeOptions'];
  223. const chromeOptions = maybeChromeOptions && typeof maybeChromeOptions === 'object' ? maybeChromeOptions : undefined;
  224. const debuggerAddress = chromeOptions && typeof chromeOptions.debuggerAddress === 'string' ? chromeOptions.debuggerAddress : undefined;
  225. const chromeOptionsURL = typeof maybeChromeOptions === 'string' ? maybeChromeOptions : undefined;
  226. // TODO(dgozman): figure out if we can make ChromeDriver to return 127.0.0.1 instead of localhost.
  227. const endpointURLString = addProtocol(debuggerAddress || chromeOptionsURL).replace('localhost', '127.0.0.1');
  228. progress.log(`<selenium> retrieved endpoint ${endpointURLString} for sessionId=${sessionId}`);
  229. endpointURL = new URL(endpointURLString);
  230. if (endpointURL.hostname === 'localhost' || endpointURL.hostname === '127.0.0.1') {
  231. const sessionInfoUrl = new URL(hubUrl).origin + '/grid/api/testsession?session=' + sessionId;
  232. try {
  233. const sessionResponse = await (0, _network.fetchData)({
  234. url: sessionInfoUrl,
  235. method: 'GET',
  236. timeout: progress.timeUntilDeadline(),
  237. headers
  238. }, seleniumErrorHandler);
  239. const proxyId = JSON.parse(sessionResponse).proxyId;
  240. endpointURL.hostname = new URL(proxyId).hostname;
  241. progress.log(`<selenium> resolved endpoint ip ${endpointURL.toString()} for sessionId=${sessionId}`);
  242. } catch (e) {
  243. progress.log(`<selenium> unable to resolve endpoint ip for sessionId=${sessionId}, running in standalone?`);
  244. }
  245. }
  246. }
  247. return await this._connectOverCDPInternal(progress, endpointURL.toString(), {
  248. ...options,
  249. headers: (0, _utils.headersObjectToArray)(headers)
  250. }, disconnectFromSelenium);
  251. } catch (e) {
  252. await disconnectFromSelenium();
  253. throw e;
  254. }
  255. }
  256. _defaultArgs(options, isPersistent, userDataDir) {
  257. const chromeArguments = this._innerDefaultArgs(options);
  258. chromeArguments.push(`--user-data-dir=${userDataDir}`);
  259. if (options.useWebSocket) chromeArguments.push('--remote-debugging-port=0');else chromeArguments.push('--remote-debugging-pipe');
  260. if (isPersistent) chromeArguments.push('about:blank');else chromeArguments.push('--no-startup-window');
  261. return chromeArguments;
  262. }
  263. _innerDefaultArgs(options) {
  264. const {
  265. args = [],
  266. proxy
  267. } = options;
  268. const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
  269. if (userDataDirArg) throw this._createUserDataDirArgMisuseError('--user-data-dir');
  270. if (args.find(arg => arg.startsWith('--remote-debugging-pipe'))) throw new Error('Playwright manages remote debugging connection itself.');
  271. if (args.find(arg => !arg.startsWith('-'))) throw new Error('Arguments can not specify page to be opened');
  272. const chromeArguments = [..._chromiumSwitches.chromiumSwitches];
  273. if (_os.default.platform() === 'darwin') {
  274. // See https://github.com/microsoft/playwright/issues/7362
  275. chromeArguments.push('--enable-use-zoom-for-dsf=false');
  276. // See https://bugs.chromium.org/p/chromium/issues/detail?id=1407025.
  277. if (options.headless) chromeArguments.push('--use-angle');
  278. }
  279. if (options.devtools) chromeArguments.push('--auto-open-devtools-for-tabs');
  280. if (options.headless) {
  281. if (process.env.PLAYWRIGHT_CHROMIUM_USE_HEADLESS_NEW) chromeArguments.push('--headless=new');else chromeArguments.push('--headless');
  282. chromeArguments.push('--hide-scrollbars', '--mute-audio', '--blink-settings=primaryHoverType=2,availableHoverTypes=2,primaryPointerType=4,availablePointerTypes=4');
  283. }
  284. if (options.chromiumSandbox !== true) chromeArguments.push('--no-sandbox');
  285. if (proxy) {
  286. const proxyURL = new URL(proxy.server);
  287. const isSocks = proxyURL.protocol === 'socks5:';
  288. // https://www.chromium.org/developers/design-documents/network-settings
  289. if (isSocks && !this.attribution.playwright.options.socksProxyPort) {
  290. // https://www.chromium.org/developers/design-documents/network-stack/socks-proxy
  291. chromeArguments.push(`--host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE ${proxyURL.hostname}"`);
  292. }
  293. chromeArguments.push(`--proxy-server=${proxy.server}`);
  294. const proxyBypassRules = [];
  295. // https://source.chromium.org/chromium/chromium/src/+/master:net/docs/proxy.md;l=548;drc=71698e610121078e0d1a811054dcf9fd89b49578
  296. if (this.attribution.playwright.options.socksProxyPort) proxyBypassRules.push('<-loopback>');
  297. if (proxy.bypass) proxyBypassRules.push(...proxy.bypass.split(',').map(t => t.trim()).map(t => t.startsWith('.') ? '*' + t : t));
  298. if (!process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK && !proxyBypassRules.includes('<-loopback>')) proxyBypassRules.push('<-loopback>');
  299. if (proxyBypassRules.length > 0) chromeArguments.push(`--proxy-bypass-list=${proxyBypassRules.join(';')}`);
  300. }
  301. chromeArguments.push(...args);
  302. return chromeArguments;
  303. }
  304. }
  305. exports.Chromium = Chromium;
  306. async function urlToWSEndpoint(progress, endpointURL) {
  307. if (endpointURL.startsWith('ws')) return endpointURL;
  308. progress.log(`<ws preparing> retrieving websocket url from ${endpointURL}`);
  309. const httpURL = endpointURL.endsWith('/') ? `${endpointURL}json/version/` : `${endpointURL}/json/version/`;
  310. const json = await (0, _network.fetchData)({
  311. url: httpURL
  312. }, async (_, resp) => new Error(`Unexpected status ${resp.statusCode} when connecting to ${httpURL}.\n` + `This does not look like a DevTools server, try connecting via ws://.`));
  313. return JSON.parse(json).webSocketDebuggerUrl;
  314. }
  315. async function seleniumErrorHandler(params, response) {
  316. const body = await streamToString(response);
  317. let message = body;
  318. try {
  319. const json = JSON.parse(body);
  320. message = json.value.localizedMessage || json.value.message;
  321. } catch (e) {}
  322. return new Error(`Error connecting to Selenium at ${params.url}: ${message}`);
  323. }
  324. function addProtocol(url) {
  325. if (!['ws://', 'wss://', 'http://', 'https://'].some(protocol => url.startsWith(protocol))) return 'http://' + url;
  326. return url;
  327. }
  328. function streamToString(stream) {
  329. return new Promise((resolve, reject) => {
  330. const chunks = [];
  331. stream.on('data', chunk => chunks.push(Buffer.from(chunk)));
  332. stream.on('error', reject);
  333. stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
  334. });
  335. }
  336. function parseSeleniumRemoteParams(env, progress) {
  337. try {
  338. const parsed = JSON.parse(env.value);
  339. progress.log(`<selenium> using additional ${env.name} "${env.value}"`);
  340. return parsed;
  341. } catch (e) {
  342. progress.log(`<selenium> ignoring additional ${env.name} "${env.value}": ${e}`);
  343. }
  344. }