playwrightConnection.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.PlaywrightConnection = void 0;
  6. var _server = require("../server");
  7. var _browser = require("../server/browser");
  8. var _instrumentation = require("../server/instrumentation");
  9. var _socksProxy = require("../common/socksProxy");
  10. var _utils = require("../utils");
  11. var _android = require("../server/android/android");
  12. var _debugControllerDispatcher = require("../server/dispatchers/debugControllerDispatcher");
  13. var _debugLogger = require("../common/debugLogger");
  14. /**
  15. * Copyright (c) Microsoft Corporation.
  16. *
  17. * Licensed under the Apache License, Version 2.0 (the "License");
  18. * you may not use this file except in compliance with the License.
  19. * You may obtain a copy of the License at
  20. *
  21. * http://www.apache.org/licenses/LICENSE-2.0
  22. *
  23. * Unless required by applicable law or agreed to in writing, software
  24. * distributed under the License is distributed on an "AS IS" BASIS,
  25. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  26. * See the License for the specific language governing permissions and
  27. * limitations under the License.
  28. */
  29. class PlaywrightConnection {
  30. constructor(lock, clientType, ws, options, preLaunched, id, onClose) {
  31. this._ws = void 0;
  32. this._onClose = void 0;
  33. this._dispatcherConnection = void 0;
  34. this._cleanups = [];
  35. this._id = void 0;
  36. this._disconnected = false;
  37. this._preLaunched = void 0;
  38. this._options = void 0;
  39. this._root = void 0;
  40. this._profileName = void 0;
  41. this._ws = ws;
  42. this._preLaunched = preLaunched;
  43. this._options = options;
  44. options.launchOptions = filterLaunchOptions(options.launchOptions);
  45. if (clientType === 'reuse-browser' || clientType === 'pre-launched-browser-or-android') (0, _utils.assert)(preLaunched.playwright);
  46. if (clientType === 'pre-launched-browser-or-android') (0, _utils.assert)(preLaunched.browser || preLaunched.androidDevice);
  47. this._onClose = onClose;
  48. this._id = id;
  49. this._profileName = `${new Date().toISOString()}-${clientType}`;
  50. this._dispatcherConnection = new _server.DispatcherConnection();
  51. this._dispatcherConnection.onmessage = async message => {
  52. await lock;
  53. if (ws.readyState !== ws.CLOSING) {
  54. const messageString = JSON.stringify(message);
  55. if (_debugLogger.debugLogger.isEnabled('server:channel')) _debugLogger.debugLogger.log('server:channel', `[${this._id}] ${(0, _utils.monotonicTime)() * 1000} SEND ► ${messageString}`);
  56. ws.send(messageString);
  57. }
  58. };
  59. ws.on('message', async message => {
  60. await lock;
  61. const messageString = Buffer.from(message).toString();
  62. if (_debugLogger.debugLogger.isEnabled('server:channel')) _debugLogger.debugLogger.log('server:channel', `[${this._id}] ${(0, _utils.monotonicTime)() * 1000} ◀ RECV ${messageString}`);
  63. this._dispatcherConnection.dispatch(JSON.parse(messageString));
  64. });
  65. ws.on('close', () => this._onDisconnect());
  66. ws.on('error', error => this._onDisconnect(error));
  67. if (clientType === 'controller') {
  68. this._root = this._initDebugControllerMode();
  69. return;
  70. }
  71. this._root = new _server.RootDispatcher(this._dispatcherConnection, async scope => {
  72. await (0, _utils.startProfiling)();
  73. if (clientType === 'reuse-browser') return await this._initReuseBrowsersMode(scope);
  74. if (clientType === 'pre-launched-browser-or-android') return this._preLaunched.browser ? await this._initPreLaunchedBrowserMode(scope) : await this._initPreLaunchedAndroidMode(scope);
  75. if (clientType === 'launch-browser') return await this._initLaunchBrowserMode(scope);
  76. throw new Error('Unsupported client type: ' + clientType);
  77. });
  78. }
  79. async _initLaunchBrowserMode(scope) {
  80. _debugLogger.debugLogger.log('server', `[${this._id}] engaged launch mode for "${this._options.browserName}"`);
  81. const playwright = (0, _server.createPlaywright)({
  82. sdkLanguage: 'javascript',
  83. isServer: true
  84. });
  85. const ownedSocksProxy = await this._createOwnedSocksProxy(playwright);
  86. const browser = await playwright[this._options.browserName].launch((0, _instrumentation.serverSideCallMetadata)(), this._options.launchOptions);
  87. this._cleanups.push(async () => {
  88. for (const browser of playwright.allBrowsers()) await browser.close();
  89. });
  90. browser.on(_browser.Browser.Events.Disconnected, () => {
  91. // Underlying browser did close for some reason - force disconnect the client.
  92. this.close({
  93. code: 1001,
  94. reason: 'Browser closed'
  95. });
  96. });
  97. return new _server.PlaywrightDispatcher(scope, playwright, ownedSocksProxy, browser);
  98. }
  99. async _initPreLaunchedBrowserMode(scope) {
  100. var _this$_preLaunched$so;
  101. _debugLogger.debugLogger.log('server', `[${this._id}] engaged pre-launched (browser) mode`);
  102. const playwright = this._preLaunched.playwright;
  103. // Note: connected client owns the socks proxy and configures the pattern.
  104. (_this$_preLaunched$so = this._preLaunched.socksProxy) === null || _this$_preLaunched$so === void 0 ? void 0 : _this$_preLaunched$so.setPattern(this._options.socksProxyPattern);
  105. const browser = this._preLaunched.browser;
  106. browser.on(_browser.Browser.Events.Disconnected, () => {
  107. // Underlying browser did close for some reason - force disconnect the client.
  108. this.close({
  109. code: 1001,
  110. reason: 'Browser closed'
  111. });
  112. });
  113. const playwrightDispatcher = new _server.PlaywrightDispatcher(scope, playwright, this._preLaunched.socksProxy, browser);
  114. // In pre-launched mode, keep only the pre-launched browser.
  115. for (const b of playwright.allBrowsers()) {
  116. if (b !== browser) await b.close();
  117. }
  118. this._cleanups.push(() => playwrightDispatcher.cleanup());
  119. return playwrightDispatcher;
  120. }
  121. async _initPreLaunchedAndroidMode(scope) {
  122. _debugLogger.debugLogger.log('server', `[${this._id}] engaged pre-launched (Android) mode`);
  123. const playwright = this._preLaunched.playwright;
  124. const androidDevice = this._preLaunched.androidDevice;
  125. androidDevice.on(_android.AndroidDevice.Events.Close, () => {
  126. // Underlying browser did close for some reason - force disconnect the client.
  127. this.close({
  128. code: 1001,
  129. reason: 'Android device disconnected'
  130. });
  131. });
  132. const playwrightDispatcher = new _server.PlaywrightDispatcher(scope, playwright, undefined, undefined, androidDevice);
  133. this._cleanups.push(() => playwrightDispatcher.cleanup());
  134. return playwrightDispatcher;
  135. }
  136. _initDebugControllerMode() {
  137. _debugLogger.debugLogger.log('server', `[${this._id}] engaged reuse controller mode`);
  138. const playwright = this._preLaunched.playwright;
  139. // Always create new instance based on the reused Playwright instance.
  140. return new _debugControllerDispatcher.DebugControllerDispatcher(this._dispatcherConnection, playwright.debugController);
  141. }
  142. async _initReuseBrowsersMode(scope) {
  143. // Note: reuse browser mode does not support socks proxy, because
  144. // clients come and go, while the browser stays the same.
  145. _debugLogger.debugLogger.log('server', `[${this._id}] engaged reuse browsers mode for ${this._options.browserName}`);
  146. const playwright = this._preLaunched.playwright;
  147. const requestedOptions = launchOptionsHash(this._options.launchOptions);
  148. let browser = playwright.allBrowsers().find(b => {
  149. if (b.options.name !== this._options.browserName) return false;
  150. const existingOptions = launchOptionsHash(b.options.originalLaunchOptions);
  151. return existingOptions === requestedOptions;
  152. });
  153. // Close remaining browsers of this type+channel. Keep different browser types for the speed.
  154. for (const b of playwright.allBrowsers()) {
  155. if (b === browser) continue;
  156. if (b.options.name === this._options.browserName && b.options.channel === this._options.launchOptions.channel) await b.close();
  157. }
  158. if (!browser) {
  159. browser = await playwright[this._options.browserName || 'chromium'].launch((0, _instrumentation.serverSideCallMetadata)(), {
  160. ...this._options.launchOptions,
  161. headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS
  162. });
  163. browser.on(_browser.Browser.Events.Disconnected, () => {
  164. // Underlying browser did close for some reason - force disconnect the client.
  165. this.close({
  166. code: 1001,
  167. reason: 'Browser closed'
  168. });
  169. });
  170. }
  171. this._cleanups.push(async () => {
  172. // Don't close the pages so that user could debug them,
  173. // but close all the empty browsers and contexts to clean up.
  174. for (const browser of playwright.allBrowsers()) {
  175. for (const context of browser.contexts()) {
  176. if (!context.pages().length) await context.close((0, _instrumentation.serverSideCallMetadata)());else await context.stopPendingOperations();
  177. }
  178. if (!browser.contexts()) await browser.close();
  179. }
  180. });
  181. const playwrightDispatcher = new _server.PlaywrightDispatcher(scope, playwright, undefined, browser);
  182. return playwrightDispatcher;
  183. }
  184. async _createOwnedSocksProxy(playwright) {
  185. if (!this._options.socksProxyPattern) return;
  186. const socksProxy = new _socksProxy.SocksProxy();
  187. socksProxy.setPattern(this._options.socksProxyPattern);
  188. playwright.options.socksProxyPort = await socksProxy.listen(0);
  189. _debugLogger.debugLogger.log('server', `[${this._id}] started socks proxy on port ${playwright.options.socksProxyPort}`);
  190. this._cleanups.push(() => socksProxy.close());
  191. return socksProxy;
  192. }
  193. async _onDisconnect(error) {
  194. this._disconnected = true;
  195. _debugLogger.debugLogger.log('server', `[${this._id}] disconnected. error: ${error}`);
  196. this._root._dispose();
  197. _debugLogger.debugLogger.log('server', `[${this._id}] starting cleanup`);
  198. for (const cleanup of this._cleanups) await cleanup().catch(() => {});
  199. await (0, _utils.stopProfiling)(this._profileName);
  200. this._onClose();
  201. _debugLogger.debugLogger.log('server', `[${this._id}] finished cleanup`);
  202. }
  203. async close(reason) {
  204. if (this._disconnected) return;
  205. _debugLogger.debugLogger.log('server', `[${this._id}] force closing connection: ${(reason === null || reason === void 0 ? void 0 : reason.reason) || ''} (${(reason === null || reason === void 0 ? void 0 : reason.code) || 0})`);
  206. try {
  207. this._ws.close(reason === null || reason === void 0 ? void 0 : reason.code, reason === null || reason === void 0 ? void 0 : reason.reason);
  208. } catch (e) {}
  209. }
  210. }
  211. exports.PlaywrightConnection = PlaywrightConnection;
  212. function launchOptionsHash(options) {
  213. const copy = {
  214. ...options
  215. };
  216. for (const k of Object.keys(copy)) {
  217. const key = k;
  218. if (copy[key] === defaultLaunchOptions[key]) delete copy[key];
  219. }
  220. for (const key of optionsThatAllowBrowserReuse) delete copy[key];
  221. return JSON.stringify(copy);
  222. }
  223. function filterLaunchOptions(options) {
  224. return {
  225. channel: options.channel,
  226. args: options.args,
  227. ignoreAllDefaultArgs: options.ignoreAllDefaultArgs,
  228. ignoreDefaultArgs: options.ignoreDefaultArgs,
  229. timeout: options.timeout,
  230. headless: options.headless,
  231. proxy: options.proxy,
  232. chromiumSandbox: options.chromiumSandbox,
  233. firefoxUserPrefs: options.firefoxUserPrefs,
  234. slowMo: options.slowMo,
  235. executablePath: (0, _utils.isUnderTest)() ? options.executablePath : undefined
  236. };
  237. }
  238. const defaultLaunchOptions = {
  239. ignoreAllDefaultArgs: false,
  240. handleSIGINT: false,
  241. handleSIGTERM: false,
  242. handleSIGHUP: false,
  243. headless: true,
  244. devtools: false
  245. };
  246. const optionsThatAllowBrowserReuse = ['headless', 'tracesDir'];