Skip to content

Commit 8f83864

Browse files
committed
chore: download browser downloads in-memory
1 parent 44e812f commit 8f83864

File tree

6 files changed

+28
-40
lines changed

6 files changed

+28
-40
lines changed

packages/playwright-core/bundles/zip/src/third_party/extract-zip.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ declare namespace extract {
4949
}
5050

5151
declare function extract(
52-
zipPath: string,
52+
zipPath: Buffer,
5353
opts: extract.Options,
5454
): Promise<void>;
5555

packages/playwright-core/bundles/zip/src/third_party/extract-zip.js

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,20 +33,21 @@ const { promisify } = require('util')
3333
const stream = require('stream')
3434
const yauzl = require('yauzl')
3535

36-
const openZip = promisify(yauzl.open)
36+
const openZipFromBuffer = promisify(yauzl.fromBuffer)
3737
const pipeline = promisify(stream.pipeline)
3838

3939
class Extractor {
40-
constructor (zipPath, opts) {
41-
this.zipPath = zipPath
40+
constructor (zipFile, opts) {
41+
/** @type {Buffer} */
42+
this.zipFile = zipFile
4243
this.opts = opts
4344
}
4445

4546
async extract () {
46-
debug('opening', this.zipPath, 'with opts', this.opts)
47+
debug('opening', `${this.zipFile.byteLength} bytes`, 'with opts', this.opts);
4748

48-
this.zipfile = await openZip(this.zipPath, { lazyEntries: true })
49-
this.canceled = false
49+
this.zipfile = await openZipFromBuffer(this.zipFile, { lazyEntries: true });
50+
this.canceled = false;
5051

5152
return new Promise((resolve, reject) => {
5253
this.zipfile.on('error', err => {
@@ -55,7 +56,7 @@ class Extractor {
5556
})
5657
this.zipfile.readEntry()
5758

58-
this.zipfile.on('close', () => {
59+
this.zipfile.on('end', () => {
5960
if (!this.canceled) {
6061
debug('zip extraction complete')
6162
resolve()
@@ -186,7 +187,7 @@ class Extractor {
186187
}
187188
}
188189

189-
module.exports = async function (zipPath, opts) {
190+
module.exports = async function (zipFile, opts) {
190191
debug('creating target directory', opts.dir)
191192

192193
if (!path.isAbsolute(opts.dir)) {
@@ -195,5 +196,5 @@ module.exports = async function (zipPath, opts) {
195196

196197
await fs.mkdir(opts.dir, { recursive: true })
197198
opts.dir = await fs.realpath(opts.dir)
198-
return new Extractor(zipPath, opts).extract()
199+
return new Extractor(zipFile, opts).extract()
199200
}

packages/playwright-core/src/server/registry/browserFetcher.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import * as childProcess from 'child_process';
1919
import fs from 'fs';
20-
import os from 'os';
2120
import path from 'path';
2221

2322
import { debugLogger } from '../utils/debugLogger';
@@ -37,20 +36,17 @@ export async function downloadBrowserWithProgressBar(title: string, browserDirec
3736
return false;
3837
}
3938

40-
const zipPath = path.join(os.tmpdir(), downloadFileName);
4139
try {
4240
const retryCount = 5;
4341
for (let attempt = 1; attempt <= retryCount; ++attempt) {
4442
debugLogger.log('install', `downloading ${title} - attempt #${attempt}`);
4543
const url = downloadURLs[(attempt - 1) % downloadURLs.length];
4644
logPolitely(`Downloading ${title}` + colors.dim(` from ${url}`));
47-
const { error } = await downloadBrowserWithProgressBarOutOfProcess(title, browserDirectory, url, zipPath, executablePath, downloadSocketTimeout);
45+
const { error } = await downloadBrowserWithProgressBarOutOfProcess(title, browserDirectory, url, executablePath, downloadSocketTimeout);
4846
if (!error) {
4947
debugLogger.log('install', `SUCCESS installing ${title}`);
5048
break;
5149
}
52-
if (await existsAsync(zipPath))
53-
await fs.promises.unlink(zipPath);
5450
if (await existsAsync(browserDirectory))
5551
await fs.promises.rmdir(browserDirectory, { recursive: true });
5652
const errorMessage = error?.message || '';
@@ -62,9 +58,6 @@ export async function downloadBrowserWithProgressBar(title: string, browserDirec
6258
debugLogger.log('install', `FAILED installation ${title} with error: ${e}`);
6359
process.exitCode = 1;
6460
throw e;
65-
} finally {
66-
if (await existsAsync(zipPath))
67-
await fs.promises.unlink(zipPath);
6861
}
6962
logPolitely(`${title} downloaded to ${browserDirectory}`);
7063
return true;
@@ -75,7 +68,7 @@ export async function downloadBrowserWithProgressBar(title: string, browserDirec
7568
* Thats why we execute it in a separate process and check manually if the destination file exists.
7669
* https://github.com/microsoft/playwright/issues/17394
7770
*/
78-
function downloadBrowserWithProgressBarOutOfProcess(title: string, browserDirectory: string, url: string, zipPath: string, executablePath: string | undefined, socketTimeout: number): Promise<{ error: Error | null }> {
71+
function downloadBrowserWithProgressBarOutOfProcess(title: string, browserDirectory: string, url: string, executablePath: string | undefined, socketTimeout: number): Promise<{ error: Error | null }> {
7972
const cp = childProcess.fork(path.join(__dirname, 'oopDownloadBrowserMain.js'));
8073
const promise = new ManualPromise<{ error: Error | null }>();
8174
const progress = getDownloadProgress();
@@ -101,12 +94,11 @@ function downloadBrowserWithProgressBarOutOfProcess(title: string, browserDirect
10194

10295
debugLogger.log('install', `running download:`);
10396
debugLogger.log('install', `-- from url: ${url}`);
104-
debugLogger.log('install', `-- to location: ${zipPath}`);
97+
debugLogger.log('install', `-- to location: ${browserDirectory}`);
10598
const downloadParams: DownloadParams = {
10699
title,
107100
browserDirectory,
108101
url,
109-
zipPath,
110102
executablePath,
111103
socketTimeout,
112104
userAgent: getUserAgent(),

packages/playwright-core/src/server/registry/oopDownloadBrowserMain.ts

Lines changed: 12 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ export type DownloadParams = {
2525
title: string;
2626
browserDirectory: string;
2727
url: string;
28-
zipPath: string;
2928
executablePath: string | undefined;
3029
socketTimeout: number;
3130
userAgent: string;
@@ -43,11 +42,11 @@ function browserDirectoryToMarkerFilePath(browserDirectory: string): string {
4342
return path.join(browserDirectory, 'INSTALLATION_COMPLETE');
4443
}
4544

46-
function downloadFile(options: DownloadParams): Promise<void> {
45+
function downloadFile(options: DownloadParams): Promise<Buffer> {
4746
let downloadedBytes = 0;
4847
let totalBytes = 0;
4948

50-
const promise = new ManualPromise<void>();
49+
const promise = new ManualPromise<Buffer>();
5150

5251
httpRequest({
5352
url: options.url,
@@ -73,21 +72,22 @@ function downloadFile(options: DownloadParams): Promise<void> {
7372
}
7473
totalBytes = parseInt(response.headers['content-length'] || '0', 10);
7574
log(`-- total bytes: ${totalBytes}`);
76-
const file = fs.createWriteStream(options.zipPath);
77-
file.on('finish', () => {
75+
const chunks: Buffer[] = [];
76+
response.on('end', () => {
7877
if (downloadedBytes !== totalBytes) {
7978
log(`-- download failed, size mismatch: ${downloadedBytes} != ${totalBytes}`);
8079
promise.reject(new Error(`Download failed: size mismatch, file size: ${downloadedBytes}, expected size: ${totalBytes} URL: ${options.url}`));
8180
} else {
8281
log(`-- download complete, size: ${downloadedBytes}`);
83-
promise.resolve();
82+
promise.resolve(Buffer.concat(chunks));
8483
}
8584
});
86-
file.on('error', error => promise.reject(error));
87-
response.pipe(file);
88-
response.on('data', onData);
85+
response.on('data', chunk => {
86+
chunks.push(chunk);
87+
downloadedBytes += chunk.length;
88+
progress(downloadedBytes, totalBytes);
89+
});
8990
response.on('error', (error: any) => {
90-
file.close();
9191
if (error?.code === 'ECONNRESET') {
9292
log(`-- download failed, server closed connection`);
9393
promise.reject(new Error(`Download failed: server closed connection. URL: ${options.url}`));
@@ -98,18 +98,13 @@ function downloadFile(options: DownloadParams): Promise<void> {
9898
});
9999
}, (error: any) => promise.reject(error));
100100
return promise;
101-
102-
function onData(chunk: string) {
103-
downloadedBytes += chunk.length;
104-
progress(downloadedBytes, totalBytes);
105-
}
106101
}
107102

108103
async function main(options: DownloadParams) {
109-
await downloadFile(options);
104+
const zipFile = await downloadFile(options);
110105
log(`SUCCESS downloading ${options.title}`);
111106
log(`extracting archive`);
112-
await extract(options.zipPath, { dir: options.browserDirectory });
107+
await extract(zipFile, { dir: options.browserDirectory });
113108
if (options.executablePath) {
114109
log(`fixing permissions at ${options.executablePath}`);
115110
await fs.promises.chmod(options.executablePath, 0o755);

tests/library/browsercontext-har.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ it('should round-trip extracted har.zip', async ({ contextFactory, server }, tes
283283
await context1.close();
284284

285285
const harDir = testInfo.outputPath('hardir');
286-
await extractZip(harPath, { dir: harDir });
286+
await extractZip(await fs.promises.readFile(harPath), { dir: harDir });
287287

288288
const context2 = await contextFactory();
289289
await context2.routeFromHAR(path.join(harDir, 'har.har'));

tests/playwright-test/reporter-blob.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1438,7 +1438,7 @@ test('blob report should include version', async ({ runInlineTest }) => {
14381438
});
14391439

14401440
async function extractReport(reportZipFile: string, unzippedReportDir: string): Promise<any[]> {
1441-
await extractZip(reportZipFile, { dir: unzippedReportDir });
1441+
await extractZip(await fs.promises.readFile(reportZipFile), { dir: unzippedReportDir });
14421442
const reportFile = path.join(unzippedReportDir, 'report.jsonl');
14431443
const data = await fs.promises.readFile(reportFile, 'utf8');
14441444
const events = data.split('\n').filter(Boolean).map(line => JSON.parse(line));

0 commit comments

Comments
 (0)