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' ;
2021-07-07 20:19:42 +02:00
import net from 'net' ;
2022-01-13 23:55:46 +01:00
import debug from 'debug' ;
2022-01-25 11:22:28 -08:00
import { raceAgainstTimeout } from 'playwright-core/lib/utils/async' ;
2021-10-14 05:55:08 -04:00
import { WebServerConfig } from './types' ;
2021-10-22 15:59:52 -04:00
import { launchProcess } from 'playwright-core/lib/utils/processLauncher' ;
2022-02-18 07:54:01 -08:00
import { Reporter } from '../types/testReporter' ;
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' ) ;
2021-08-03 23:24:14 +02:00
export class WebServer {
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-02-18 07:54:01 -08:00
constructor ( private readonly config : WebServerConfig , private readonly reporter : Reporter ) {
2022-03-24 17:30:52 +01:00
this . _isAvailable = getIsAvailableFunction ( config , reporter . onStdErr ? . bind ( reporter ) ) ;
2022-01-27 01:32:58 +01:00
}
2021-07-07 20:19:42 +02:00
2022-02-18 07:54:01 -08:00
public static async create ( config : WebServerConfig , reporter : Reporter ) : Promise < WebServer > {
const webServer = new WebServer ( config , reporter ) ;
2021-07-07 20:19:42 +02:00
try {
2021-08-03 23:24:14 +02:00
await webServer . _startProcess ( ) ;
await webServer . _waitForProcess ( ) ;
return webServer ;
2021-07-07 20:19:42 +02:00
} catch ( error ) {
2021-08-03 23:24:14 +02:00
await webServer . kill ( ) ;
2021-07-07 20:19:42 +02:00
throw error ;
}
}
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 ) {
2021-08-03 23:24:14 +02:00
if ( this . config . reuseExistingServer )
2021-07-15 01:19:45 +02:00
return ;
2022-03-04 20:14:49 +11:00
throw new Error ( ` ${ this . config . url ? ? ` http://localhost: ${ this . config . 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
}
2021-07-07 20:19:42 +02:00
const { launchedProcess , kill } = await launchProcess ( {
command : this.config.command ,
env : {
. . . DEFAULT_ENVIRONMENT_VARIABLES ,
. . . process . env ,
. . . this . config . env ,
} ,
cwd : this.config.cwd ,
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-02-18 07:54:01 -08:00
launchedProcess . stderr ! . on ( 'data' , line = > this . reporter . onStdErr ? . ( '[WebServer] ' + line . toString ( ) ) ) ;
launchedProcess . stdout ! . on ( 'data' , line = > {
if ( debugWebServer . enabled )
this . reporter . onStdOut ? . ( '[WebServer] ' + line . toString ( ) ) ;
} ) ;
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() {
2021-08-03 23:24:14 +02:00
await this . _waitForAvailability ( ) ;
2022-02-08 15:57:36 -08:00
if ( this . config . port !== undefined )
process . env . PLAYWRIGHT_TEST_BASE_URL = ` http://localhost: ${ this . config . port } ` ;
2021-07-07 20:19:42 +02:00
}
2021-08-03 23:24:14 +02:00
private async _waitForAvailability() {
const launchTimeout = this . config . timeout || 60 * 1000 ;
2021-07-07 20:19:42 +02:00
const cancellationToken = { canceled : false } ;
const { timedOut } = ( await Promise . race ( [
2022-01-27 01:32:58 +01:00
raceAgainstTimeout ( ( ) = > waitFor ( this . _isAvailable , 100 , 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
}
public async kill() {
await this . _killProcess ? . ( ) ;
}
}
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-03-24 17:30:52 +01:00
async function isURLAvailable ( url : URL , ignoreHTTPSErrors : boolean | undefined , onStdErr : Reporter [ 'onStdErr' ] ) {
const isHttps = url . protocol === 'https:' ;
const requestOptions = isHttps ? {
rejectUnauthorized : ! ignoreHTTPSErrors ,
} : { } ;
2022-01-27 01:32:58 +01:00
return new Promise < boolean > ( resolve = > {
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 ;
resolve ( statusCode >= 200 && statusCode < 300 ) ;
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. ` ) ;
else
debugWebServer ( ` Error while checking if ${ url } is available: ${ error . message } ` ) ;
2022-01-27 01:32:58 +01:00
resolve ( false ) ;
} ) ;
} ) ;
}
async function waitFor ( waitFn : ( ) = > Promise < boolean > , delay : number , cancellationToken : { canceled : boolean } ) {
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 ;
await new Promise ( x = > setTimeout ( x , delay ) ) ;
}
}
2022-01-27 01:32:58 +01:00
2022-03-24 17:30:52 +01:00
function getIsAvailableFunction ( { url , port , ignoreHTTPSErrors } : Pick < WebServerConfig , 'port' | 'url' | 'ignoreHTTPSErrors' > , onStdErr : Reporter [ 'onStdErr' ] ) {
2022-02-08 15:57:36 -08:00
if ( url !== undefined && port === undefined ) {
2022-01-27 01:32:58 +01:00
const urlObject = new URL ( url ) ;
2022-03-24 17:30:52 +01:00
return ( ) = > isURLAvailable ( urlObject , ignoreHTTPSErrors , onStdErr ) ;
2022-02-08 15:57:36 -08:00
} else if ( port !== undefined && url === undefined ) {
2022-01-27 01:32:58 +01:00
return ( ) = > isPortUsed ( port ) ;
} else {
throw new Error ( ` Exactly one of 'port' or 'url' is required in config.webServer. ` ) ;
}
}