/**
 * 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 { Semaphore } from '@isomorphic/semaphore';
import { DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT } from '@isomorphic/time';
import { WSServer } from '@utils/wsServer';
import { wrapInASCIIBox } from '@utils/ascii';
import { SocksProxy } from '@utils/socksProxy';
import { debugLogger } from '@utils/debugLogger';
import { isUnderTest } from '@utils/debug';
import { getPlaywrightVersion } from '../server/userAgent';
import { PlaywrightConnection } from './playwrightConnection';
import { WebSocketServerTransport } from './serverTransport';
import { createPlaywright } from '../server/playwright';
import { Browser } from '../server/browser';
import { nullProgress, ProgressController } from '../server/progress';
export class PlaywrightServer {
    _playwright;
    _options;
    _wsServer;
    _dontReuseBrowsers = new Set();
    constructor(options) {
        this._options = options;
        if (options.preLaunchedBrowser) {
            this._playwright = options.preLaunchedBrowser.attribution.playwright;
            this._dontReuse(options.preLaunchedBrowser);
        }
        if (options.preLaunchedAndroidDevice)
            this._playwright = options.preLaunchedAndroidDevice._android.attribution.playwright;
        this._playwright ??= createPlaywright({ sdkLanguage: 'javascript', isServer: true });
        const browserSemaphore = new Semaphore(this._options.maxConnections);
        const controllerSemaphore = new Semaphore(1);
        const reuseBrowserSemaphore = new Semaphore(1);
        this._wsServer = new WSServer({
            onRequest: (request, response) => {
                if (request.method === 'GET' && request.url === '/json') {
                    response.setHeader('Content-Type', 'application/json');
                    response.end(JSON.stringify({
                        wsEndpointPath: this._options.path,
                    }));
                    return;
                }
                response.end('Running');
            },
            onUpgrade: (request, socket) => {
                const uaError = userAgentVersionMatchesErrorMessage(request.headers['user-agent'] || '');
                if (uaError)
                    return { error: `HTTP/${request.httpVersion} 428 Precondition Required\r\n\r\n${uaError}` };
            },
            onHeaders: headers => {
                if (process.env.PWTEST_SERVER_WS_HEADERS)
                    headers.push(process.env.PWTEST_SERVER_WS_HEADERS);
            },
            onConnection: (request, url, ws, id) => {
                const browserHeader = request.headers['x-playwright-browser'];
                const browserName = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null;
                const proxyHeader = request.headers['x-playwright-proxy'];
                const proxyValue = url.searchParams.get('proxy') || (Array.isArray(proxyHeader) ? proxyHeader[0] : proxyHeader);
                const launchOptionsHeader = request.headers['x-playwright-launch-options'] || '';
                const launchOptionsHeaderValue = Array.isArray(launchOptionsHeader) ? launchOptionsHeader[0] : launchOptionsHeader;
                const launchOptionsParam = url.searchParams.get('launch-options');
                let launchOptions = { timeout: DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT };
                try {
                    launchOptions = JSON.parse(launchOptionsParam || launchOptionsHeaderValue);
                    if (!launchOptions.timeout)
                        launchOptions.timeout = DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT;
                }
                catch (e) {
                }
                const isExtension = this._options.mode === 'extension';
                launchOptions = filterLaunchOptions(launchOptions, isExtension || !!this._options.unsafe);
                // Always override artifacts dir with the one from server options.
                if (this._options.artifactsDir)
                    launchOptions.artifactsDir = this._options.artifactsDir;
                if (isExtension) {
                    const connectFilter = url.searchParams.get('connect');
                    if (connectFilter) {
                        if (connectFilter !== 'first')
                            throw new Error(`Unknown connect filter: ${connectFilter}`);
                        return new PlaywrightConnection(browserSemaphore, new WebSocketServerTransport(ws), false, this._playwright, () => this._initConnectMode(id, connectFilter, browserName, launchOptions), id);
                    }
                    if (url.searchParams.has('debug-controller')) {
                        return new PlaywrightConnection(controllerSemaphore, new WebSocketServerTransport(ws), true, this._playwright, async () => { throw new Error('shouldnt be used'); }, id);
                    }
                    return new PlaywrightConnection(reuseBrowserSemaphore, new WebSocketServerTransport(ws), false, this._playwright, () => this._initReuseBrowsersMode(browserName, launchOptions, id), id);
                }
                if (this._options.mode === 'launchServer' || this._options.mode === 'launchServerShared') {
                    if (this._options.preLaunchedBrowser) {
                        return new PlaywrightConnection(browserSemaphore, new WebSocketServerTransport(ws), false, this._playwright, () => this._initPreLaunchedBrowserMode(id), id);
                    }
                    return new PlaywrightConnection(browserSemaphore, new WebSocketServerTransport(ws), false, this._playwright, () => this._initPreLaunchedAndroidMode(id), id);
                }
                return new PlaywrightConnection(browserSemaphore, new WebSocketServerTransport(ws), false, this._playwright, () => this._initLaunchBrowserMode(browserName, proxyValue, launchOptions, id), id);
            },
        });
    }
    async _initReuseBrowsersMode(browserName, launchOptions, id) {
        // Note: reuse browser mode does not support socks proxy, because
        // clients come and go, while the browser stays the same.
        debugLogger.log('server', `[${id}] engaged reuse browsers mode for ${browserName}`);
        const requestedOptions = launchOptionsHash(launchOptions);
        let browser = this._playwright.allBrowsers().find(b => {
            if (b.options.name !== browserName)
                return false;
            if (this._dontReuseBrowsers.has(b))
                return false;
            const existingOptions = launchOptionsHash({ ...b.options.originalLaunchOptions, timeout: DEFAULT_PLAYWRIGHT_LAUNCH_TIMEOUT });
            return existingOptions === requestedOptions;
        });
        // Close remaining browsers of this type+channel. Keep different browser types for the speed.
        for (const b of this._playwright.allBrowsers()) {
            if (b === browser)
                continue;
            if (this._dontReuseBrowsers.has(b))
                continue;
            if (b.options.name === browserName && b.options.channel === launchOptions.channel)
                await b.close(nullProgress, { reason: 'Connection terminated' });
        }
        if (!browser) {
            const browserType = this._playwright[(browserName || 'chromium')];
            const controller = new ProgressController();
            browser = await controller.run(progress => browserType.launch(progress, {
                ...launchOptions,
                headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS,
            }), launchOptions.timeout);
        }
        return {
            preLaunchedBrowser: browser,
            denyLaunch: true,
            dispose: async () => {
                // Don't close the pages so that user could debug them,
                // but close all the empty contexts to clean up.
                // keep around browser so it can be reused by the next connection.
                for (const context of browser.contexts()) {
                    if (!context.pages().length)
                        await context.close(nullProgress, { reason: 'Connection terminated' });
                }
            }
        };
    }
    async _initConnectMode(id, filter, browserName, launchOptions) {
        browserName ??= 'chromium';
        debugLogger.log('server', `[${id}] engaged connect mode`);
        let browser = this._playwright.allBrowsers().find(b => b.options.name === browserName);
        if (!browser) {
            const browserType = this._playwright[browserName];
            const controller = new ProgressController();
            browser = await controller.run(progress => browserType.launch(progress, launchOptions), launchOptions.timeout);
            this._dontReuse(browser);
        }
        return {
            preLaunchedBrowser: browser,
            denyLaunch: true,
            sharedBrowser: true,
        };
    }
    async _initPreLaunchedBrowserMode(id) {
        debugLogger.log('server', `[${id}] engaged pre-launched (browser) mode`);
        const browser = this._options.preLaunchedBrowser;
        // In pre-launched mode, keep only the pre-launched browser.
        for (const b of this._playwright.allBrowsers()) {
            if (b !== browser)
                await b.close(nullProgress, { reason: 'Connection terminated' });
        }
        return {
            preLaunchedBrowser: browser,
            socksProxy: this._options.preLaunchedSocksProxy,
            sharedBrowser: this._options.mode === 'launchServerShared',
            denyLaunch: true,
        };
    }
    async _initPreLaunchedAndroidMode(id) {
        debugLogger.log('server', `[${id}] engaged pre-launched (Android) mode`);
        const androidDevice = this._options.preLaunchedAndroidDevice;
        return {
            preLaunchedAndroidDevice: androidDevice,
            denyLaunch: true,
        };
    }
    async _initLaunchBrowserMode(browserName, proxyValue, launchOptions, id) {
        debugLogger.log('server', `[${id}] engaged launch mode for "${browserName}"`);
        let socksProxy;
        if (proxyValue) {
            socksProxy = new SocksProxy();
            socksProxy.setPattern(proxyValue);
            launchOptions.socksProxyPort = await socksProxy.listen(0);
            debugLogger.log('server', `[${id}] started socks proxy on port ${launchOptions.socksProxyPort}`);
        }
        else {
            launchOptions.socksProxyPort = undefined;
        }
        const browserType = this._playwright[browserName];
        const controller = new ProgressController();
        const browser = await controller.run(progress => browserType.launch(progress, launchOptions), launchOptions.timeout);
        this._dontReuseBrowsers.add(browser);
        return {
            preLaunchedBrowser: browser,
            socksProxy,
            denyLaunch: true,
            dispose: async () => {
                await browser.close(nullProgress, { reason: 'Connection terminated' });
                socksProxy?.close();
            },
        };
    }
    _dontReuse(browser) {
        this._dontReuseBrowsers.add(browser);
        browser.on(Browser.Events.Disconnected, () => {
            this._dontReuseBrowsers.delete(browser);
        });
    }
    async listen(port = 0, hostname) {
        return this._wsServer.listen(port, hostname, this._options.path);
    }
    async close() {
        await this._wsServer.close();
        // Close all browsers that were launched by this server (e.g. in reuse mode)
        // to avoid leaking browser processes that may hold connections to test servers.
        for (const browser of this._playwright.allBrowsers())
            await browser.close(nullProgress, { reason: 'Server closed' });
    }
}
function userAgentVersionMatchesErrorMessage(userAgent) {
    const match = userAgent.match(/^Playwright\/(\d+\.\d+\.\d+)/);
    if (!match) {
        // Cannot parse user agent - be lax.
        return;
    }
    const received = match[1].split('.').slice(0, 2).join('.');
    const expected = getPlaywrightVersion(true);
    if (received !== expected) {
        return wrapInASCIIBox([
            `Playwright version mismatch:`,
            `  - server version: v${expected}`,
            `  - client version: v${received}`,
            ``,
            `If you are using VSCode extension, restart VSCode.`,
            ``,
            `If you are connecting to a remote service,`,
            `keep your local Playwright version in sync`,
            `with the remote service version.`,
            ``,
            `<3 Playwright Team`
        ].join('\n'), 1);
    }
}
function launchOptionsHash(options) {
    const copy = { ...options };
    for (const k of Object.keys(copy)) {
        const key = k;
        if (copy[key] === defaultLaunchOptions[key])
            delete copy[key];
    }
    for (const key of optionsThatAllowBrowserReuse)
        delete copy[key];
    return JSON.stringify(copy);
}
function filterLaunchOptions(options, allowUnsafe) {
    return {
        channel: options.channel,
        args: allowUnsafe ? options.args : undefined,
        ignoreAllDefaultArgs: allowUnsafe ? options.ignoreAllDefaultArgs : undefined,
        ignoreDefaultArgs: allowUnsafe ? options.ignoreDefaultArgs : undefined,
        timeout: options.timeout,
        headless: options.headless,
        proxy: options.proxy,
        chromiumSandbox: allowUnsafe ? options.chromiumSandbox : undefined,
        firefoxUserPrefs: allowUnsafe ? options.firefoxUserPrefs : undefined,
        slowMo: options.slowMo,
        executablePath: (isUnderTest() || allowUnsafe) ? options.executablePath : undefined,
        downloadsPath: allowUnsafe ? options.downloadsPath : undefined,
        artifactsDir: (isUnderTest() || allowUnsafe) ? options.artifactsDir : undefined,
    };
}
const defaultLaunchOptions = {
    ignoreAllDefaultArgs: false,
    handleSIGINT: false,
    handleSIGTERM: false,
    handleSIGHUP: false,
    headless: true,
};
const optionsThatAllowBrowserReuse = [
    'headless',
    'timeout',
    'tracesDir',
    'artifactsDir',
];
//# sourceMappingURL=playwrightServer.js.map