2017-05-15 03:05:41 +00:00
/ * *
* Copyright 2017 Google Inc . All rights reserved .
*
* 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 .
* /
2020-04-24 07:57:53 +00:00
import * as os from 'os' ;
import * as fs from 'fs' ;
import * as path from 'path' ;
import * as util from 'util' ;
import * as childProcess from 'child_process' ;
import * as https from 'https' ;
import * as http from 'http' ;
import * as extract from 'extract-zip' ;
import * as debug from 'debug' ;
import * as removeRecursive from 'rimraf' ;
import * as URL from 'url' ;
import * as ProxyAgent from 'https-proxy-agent' ;
import { getProxyForUrl } from 'proxy-from-env' ;
import { helper , assert } from './helper' ;
const debugFetcher = debug ( ` puppeteer:fetcher ` ) ;
2017-05-15 03:05:41 +00:00
2017-08-02 19:06:47 +00:00
const downloadURLs = {
2020-03-10 20:59:03 +00:00
chrome : {
linux : '%s/chromium-browser-snapshots/Linux_x64/%d/%s.zip' ,
mac : '%s/chromium-browser-snapshots/Mac/%d/%s.zip' ,
win32 : '%s/chromium-browser-snapshots/Win/%d/%s.zip' ,
win64 : '%s/chromium-browser-snapshots/Win_x64/%d/%s.zip' ,
} ,
firefox : {
linux : '%s/firefox-%s.0a1.en-US.%s-x86_64.tar.bz2' ,
mac : '%s/firefox-%s.0a1.en-US.%s.dmg' ,
win32 : '%s/firefox-%s.0a1.en-US.%s.zip' ,
win64 : '%s/firefox-%s.0a1.en-US.%s.zip' ,
} ,
2020-04-24 07:57:53 +00:00
} as const ;
2020-03-10 20:59:03 +00:00
const browserConfig = {
chrome : {
host : 'https://storage.googleapis.com' ,
destination : '.local-chromium' ,
} ,
firefox : {
host : 'https://archive.mozilla.org/pub/firefox/nightly/latest-mozilla-central' ,
destination : '.local-firefox' ,
}
2020-04-24 07:57:53 +00:00
} as const ;
2017-05-15 03:05:41 +00:00
2020-04-24 07:57:53 +00:00
type Platform = 'linux' | 'mac' | 'win32' | 'win64' ;
type Product = 'chrome' | 'firefox' ;
function archiveName ( product : Product , platform : Platform , revision : string ) : string {
2020-03-10 20:59:03 +00:00
if ( product === 'chrome' ) {
if ( platform === 'linux' )
return 'chrome-linux' ;
if ( platform === 'mac' )
return 'chrome-mac' ;
if ( platform === 'win32' || platform === 'win64' ) {
// Windows archive name changed at r591479.
return parseInt ( revision , 10 ) > 591479 ? 'chrome-win' : 'chrome-win32' ;
}
} else if ( product === 'firefox' ) {
return platform ;
2018-09-17 17:15:08 +00:00
}
}
/ * *
2020-03-10 20:59:03 +00:00
* @param { string } product
2018-09-17 17:15:08 +00:00
* @param { string } platform
* @param { string } host
* @param { string } revision
* @return { string }
* /
2020-04-24 07:57:53 +00:00
function downloadURL ( product : Product , platform : Platform , host : string , revision : string ) : string {
2020-03-10 20:59:03 +00:00
const url = util . format ( downloadURLs [ product ] [ platform ] , host , revision , archiveName ( product , platform , revision ) ) ;
return url ;
2018-09-17 17:15:08 +00:00
}
2018-02-07 17:31:53 +00:00
const readdirAsync = helper . promisify ( fs . readdir . bind ( fs ) ) ;
const mkdirAsync = helper . promisify ( fs . mkdir . bind ( fs ) ) ;
const unlinkAsync = helper . promisify ( fs . unlink . bind ( fs ) ) ;
2018-04-10 21:11:59 +00:00
const chmodAsync = helper . promisify ( fs . chmod . bind ( fs ) ) ;
2017-12-08 21:39:13 +00:00
2020-04-24 07:57:53 +00:00
function existsAsync ( filePath : string ) : Promise < boolean > {
return new Promise ( resolve = > {
fs . access ( filePath , err = > resolve ( ! err ) ) ;
} ) ;
2018-02-07 17:31:53 +00:00
}
2017-12-08 21:39:13 +00:00
2020-04-24 07:57:53 +00:00
/ * *
* @typedef { Object } BrowserFetcher . Options
* /
export interface BrowserFetcherOptions {
platform? : Platform ;
product? : string ;
path? : string ;
host? : string ;
}
interface BrowserFetcherRevisionInfo {
folderPath : string ;
executablePath : string ;
url : string ;
local : boolean ;
revision : string ;
product : string ;
}
/ * *
* /
export class BrowserFetcher {
private _product : Product ;
private _downloadsFolder : string ;
private _downloadHost : string ;
private _platform : Platform ;
constructor ( projectRoot : string , options : BrowserFetcherOptions = { } ) {
this . _product = ( options . product || 'chrome' ) . toLowerCase ( ) as Product ;
2020-03-10 20:59:03 +00:00
assert ( this . _product === 'chrome' || this . _product === 'firefox' , ` Unknown product: " ${ options . product } " ` ) ;
2020-04-24 07:57:53 +00:00
2020-03-10 20:59:03 +00:00
this . _downloadsFolder = options . path || path . join ( projectRoot , browserConfig [ this . _product ] . destination ) ;
this . _downloadHost = options . host || browserConfig [ this . _product ] . host ;
2020-04-24 07:57:53 +00:00
this . setPlatform ( options . platform ) ;
2020-03-10 20:59:03 +00:00
assert ( downloadURLs [ this . _product ] [ this . _platform ] , 'Unsupported platform: ' + this . _platform ) ;
2017-12-08 21:39:13 +00:00
}
2017-05-15 03:05:41 +00:00
2020-04-24 07:57:53 +00:00
private setPlatform ( platformFromOptions? : Platform ) : void {
if ( platformFromOptions ) {
this . _platform = platformFromOptions ;
return ;
}
const platform = os . platform ( ) ;
if ( platform === 'darwin' )
this . _platform = 'mac' ;
else if ( platform === 'linux' )
this . _platform = 'linux' ;
else if ( platform === 'win32' )
this . _platform = os . arch ( ) === 'x64' ? 'win64' : 'win32' ;
else
assert ( this . _platform , 'Unsupported platform: ' + os . platform ( ) ) ;
}
platform ( ) : string {
2018-02-07 17:31:53 +00:00
return this . _platform ;
2017-12-08 21:39:13 +00:00
}
2017-05-15 03:05:41 +00:00
2020-04-24 07:57:53 +00:00
product ( ) : string {
2020-03-10 20:59:03 +00:00
return this . _product ;
}
2020-04-24 07:57:53 +00:00
host ( ) : string {
2020-03-10 20:59:03 +00:00
return this . _downloadHost ;
}
2020-04-24 07:57:53 +00:00
canDownload ( revision : string ) : Promise < boolean > {
2020-03-10 20:59:03 +00:00
const url = downloadURL ( this . _product , this . _platform , this . _downloadHost , revision ) ;
2020-04-24 07:57:53 +00:00
return new Promise ( resolve = > {
const request = httpRequest ( url , 'HEAD' , response = > {
resolve ( response . statusCode === 200 ) ;
} ) ;
request . on ( 'error' , error = > {
console . error ( error ) ;
resolve ( false ) ;
} ) ;
2017-06-21 20:51:06 +00:00
} ) ;
2017-12-08 21:39:13 +00:00
}
2017-05-15 03:05:41 +00:00
2017-06-21 20:51:06 +00:00
/ * *
2017-07-12 21:46:41 +00:00
* @param { string } revision
2019-01-11 06:56:39 +00:00
* @param { ? function ( number , number ) : void } progressCallback
2018-02-07 17:31:53 +00:00
* @return { ! Promise < ! BrowserFetcher . RevisionInfo > }
2017-07-12 21:46:41 +00:00
* /
2020-04-24 07:57:53 +00:00
async download ( revision : string , progressCallback : ( x : number , y : number ) = > void ) : Promise < BrowserFetcherRevisionInfo > {
2020-03-10 20:59:03 +00:00
const url = downloadURL ( this . _product , this . _platform , this . _downloadHost , revision ) ;
const fileName = url . split ( '/' ) . pop ( ) ;
const archivePath = path . join ( this . _downloadsFolder , fileName ) ;
const outputPath = this . _getFolderPath ( revision ) ;
if ( await existsAsync ( outputPath ) )
2018-02-07 17:31:53 +00:00
return this . revisionInfo ( revision ) ;
if ( ! ( await existsAsync ( this . _downloadsFolder ) ) )
await mkdirAsync ( this . _downloadsFolder ) ;
try {
2020-03-10 20:59:03 +00:00
await downloadFile ( url , archivePath , progressCallback ) ;
await install ( archivePath , outputPath ) ;
2018-02-07 17:31:53 +00:00
} finally {
2020-03-10 20:59:03 +00:00
if ( await existsAsync ( archivePath ) )
await unlinkAsync ( archivePath ) ;
2018-02-07 17:31:53 +00:00
}
2018-04-10 21:11:59 +00:00
const revisionInfo = this . revisionInfo ( revision ) ;
if ( revisionInfo )
await chmodAsync ( revisionInfo . executablePath , 0 o755 ) ;
return revisionInfo ;
2017-12-08 21:39:13 +00:00
}
2017-05-15 03:05:41 +00:00
2020-04-24 07:57:53 +00:00
async localRevisions ( ) : Promise < string [ ] > {
2018-02-07 17:31:53 +00:00
if ( ! await existsAsync ( this . _downloadsFolder ) )
2017-07-12 21:54:48 +00:00
return [ ] ;
2018-02-07 17:31:53 +00:00
const fileNames = await readdirAsync ( this . _downloadsFolder ) ;
2020-03-10 20:59:03 +00:00
return fileNames . map ( fileName = > parseFolderPath ( this . _product , fileName ) ) . filter ( entry = > entry && entry . platform === this . _platform ) . map ( entry = > entry . revision ) ;
2017-12-08 21:39:13 +00:00
}
2017-07-12 21:46:41 +00:00
2020-04-24 07:57:53 +00:00
async remove ( revision : string ) : Promise < void > {
2018-02-07 17:31:53 +00:00
const folderPath = this . _getFolderPath ( revision ) ;
2018-05-31 23:53:51 +00:00
assert ( await existsAsync ( folderPath ) , ` Failed to remove: revision ${ revision } is not downloaded ` ) ;
2018-02-07 17:31:53 +00:00
await new Promise ( fulfill = > removeRecursive ( folderPath , fulfill ) ) ;
2017-12-08 21:39:13 +00:00
}
2017-07-12 21:46:41 +00:00
2020-04-24 07:57:53 +00:00
revisionInfo ( revision : string ) : BrowserFetcherRevisionInfo {
2018-02-07 17:31:53 +00:00
const folderPath = this . _getFolderPath ( revision ) ;
2017-06-22 20:38:10 +00:00
let executablePath = '' ;
2020-03-10 20:59:03 +00:00
if ( this . _product === 'chrome' ) {
if ( this . _platform === 'mac' )
executablePath = path . join ( folderPath , archiveName ( this . _product , this . _platform , revision ) , 'Chromium.app' , 'Contents' , 'MacOS' , 'Chromium' ) ;
else if ( this . _platform === 'linux' )
executablePath = path . join ( folderPath , archiveName ( this . _product , this . _platform , revision ) , 'chrome' ) ;
else if ( this . _platform === 'win32' || this . _platform === 'win64' )
executablePath = path . join ( folderPath , archiveName ( this . _product , this . _platform , revision ) , 'chrome.exe' ) ;
else
throw new Error ( 'Unsupported platform: ' + this . _platform ) ;
} else if ( this . _product === 'firefox' ) {
if ( this . _platform === 'mac' )
executablePath = path . join ( folderPath , 'Firefox Nightly.app' , 'Contents' , 'MacOS' , 'firefox' ) ;
else if ( this . _platform === 'linux' )
executablePath = path . join ( folderPath , 'firefox' , 'firefox' ) ;
else if ( this . _platform === 'win32' || this . _platform === 'win64' )
executablePath = path . join ( folderPath , 'firefox' , 'firefox.exe' ) ;
else
throw new Error ( 'Unsupported platform: ' + this . _platform ) ;
} else {
throw new Error ( 'Unsupported product: ' + this . _product ) ;
}
const url = downloadURL ( this . _product , this . _platform , this . _downloadHost , revision ) ;
2018-02-07 17:31:53 +00:00
const local = fs . existsSync ( folderPath ) ;
2020-03-10 20:59:03 +00:00
debugFetcher ( { revision , executablePath , folderPath , local , url , product : this._product } ) ;
return { revision , executablePath , folderPath , local , url , product : this._product } ;
2017-12-08 21:39:13 +00:00
}
2017-05-15 03:05:41 +00:00
2017-12-08 21:39:13 +00:00
/ * *
* @param { string } revision
* @return { string }
* /
2020-04-24 07:57:53 +00:00
_getFolderPath ( revision : string ) : string {
2018-02-07 17:31:53 +00:00
return path . join ( this . _downloadsFolder , this . _platform + '-' + revision ) ;
2017-12-08 21:39:13 +00:00
}
2017-07-12 21:46:41 +00:00
}
2020-04-24 07:57:53 +00:00
function parseFolderPath ( product : Product , folderPath : string ) : { product : string ; platform : string ; revision : string } | null {
2017-08-21 23:39:04 +00:00
const name = path . basename ( folderPath ) ;
const splits = name . split ( '-' ) ;
2017-07-12 21:46:41 +00:00
if ( splits . length !== 2 )
return null ;
2017-08-21 23:39:04 +00:00
const [ platform , revision ] = splits ;
2020-03-10 20:59:03 +00:00
if ( ! downloadURLs [ product ] [ platform ] )
2017-07-12 21:46:41 +00:00
return null ;
2020-03-10 20:59:03 +00:00
return { product , platform , revision } ;
2017-05-15 03:05:41 +00:00
}
/ * *
* @param { string } url
* @param { string } destinationPath
2019-01-11 06:56:39 +00:00
* @param { ? function ( number , number ) : void } progressCallback
2017-05-15 03:05:41 +00:00
* @return { ! Promise }
* /
2020-04-24 07:57:53 +00:00
function downloadFile ( url : string , destinationPath : string , progressCallback : ( x : number , y : number ) = > void ) : Promise < void > {
2020-03-10 20:59:03 +00:00
debugFetcher ( ` Downloading binary from ${ url } ` ) ;
2017-06-22 20:38:10 +00:00
let fulfill , reject ;
2018-02-07 17:31:53 +00:00
let downloadedBytes = 0 ;
let totalBytes = 0 ;
2017-08-23 15:33:29 +00:00
2020-04-24 07:57:53 +00:00
const promise = new Promise < void > ( ( x , y ) = > { fulfill = x ; reject = y ; } ) ;
2017-08-23 15:33:29 +00:00
2017-12-04 21:45:21 +00:00
const request = httpRequest ( url , 'GET' , response = > {
2017-06-21 20:51:06 +00:00
if ( response . statusCode !== 200 ) {
2017-08-21 23:39:04 +00:00
const error = new Error ( ` Download failed: server returned code ${ response . statusCode } . URL: ${ url } ` ) ;
2017-06-21 20:51:06 +00:00
// consume response data to free up memory
response . resume ( ) ;
reject ( error ) ;
return ;
2017-06-21 20:36:04 +00:00
}
2017-08-21 23:39:04 +00:00
const file = fs . createWriteStream ( destinationPath ) ;
2017-06-21 20:51:06 +00:00
file . on ( 'finish' , ( ) = > fulfill ( ) ) ;
file . on ( 'error' , error = > reject ( error ) ) ;
response . pipe ( file ) ;
2018-02-07 17:31:53 +00:00
totalBytes = parseInt ( /** @type {string} */ ( response . headers [ 'content-length' ] ) , 10 ) ;
2017-06-21 20:51:06 +00:00
if ( progressCallback )
2018-02-07 17:31:53 +00:00
response . on ( 'data' , onData ) ;
2017-06-21 20:51:06 +00:00
} ) ;
request . on ( 'error' , error = > reject ( error ) ) ;
return promise ;
2020-04-24 07:57:53 +00:00
function onData ( chunk : string ) : void {
2018-02-07 17:31:53 +00:00
downloadedBytes += chunk . length ;
progressCallback ( downloadedBytes , totalBytes ) ;
2017-06-21 20:51:06 +00:00
}
2017-05-15 03:05:41 +00:00
}
2020-04-24 07:57:53 +00:00
function install ( archivePath : string , folderPath : string ) : Promise < unknown > {
2020-03-10 20:59:03 +00:00
debugFetcher ( ` Installing ${ archivePath } to ${ folderPath } ` ) ;
if ( archivePath . endsWith ( '.zip' ) )
return extractZip ( archivePath , folderPath ) ;
else if ( archivePath . endsWith ( '.tar.bz2' ) )
return extractTar ( archivePath , folderPath ) ;
else if ( archivePath . endsWith ( '.dmg' ) )
return mkdirAsync ( folderPath ) . then ( ( ) = > installDMG ( archivePath , folderPath ) ) ;
else
throw new Error ( ` Unsupported archive format: ${ archivePath } ` ) ;
}
2020-04-24 07:57:53 +00:00
async function extractZip ( zipPath : string , folderPath : string ) : Promise < void > {
2020-04-27 10:38:17 +00:00
const nodeVersion = process . version ;
/ * T h e r e i s c u r r e n t l y a b u g w i t h e x t r a c t - z i p a n d N o d e v 1 4 . 0 . 0 t h a t
* causes extractZip to silently fail :
* https : //github.com/puppeteer/puppeteer/issues/5719
*
* Rather than silenty fail if the user is on Node 14 we instead
* detect that and throw an error directing the user to that bug . The
* rejection message below is surfaced to the user in the command
* line .
*
* The issue seems to be in streams never resolving so we wrap the
* call in a timeout and give it 10 s to resolve before deciding on
* an error .
*
* If the user is on Node < 14 we maintain the behaviour we had before
* this patch .
* /
if ( nodeVersion . startsWith ( 'v14.' ) ) {
let timeoutReject ;
const timeoutPromise = new Promise ( ( resolve , reject ) = > { timeoutReject = reject ; } ) ;
const timeoutToken = setTimeout ( ( ) = > {
const error = new Error ( ` Puppeteer currently does not work on Node v14 due to an upstream bug. Please see: https://github.com/puppeteer/puppeteer/issues/5719 for details. ` ) ;
timeoutReject ( error ) ;
} , 10 * 1000 ) ;
await Promise . race ( [
extract ( zipPath , { dir : folderPath } ) ,
timeoutPromise
] ) ;
clearTimeout ( timeoutToken ) ;
} else {
try {
await extract ( zipPath , { dir : folderPath } ) ;
} catch ( error ) {
return error ;
}
2020-04-09 19:13:25 +00:00
}
2017-05-15 03:05:41 +00:00
}
2017-08-23 15:33:29 +00:00
2020-03-10 20:59:03 +00:00
/ * *
* @param { string } tarPath
* @param { string } folderPath
* @return { ! Promise < ? Error > }
* /
2020-04-24 07:57:53 +00:00
function extractTar ( tarPath : string , folderPath : string ) : Promise < unknown > {
// eslint-disable-next-line @typescript-eslint/no-var-requires
2020-03-10 20:59:03 +00:00
const tar = require ( 'tar-fs' ) ;
2020-04-24 07:57:53 +00:00
// eslint-disable-next-line @typescript-eslint/no-var-requires
2020-03-10 20:59:03 +00:00
const bzip = require ( 'unbzip2-stream' ) ;
return new Promise ( ( fulfill , reject ) = > {
const tarStream = tar . extract ( folderPath ) ;
tarStream . on ( 'error' , reject ) ;
tarStream . on ( 'finish' , fulfill ) ;
const readStream = fs . createReadStream ( tarPath ) ;
readStream . on ( 'data' , ( ) = > { process . stdout . write ( '\rExtracting...' ) ; } ) ;
readStream . pipe ( bzip ( ) ) . pipe ( tarStream ) ;
} ) ;
}
/ * *
* Install * . app directory from dmg file
*
* @param { string } dmgPath
* @param { string } folderPath
* @return { ! Promise < ? Error > }
* /
2020-04-24 07:57:53 +00:00
function installDMG ( dmgPath : string , folderPath : string ) : Promise < void > {
2020-03-10 20:59:03 +00:00
let mountPath ;
2020-04-24 07:57:53 +00:00
function mountAndCopy ( fulfill : ( ) = > void , reject : ( Error ) = > void ) : void {
2020-03-10 20:59:03 +00:00
const mountCommand = ` hdiutil attach -nobrowse -noautoopen " ${ dmgPath } " ` ;
2020-04-24 07:57:53 +00:00
childProcess . exec ( mountCommand , ( err , stdout ) = > {
2020-03-10 20:59:03 +00:00
if ( err )
return reject ( err ) ;
const volumes = stdout . match ( /\/Volumes\/(.*)/m ) ;
if ( ! volumes )
return reject ( new Error ( ` Could not find volume path in ${ stdout } ` ) ) ;
mountPath = volumes [ 0 ] ;
readdirAsync ( mountPath ) . then ( fileNames = > {
const appName = fileNames . filter ( item = > typeof item === 'string' && item . endsWith ( '.app' ) ) [ 0 ] ;
if ( ! appName )
return reject ( new Error ( ` Cannot find app in ${ mountPath } ` ) ) ;
const copyPath = path . join ( mountPath , appName ) ;
debugFetcher ( ` Copying ${ copyPath } to ${ folderPath } ` ) ;
2020-04-24 07:57:53 +00:00
childProcess . exec ( ` cp -R " ${ copyPath } " " ${ folderPath } " ` , err = > {
2020-03-10 20:59:03 +00:00
if ( err )
reject ( err ) ;
else
fulfill ( ) ;
} ) ;
} ) . catch ( reject ) ;
} ) ;
}
2020-04-24 07:57:53 +00:00
function unmount ( ) : void {
2020-03-10 20:59:03 +00:00
if ( ! mountPath )
return ;
const unmountCommand = ` hdiutil detach " ${ mountPath } " -quiet ` ;
debugFetcher ( ` Unmounting ${ mountPath } ` ) ;
childProcess . exec ( unmountCommand , err = > {
if ( err )
console . error ( ` Error unmounting dmg: ${ err } ` ) ;
} ) ;
}
2020-04-24 07:57:53 +00:00
return new Promise < void > ( mountAndCopy ) . catch ( err = > { console . error ( err ) ; } ) . finally ( unmount ) ;
2020-03-10 20:59:03 +00:00
}
2020-04-24 07:57:53 +00:00
function httpRequest ( url : string , method : string , response : ( x : http.IncomingMessage ) = > void ) : http . ClientRequest {
const urlParsed = URL . parse ( url ) ;
type Options = Partial < URL.UrlWithStringQuery > & {
method? : string ;
agent? : ProxyAgent ;
rejectUnauthorized? : boolean ;
} ;
let options : Options = {
. . . urlParsed ,
method ,
} ;
const proxyURL = getProxyForUrl ( url ) ;
if ( proxyURL ) {
if ( url . startsWith ( 'http:' ) ) {
const proxy = URL . parse ( proxyURL ) ;
options = {
path : options.href ,
host : proxy.hostname ,
port : proxy.port ,
} ;
} else {
const parsedProxyURL = URL . parse ( proxyURL ) ;
const proxyOptions = {
. . . parsedProxyURL ,
secureProxy : parsedProxyURL.protocol === 'https:' ,
} as ProxyAgent . HttpsProxyAgentOptions ;
options . agent = new ProxyAgent ( proxyOptions ) ;
options . rejectUnauthorized = false ;
}
}
const requestCallback = ( res : http.IncomingMessage ) : void = > {
if ( res . statusCode >= 300 && res . statusCode < 400 && res . headers . location )
httpRequest ( res . headers . location , method , response ) ;
else
response ( res ) ;
} ;
const request = options . protocol === 'https:' ?
https . request ( options , requestCallback ) :
http . request ( options , requestCallback ) ;
request . end ( ) ;
return request ;
2017-09-01 16:47:57 +00:00
}
2018-02-07 17:31:53 +00:00