2021-07-07 20:19:42 +02:00
/ * *
* 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 .
* /
2022-01-27 01:32:58 +01:00
import http from 'http' ;
import https from 'https' ;
2022-04-28 15:08:10 -07:00
import path from 'path' ;
2021-07-07 20:19:42 +02:00
import net from 'net' ;
2022-04-28 15:08:10 -07:00
2022-04-18 16:50:25 -08:00
import { debug } from 'playwright-core/lib/utilsBundle' ;
2022-04-07 19:18:22 -08:00
import { raceAgainstTimeout } from 'playwright-core/lib/utils/timeoutRunner' ;
2021-10-22 15:59:52 -04:00
import { launchProcess } from 'playwright-core/lib/utils/processLauncher' ;
2022-04-28 15:08:10 -07:00
2022-05-03 13:25:56 -08:00
import type { FullConfig , Reporter } from '../../types/testReporter' ;
import type { TestRunnerPlugin } from '.' ;
2022-04-28 15:08:10 -07:00
2022-05-03 10:47:37 -08:00
2022-04-28 15:08:10 -07:00
export type WebServerPluginOptions = {
command : string ;
url : string ;
ignoreHTTPSErrors? : boolean ;
timeout? : number ;
reuseExistingServer? : boolean ;
cwd? : string ;
env ? : { [ key : string ] : string ; } ;
} ;
2021-07-07 20:19:42 +02:00
const DEFAULT_ENVIRONMENT_VARIABLES = {
'BROWSER' : 'none' , // Disable that create-react-app will open the page in the browser
} ;
2022-01-13 23:55:46 +01:00
const debugWebServer = debug ( 'pw:webserver' ) ;
2022-05-03 13:25:56 -08:00
export class WebServerPlugin implements TestRunnerPlugin {
2022-01-27 01:32:58 +01:00
private _isAvailable : ( ) = > Promise < boolean > ;
2021-07-07 20:19:42 +02:00
private _killProcess ? : ( ) = > Promise < void > ;
private _processExitedPromise ! : Promise < any > ;
2022-05-03 10:47:37 -08:00
private _options : WebServerPluginOptions ;
2022-04-28 15:08:10 -07:00
private _reporter : Reporter ;
2022-05-03 10:47:37 -08:00
name = 'playwright:webserver' ;
2022-04-28 15:08:10 -07:00
2022-05-03 10:47:37 -08:00
constructor ( options : WebServerPluginOptions , checkPortOnly : boolean , reporter : Reporter ) {
2022-04-28 15:08:10 -07:00
this . _reporter = reporter ;
2022-05-03 10:47:37 -08:00
this . _options = options ;
this . _isAvailable = getIsAvailableFunction ( options . url , checkPortOnly , ! ! options . ignoreHTTPSErrors , this . _reporter . onStdErr ? . bind ( this . _reporter ) ) ;
2022-04-28 15:08:10 -07:00
}
2022-02-18 07:54:01 -08:00
2022-05-03 13:25:56 -08:00
public async setup ( config : FullConfig , configDir : string ) {
2022-05-03 10:47:37 -08:00
this . _options . cwd = this . _options . cwd ? path . resolve ( configDir , this . _options . cwd ) : configDir ;
2021-07-07 20:19:42 +02:00
try {
2022-04-28 15:08:10 -07:00
await this . _startProcess ( ) ;
await this . _waitForProcess ( ) ;
2021-07-07 20:19:42 +02:00
} catch ( error ) {
2022-04-28 15:08:10 -07:00
await this . teardown ( ) ;
2021-07-07 20:19:42 +02:00
throw error ;
}
}
2022-04-28 15:08:10 -07:00
public async teardown() {
await this . _killProcess ? . ( ) ;
}
2021-07-15 01:19:45 +02:00
private async _startProcess ( ) : Promise < void > {
2021-07-07 20:19:42 +02:00
let processExitedReject = ( error : Error ) = > { } ;
this . _processExitedPromise = new Promise ( ( _ , reject ) = > processExitedReject = reject ) ;
2022-01-27 01:32:58 +01:00
const isAlreadyAvailable = await this . _isAvailable ( ) ;
if ( isAlreadyAvailable ) {
2022-03-29 11:19:56 -08:00
debugWebServer ( ` WebServer is already available ` ) ;
2022-05-03 10:47:37 -08:00
if ( this . _options . reuseExistingServer )
2021-07-15 01:19:45 +02:00
return ;
2022-05-03 10:47:37 -08:00
const port = new URL ( this . _options . url ) ;
throw new Error ( ` ${ this . _options . url ? ? ` http://localhost ${ port ? ':' + port : '' } ` } is already used, make sure that nothing is running on the port/url or set reuseExistingServer:true in config.webServer. ` ) ;
2021-07-15 01:19:45 +02:00
}
2022-05-03 10:47:37 -08:00
debugWebServer ( ` Starting WebServer process ${ this . _options . command } ... ` ) ;
2021-07-07 20:19:42 +02:00
const { launchedProcess , kill } = await launchProcess ( {
2022-05-03 10:47:37 -08:00
command : this._options.command ,
2021-07-07 20:19:42 +02:00
env : {
. . . DEFAULT_ENVIRONMENT_VARIABLES ,
. . . process . env ,
2022-05-03 10:47:37 -08:00
. . . this . _options . env ,
2021-07-07 20:19:42 +02:00
} ,
2022-05-03 10:47:37 -08:00
cwd : this._options.cwd ,
2021-07-07 20:19:42 +02:00
stdio : 'stdin' ,
shell : true ,
attemptToGracefullyClose : async ( ) = > { } ,
log : ( ) = > { } ,
2021-09-27 11:32:57 +02:00
onExit : code = > processExitedReject ( new Error ( ` Process from config.webServer was not able to start. Exit code: ${ code } ` ) ) ,
2021-07-07 20:19:42 +02:00
tempDirectories : [ ] ,
} ) ;
this . _killProcess = kill ;
2022-03-29 11:19:56 -08:00
debugWebServer ( ` Process started ` ) ;
2022-04-28 15:08:10 -07:00
launchedProcess . stderr ! . on ( 'data' , line = > this . _reporter . onStdErr ? . ( '[WebServer] ' + line . toString ( ) ) ) ;
2022-02-18 07:54:01 -08:00
launchedProcess . stdout ! . on ( 'data' , line = > {
if ( debugWebServer . enabled )
2022-04-28 15:08:10 -07:00
this . _reporter . onStdOut ? . ( '[WebServer] ' + line . toString ( ) ) ;
2022-02-18 07:54:01 -08:00
} ) ;
2021-07-15 01:19:45 +02:00
}
2021-07-07 20:19:42 +02:00
2021-07-15 01:19:45 +02:00
private async _waitForProcess() {
2022-03-29 11:19:56 -08:00
debugWebServer ( ` Waiting for availability... ` ) ;
2021-08-03 23:24:14 +02:00
await this . _waitForAvailability ( ) ;
2022-03-29 11:19:56 -08:00
debugWebServer ( ` WebServer available ` ) ;
2021-07-07 20:19:42 +02:00
}
2021-08-03 23:24:14 +02:00
private async _waitForAvailability() {
2022-05-03 10:47:37 -08:00
const launchTimeout = this . _options . timeout || 60 * 1000 ;
2021-07-07 20:19:42 +02:00
const cancellationToken = { canceled : false } ;
const { timedOut } = ( await Promise . race ( [
2022-03-29 11:19:56 -08:00
raceAgainstTimeout ( ( ) = > waitFor ( this . _isAvailable , cancellationToken ) , launchTimeout ) ,
2021-07-07 20:19:42 +02:00
this . _processExitedPromise ,
] ) ) ;
cancellationToken . canceled = true ;
if ( timedOut )
2021-09-27 11:32:57 +02:00
throw new Error ( ` Timed out waiting ${ launchTimeout } ms from config.webServer. ` ) ;
2021-07-07 20:19:42 +02:00
}
}
2021-09-02 18:39:41 +02:00
async function isPortUsed ( port : number ) : Promise < boolean > {
2021-11-29 19:36:35 +01:00
const innerIsPortUsed = ( host : string ) = > new Promise < boolean > ( resolve = > {
2021-09-02 18:39:41 +02:00
const conn = net
2021-11-29 19:36:35 +01:00
. connect ( port , host )
2021-09-02 18:39:41 +02:00
. on ( 'error' , ( ) = > {
resolve ( false ) ;
} )
. on ( 'connect' , ( ) = > {
conn . end ( ) ;
resolve ( true ) ;
} ) ;
2021-07-15 01:19:45 +02:00
} ) ;
2021-11-29 19:36:35 +01:00
return await innerIsPortUsed ( '127.0.0.1' ) || await innerIsPortUsed ( '::1' ) ;
2021-07-15 01:19:45 +02:00
}
2022-05-03 10:47:37 -08:00
async function isURLAvailable ( url : URL , ignoreHTTPSErrors : boolean , onStdErr : Reporter [ 'onStdErr' ] ) {
2022-03-29 11:19:56 -08:00
let statusCode = await httpStatusCode ( url , ignoreHTTPSErrors , onStdErr ) ;
if ( statusCode === 404 && url . pathname === '/' ) {
const indexUrl = new URL ( url ) ;
indexUrl . pathname = '/index.html' ;
statusCode = await httpStatusCode ( indexUrl , ignoreHTTPSErrors , onStdErr ) ;
}
return statusCode >= 200 && statusCode < 300 ;
}
2022-05-03 10:47:37 -08:00
async function httpStatusCode ( url : URL , ignoreHTTPSErrors : boolean , onStdErr : Reporter [ 'onStdErr' ] ) : Promise < number > {
2022-03-24 17:30:52 +01:00
const isHttps = url . protocol === 'https:' ;
const requestOptions = isHttps ? {
rejectUnauthorized : ! ignoreHTTPSErrors ,
} : { } ;
2022-03-29 11:19:56 -08:00
return new Promise ( resolve = > {
debugWebServer ( ` HTTP GET: ${ url } ` ) ;
2022-03-24 17:30:52 +01:00
( isHttps ? https : http ) . get ( url , requestOptions , res = > {
2022-01-27 01:32:58 +01:00
res . resume ( ) ;
const statusCode = res . statusCode ? ? 0 ;
2022-03-29 11:19:56 -08:00
debugWebServer ( ` HTTP Status: ${ statusCode } ` ) ;
resolve ( statusCode ) ;
2022-03-24 17:30:52 +01:00
} ) . on ( 'error' , error = > {
if ( ( error as NodeJS . ErrnoException ) . code === 'DEPTH_ZERO_SELF_SIGNED_CERT' )
onStdErr ? . ( ` [WebServer] Self-signed certificate detected. Try adding ignoreHTTPSErrors: true to config.webServer. ` ) ;
2022-03-29 11:19:56 -08:00
debugWebServer ( ` Error while checking if ${ url } is available: ${ error . message } ` ) ;
resolve ( 0 ) ;
2022-01-27 01:32:58 +01:00
} ) ;
} ) ;
}
2022-03-29 11:19:56 -08:00
async function waitFor ( waitFn : ( ) = > Promise < boolean > , cancellationToken : { canceled : boolean } ) {
const logScale = [ 100 , 250 , 500 ] ;
2021-07-07 20:19:42 +02:00
while ( ! cancellationToken . canceled ) {
2022-01-27 01:32:58 +01:00
const connected = await waitFn ( ) ;
2021-07-07 20:19:42 +02:00
if ( connected )
return ;
2022-03-29 11:19:56 -08:00
const delay = logScale . shift ( ) || 1000 ;
debugWebServer ( ` Waiting ${ delay } ms ` ) ;
2021-07-07 20:19:42 +02:00
await new Promise ( x = > setTimeout ( x , delay ) ) ;
}
}
2022-01-27 01:32:58 +01:00
2022-05-03 10:47:37 -08:00
function getIsAvailableFunction ( url : string , checkPortOnly : boolean , ignoreHTTPSErrors : boolean , onStdErr : Reporter [ 'onStdErr' ] ) {
const urlObject = new URL ( url ) ;
if ( ! checkPortOnly )
2022-03-24 17:30:52 +01:00
return ( ) = > isURLAvailable ( urlObject , ignoreHTTPSErrors , onStdErr ) ;
2022-05-03 10:47:37 -08:00
const port = urlObject . port ;
return ( ) = > isPortUsed ( + port ) ;
2022-01-27 01:32:58 +01:00
}
2022-04-28 15:08:10 -07:00
2022-05-03 13:25:56 -08:00
export const webServer = ( options : WebServerPluginOptions ) : TestRunnerPlugin = > {
2022-04-28 15:08:10 -07:00
// eslint-disable-next-line no-console
2022-05-03 10:47:37 -08:00
return new WebServerPlugin ( options , false , { onStdOut : d = > console . log ( d . toString ( ) ) , onStdErr : d = > console . error ( d . toString ( ) ) } ) ;
2022-04-28 15:08:10 -07:00
} ;
2022-05-03 13:25:56 -08:00
export const webServerPluginForConfig = ( config : FullConfig , reporter : Reporter ) : TestRunnerPlugin = > {
2022-05-03 10:47:37 -08:00
const webServer = config . webServer ! ;
if ( webServer . port !== undefined && webServer . url !== undefined )
throw new Error ( ` Exactly one of 'port' or 'url' is required in config.webServer. ` ) ;
const url = webServer . url || ` http://localhost: ${ webServer . port } ` ;
2022-05-03 13:25:56 -08:00
// We only set base url when only the port is given. That's a legacy mode we have regrets about.
if ( ! webServer . url )
process . env . PLAYWRIGHT_TEST_BASE_URL = url ;
2022-05-03 10:47:37 -08:00
// TODO: replace with reporter once plugins are removed.
2022-04-28 15:08:10 -07:00
// eslint-disable-next-line no-console
2022-05-03 13:25:56 -08:00
return new WebServerPlugin ( { . . . webServer , url } , webServer . port !== undefined , reporter ) ;
2022-04-28 15:08:10 -07:00
} ;