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-04-29 00:08:10 +02:00
import path from 'path' ;
2021-07-07 20:19:42 +02:00
import net from 'net' ;
2022-04-29 00:08:10 +02:00
2023-08-05 18:01:27 +02:00
import { colors , debug } from 'playwright-core/lib/utilsBundle' ;
2023-07-19 02:03:26 +02:00
import { raceAgainstDeadline , launchProcess , httpRequest , monotonicTime } from 'playwright-core/lib/utils' ;
2022-04-29 00:08:10 +02:00
2023-04-04 19:50:40 +02:00
import type { FullConfig } from '../../types/testReporter' ;
2022-05-03 23:25:56 +02:00
import type { TestRunnerPlugin } from '.' ;
2023-04-07 18:54:01 +02:00
import type { FullConfigInternal } from '../common/config' ;
2023-08-04 18:01:38 +02:00
import { envWithoutExperimentalLoaderOptions } from '../util' ;
2023-06-30 22:36:50 +02:00
import type { ReporterV2 } from '../reporters/reporterV2' ;
2022-04-29 00:08:10 +02:00
2022-05-03 20:47:37 +02:00
2022-04-29 00:08:10 +02:00
export type WebServerPluginOptions = {
command : string ;
2023-08-04 21:05:16 +02:00
url? : string ;
2022-04-29 00:08:10 +02:00
ignoreHTTPSErrors? : boolean ;
timeout? : number ;
reuseExistingServer? : boolean ;
cwd? : string ;
env ? : { [ key : string ] : string ; } ;
2023-04-26 23:39:42 +02:00
stdout ? : 'pipe' | 'ignore' ;
2023-05-17 00:46:59 +02:00
stderr ? : 'pipe' | 'ignore' ;
2022-04-29 00:08:10 +02:00
} ;
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
2023-08-05 18:01:27 +02:00
'FORCE_COLOR' : '1' ,
'DEBUG_COLORS' : '1' ,
2021-07-07 20:19:42 +02:00
} ;
2022-01-13 23:55:46 +01:00
const debugWebServer = debug ( 'pw:webserver' ) ;
2022-05-03 23:25:56 +02:00
export class WebServerPlugin implements TestRunnerPlugin {
2023-08-04 21:05:16 +02:00
private _isAvailableCallback ? : ( ) = > Promise < boolean > ;
2022-11-21 23:40:59 +01:00
private _killProcess ? : ( ) = > Promise < void > ;
private _processExitedPromise ! : Promise < any > ;
2022-05-03 20:47:37 +02:00
private _options : WebServerPluginOptions ;
2022-09-14 02:05:37 +02:00
private _checkPortOnly : boolean ;
2023-06-30 22:36:50 +02:00
private _reporter? : ReporterV2 ;
2022-05-03 20:47:37 +02:00
name = 'playwright:webserver' ;
2022-04-29 00:08:10 +02:00
2022-09-14 02:05:37 +02:00
constructor ( options : WebServerPluginOptions , checkPortOnly : boolean ) {
2022-05-03 20:47:37 +02:00
this . _options = options ;
2022-09-14 02:05:37 +02:00
this . _checkPortOnly = checkPortOnly ;
2022-04-29 00:08:10 +02:00
}
2023-06-30 22:36:50 +02:00
public async setup ( config : FullConfig , configDir : string , reporter : ReporterV2 ) {
2022-09-14 02:05:37 +02:00
this . _reporter = reporter ;
2023-08-04 21:05:16 +02:00
this . _isAvailableCallback = this . _options . url ? getIsAvailableFunction ( this . _options . url , this . _checkPortOnly , ! ! this . _options . ignoreHTTPSErrors , this . _reporter . onStdErr ? . bind ( this . _reporter ) ) : undefined ;
2022-05-03 20:47:37 +02: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-29 00:08:10 +02:00
await this . _startProcess ( ) ;
2022-11-21 23:40:59 +01:00
await this . _waitForProcess ( ) ;
2021-07-07 20:19:42 +02:00
} catch ( error ) {
2022-04-29 00:08:10 +02:00
await this . teardown ( ) ;
2021-07-07 20:19:42 +02:00
throw error ;
}
}
2022-04-29 00:08:10 +02:00
public async teardown() {
2022-11-21 23:40:59 +01:00
await this . _killProcess ? . ( ) ;
2022-04-29 00:08:10 +02:00
}
2021-07-15 01:19:45 +02:00
private async _startProcess ( ) : Promise < void > {
2022-11-21 23:40:59 +01:00
let processExitedReject = ( error : Error ) = > { } ;
this . _processExitedPromise = new Promise ( ( _ , reject ) = > processExitedReject = reject ) ;
2023-08-04 21:05:16 +02:00
const isAlreadyAvailable = await this . _isAvailableCallback ? . ( ) ;
2022-01-27 01:32:58 +01:00
if ( isAlreadyAvailable ) {
2022-03-29 21:19:56 +02:00
debugWebServer ( ` WebServer is already available ` ) ;
2022-05-03 20:47:37 +02:00
if ( this . _options . reuseExistingServer )
2021-07-15 01:19:45 +02:00
return ;
2023-08-04 21:05:16 +02:00
const port = new URL ( this . _options . url ! ) . port ;
2022-05-03 20:47:37 +02:00
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 20:47:37 +02:00
debugWebServer ( ` Starting WebServer process ${ this . _options . command } ... ` ) ;
2022-11-21 23:40:59 +01:00
const { launchedProcess , kill } = await launchProcess ( {
command : this._options.command ,
2021-07-07 20:19:42 +02:00
env : {
. . . DEFAULT_ENVIRONMENT_VARIABLES ,
2023-08-04 18:01:38 +02:00
. . . envWithoutExperimentalLoaderOptions ( ) ,
2022-05-03 20:47:37 +02:00
. . . this . _options . env ,
2021-07-07 20:19:42 +02:00
} ,
2022-05-03 20:47:37 +02:00
cwd : this._options.cwd ,
2022-11-21 23:40:59 +01:00
stdio : 'stdin' ,
2021-07-07 20:19:42 +02:00
shell : true ,
2023-07-24 17:29:29 +02:00
// Reject to indicate that we cannot close the web server gracefully
// and should fallback to non-graceful shutdown.
attemptToGracefullyClose : ( ) = > Promise . reject ( ) ,
2022-11-21 23:40:59 +01:00
log : ( ) = > { } ,
onExit : code = > processExitedReject ( new Error ( code ? ` Process from config.webServer was not able to start. Exit code: ${ code } ` : 'Process from config.webServer exited early.' ) ) ,
tempDirectories : [ ] ,
2021-07-07 20:19:42 +02:00
} ) ;
2022-11-21 23:40:59 +01:00
this . _killProcess = kill ;
2021-07-07 20:19:42 +02:00
2022-03-29 21:19:56 +02:00
debugWebServer ( ` Process started ` ) ;
2023-05-17 00:46:59 +02:00
launchedProcess . stderr ! . on ( 'data' , line = > {
if ( debugWebServer . enabled || ( this . _options . stderr === 'pipe' || ! this . _options . stderr ) )
2023-08-05 18:01:27 +02:00
this . _reporter ! . onStdErr ? . ( colors . dim ( '[WebServer] ' ) + line . toString ( ) ) ;
2023-05-17 00:46:59 +02:00
} ) ;
2022-11-21 23:40:59 +01:00
launchedProcess . stdout ! . on ( 'data' , line = > {
2023-04-26 23:39:42 +02:00
if ( debugWebServer . enabled || this . _options . stdout === 'pipe' )
2023-08-05 18:01:27 +02:00
this . _reporter ! . onStdOut ? . ( colors . dim ( '[WebServer] ' ) + line . toString ( ) ) ;
2022-02-18 16:54:01 +01:00
} ) ;
2022-11-21 23:40:59 +01:00
}
private async _waitForProcess() {
2023-08-04 21:05:16 +02:00
if ( ! this . _isAvailableCallback ) {
this . _processExitedPromise . catch ( ( ) = > { } ) ;
return ;
}
2022-03-29 21:19:56 +02:00
debugWebServer ( ` Waiting for availability... ` ) ;
2022-11-09 18:18:33 +01: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 ( [
2023-08-04 21:05:16 +02:00
raceAgainstDeadline ( ( ) = > waitFor ( this . _isAvailableCallback ! , cancellationToken ) , monotonicTime ( ) + launchTimeout ) ,
2022-11-21 23:40:59 +01:00
this . _processExitedPromise ,
2021-07-07 20:19:42 +02:00
] ) ) ;
cancellationToken . canceled = true ;
if ( timedOut )
2022-11-09 18:18:33 +01:00
throw new Error ( ` Timed out waiting ${ launchTimeout } ms from config.webServer. ` ) ;
2023-08-04 21:05:16 +02:00
debugWebServer ( ` WebServer available ` ) ;
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
}
2023-06-30 22:36:50 +02:00
async function isURLAvailable ( url : URL , ignoreHTTPSErrors : boolean , onStdErr : ReporterV2 [ 'onStdErr' ] ) {
2022-03-29 21:19:56 +02: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 ) ;
}
2022-06-10 23:47:29 +02:00
return statusCode >= 200 && statusCode < 404 ;
2022-03-29 21:19:56 +02:00
}
2023-06-30 22:36:50 +02:00
async function httpStatusCode ( url : URL , ignoreHTTPSErrors : boolean , onStdErr : ReporterV2 [ 'onStdErr' ] ) : Promise < number > {
2022-03-29 21:19:56 +02:00
return new Promise ( resolve = > {
debugWebServer ( ` HTTP GET: ${ url } ` ) ;
2023-02-22 17:09:56 +01:00
httpRequest ( {
url : url.toString ( ) ,
headers : { Accept : '*/*' } ,
rejectUnauthorized : ! ignoreHTTPSErrors
} , res = > {
2022-01-27 01:32:58 +01:00
res . resume ( ) ;
const statusCode = res . statusCode ? ? 0 ;
2022-03-29 21:19:56 +02:00
debugWebServer ( ` HTTP Status: ${ statusCode } ` ) ;
resolve ( statusCode ) ;
2023-02-22 17:09:56 +01:00
} , error = > {
2022-03-24 17:30:52 +01:00
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 21:19:56 +02:00
debugWebServer ( ` Error while checking if ${ url } is available: ${ error . message } ` ) ;
resolve ( 0 ) ;
2022-01-27 01:32:58 +01:00
} ) ;
} ) ;
}
2022-03-29 21:19:56 +02: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 21:19:56 +02: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
2023-06-30 22:36:50 +02:00
function getIsAvailableFunction ( url : string , checkPortOnly : boolean , ignoreHTTPSErrors : boolean , onStdErr : ReporterV2 [ 'onStdErr' ] ) {
2022-05-03 20:47:37 +02:00
const urlObject = new URL ( url ) ;
if ( ! checkPortOnly )
2022-03-24 17:30:52 +01:00
return ( ) = > isURLAvailable ( urlObject , ignoreHTTPSErrors , onStdErr ) ;
2022-05-03 20:47:37 +02:00
const port = urlObject . port ;
return ( ) = > isPortUsed ( + port ) ;
2022-01-27 01:32:58 +01:00
}
2022-04-29 00:08:10 +02:00
2022-05-03 23:25:56 +02:00
export const webServer = ( options : WebServerPluginOptions ) : TestRunnerPlugin = > {
2022-04-29 00:08:10 +02:00
// eslint-disable-next-line no-console
2022-09-14 02:05:37 +02:00
return new WebServerPlugin ( options , false ) ;
2022-04-29 00:08:10 +02:00
} ;
2022-09-14 02:05:37 +02:00
export const webServerPluginsForConfig = ( config : FullConfigInternal ) : TestRunnerPlugin [ ] = > {
2023-04-07 18:54:01 +02:00
const shouldSetBaseUrl = ! ! config . config . webServer ;
2022-07-08 00:27:21 +02:00
const webServerPlugins = [ ] ;
2023-04-07 18:54:01 +02:00
for ( const webServerConfig of config . webServers ) {
2023-08-04 21:05:16 +02:00
if ( webServerConfig . port && webServerConfig . url )
throw new Error ( ` Either 'port' or 'url' should be specified in config.webServer. ` ) ;
2022-05-03 20:47:37 +02:00
2023-08-04 21:05:16 +02:00
let url : string | undefined ;
if ( webServerConfig . port || webServerConfig . url ) {
url = webServerConfig . url || ` http://localhost: ${ webServerConfig . port } ` ;
2022-05-03 23:25:56 +02:00
2023-08-04 21:05:16 +02:00
// We only set base url when only the port is given. That's a legacy mode we have regrets about.
if ( shouldSetBaseUrl && ! webServerConfig . url )
process . env . PLAYWRIGHT_TEST_BASE_URL = url ;
}
2022-09-14 02:05:37 +02:00
webServerPlugins . push ( new WebServerPlugin ( { . . . webServerConfig , url } , webServerConfig . port !== undefined ) ) ;
2022-07-08 00:27:21 +02:00
}
return webServerPlugins ;
2022-04-29 00:08:10 +02:00
} ;