Skip to content

Commit

Permalink
[Reporting] Intall browser driver factory in Reporting setup phase
Browse files Browse the repository at this point in the history
  • Loading branch information
tsullivan committed Sep 28, 2021
1 parent 200d035 commit cbbce1b
Show file tree
Hide file tree
Showing 21 changed files with 123 additions and 123 deletions.
5 changes: 1 addition & 4 deletions src/dev/build/tasks/install_chromium.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
* Side Public License, v 1.
*/

import { first } from 'rxjs/operators';

// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { installBrowser } from '../../../../x-pack/plugins/reporting/server/browsers/install';

Expand All @@ -18,13 +16,12 @@ export const InstallChromium = {
for (const platform of config.getNodePlatforms()) {
log.info(`Installing Chromium for ${platform.getName()}-${platform.getArchitecture()}`);

const { binaryPath$ } = installBrowser(
await installBrowser(
log,
build.resolvePathForPlatform(platform, 'x-pack/plugins/reporting/chromium'),
platform.getName(),
platform.getArchitecture()
);
await binaryPath$.pipe(first()).toPromise();
}
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* 2.0.
*/

import apm from 'elastic-apm-node';
import { i18n } from '@kbn/i18n';
import { getDataPath } from '@kbn/utils';
import del from 'del';
import apm from 'elastic-apm-node';
import fs from 'fs';
import path from 'path';
import puppeteer from 'puppeteer';
Expand All @@ -18,12 +18,13 @@ import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators';
import { getChromiumDisconnectedError } from '../';
import { ReportingCore } from '../../..';
import { durationToNumber } from '../../../../common/schema_utils';
import { CaptureConfig } from '../../../../server/types';
import { ReportingConfigType } from '../../../config';
import { LevelLogger } from '../../../lib';
import { installBrowser } from '../../install';
import { safeChildProcess } from '../../safe_child_process';
import { HeadlessChromiumDriver } from '../driver';
import { args } from './args';
import { Metrics, getMetrics } from './metrics';
import { getMetrics, Metrics } from './metrics';

// Puppeteer type definitions do not match the documentation.
// See https://pptr.dev/#?product=Puppeteer&version=v8.0.0&show=api-puppeteerlaunchoptions
Expand All @@ -37,25 +38,22 @@ declare module 'puppeteer' {
function launch(options: ReportingLaunchOptions): Promise<puppeteer.Browser>;
}

type BrowserConfig = CaptureConfig['browser']['chromium'];
type ViewportConfig = CaptureConfig['viewport'];
type ViewportConfig = ReportingConfigType['capture']['viewport'];

export class HeadlessChromiumDriverFactory {
private binaryPath: string;
private captureConfig: CaptureConfig;
private browserConfig: BrowserConfig;
private binaryPath?: string;
private userDataDir: string;
private getChromiumArgs: (viewport: ViewportConfig) => string[];
private core: ReportingCore;

constructor(core: ReportingCore, binaryPath: string, logger: LevelLogger) {
this.core = core;
this.binaryPath = binaryPath;
const config = core.getConfig();
this.captureConfig = config.get('capture');
this.browserConfig = this.captureConfig.browser.chromium;
constructor(
core: ReportingCore,
private captureConfig: ReportingConfigType['capture'],
private logger: LevelLogger
) {
const browserConfig = this.captureConfig.browser.chromium;

if (this.browserConfig.disableSandbox) {
if (browserConfig.disableSandbox) {
logger.warning(`Enabling the Chromium sandbox provides an additional layer of protection.`);
}

Expand All @@ -64,9 +62,21 @@ export class HeadlessChromiumDriverFactory {
args({
userDataDir: this.userDataDir,
viewport,
disableSandbox: this.browserConfig.disableSandbox,
proxy: this.browserConfig.proxy,
disableSandbox: browserConfig.disableSandbox,
proxy: browserConfig.proxy,
});
this.core = core;
}

public async install() {
return this.getBinaryPath();
}

private async getBinaryPath() {
if (this.binaryPath) {
return this.binaryPath;
}
this.binaryPath = await installBrowser(this.logger);
}

type = 'chromium';
Expand All @@ -85,6 +95,7 @@ export class HeadlessChromiumDriverFactory {

const chromiumArgs = this.getChromiumArgs(viewport);
logger.debug(`Chromium launch args set to: ${chromiumArgs}`);
const binaryPath = await this.getBinaryPath();

let browser: puppeteer.Browser;
let page: puppeteer.Page;
Expand All @@ -93,9 +104,9 @@ export class HeadlessChromiumDriverFactory {

try {
browser = await puppeteer.launch({
pipe: !this.browserConfig.inspect,
pipe: !this.captureConfig.browser.chromium.inspect,
userDataDir: this.userDataDir,
executablePath: this.binaryPath,
executablePath: binaryPath,
ignoreHTTPSErrors: true,
handleSIGHUP: false,
args: chromiumArgs,
Expand Down Expand Up @@ -180,10 +191,11 @@ export class HeadlessChromiumDriverFactory {
this.getBrowserLogger(page, logger).subscribe();
this.getProcessLogger(browser, logger).subscribe();

const captureConfig = this.captureConfig;
// HeadlessChromiumDriver: object to "drive" a browser page
const driver = new HeadlessChromiumDriver(this.core, page, {
inspect: !!this.browserConfig.inspect,
networkPolicy: this.captureConfig.networkPolicy,
inspect: !!captureConfig.browser.chromium.inspect,
networkPolicy: captureConfig.networkPolicy,
});

// Rx.Observable<never>: stream to interrupt page capture
Expand Down
8 changes: 6 additions & 2 deletions x-pack/plugins/reporting/server/browsers/chromium/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,17 @@ import { i18n } from '@kbn/i18n';
import { BrowserDownload } from '../';
import { ReportingCore } from '../../../server';
import { LevelLogger } from '../../lib';
import { ReportingConfigType } from '../../config';
import { HeadlessChromiumDriverFactory } from './driver_factory';
import { ChromiumArchivePaths } from './paths';

export const chromium: BrowserDownload = {
paths: new ChromiumArchivePaths(),
createDriverFactory: (core: ReportingCore, binaryPath: string, logger: LevelLogger) =>
new HeadlessChromiumDriverFactory(core, binaryPath, logger),
createDriverFactory: (
core: ReportingCore,
captureConfig: ReportingConfigType['capture'],
logger: LevelLogger
) => new HeadlessChromiumDriverFactory(core, captureConfig, logger),
};

export const getChromiumDisconnectedError = () =>
Expand Down
23 changes: 7 additions & 16 deletions x-pack/plugins/reporting/server/browsers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,21 @@
* 2.0.
*/

import { first } from 'rxjs/operators';
import { ReportingCore } from '../';
import { ReportingConfigType } from '../config';
import { LevelLogger } from '../lib';
import { chromium, ChromiumArchivePaths } from './chromium';
import { ChromiumArchivePaths } from './chromium';
import { HeadlessChromiumDriverFactory } from './chromium/driver_factory';
import { installBrowser } from './install';

export { chromium } from './chromium';
export { HeadlessChromiumDriver } from './chromium/driver';
export { HeadlessChromiumDriverFactory } from './chromium/driver_factory';

type CreateDriverFactory = (
core: ReportingCore,
binaryPath: string,
logger: LevelLogger
) => HeadlessChromiumDriverFactory;

export interface BrowserDownload {
createDriverFactory: CreateDriverFactory;
createDriverFactory: (
core: ReportingCore,
captureConfig: ReportingConfigType['capture'],
logger: LevelLogger
) => HeadlessChromiumDriverFactory;
paths: ChromiumArchivePaths;
}

export const initializeBrowserDriverFactory = async (core: ReportingCore, logger: LevelLogger) => {
const { binaryPath$ } = installBrowser(logger);
const binaryPath = await binaryPath$.pipe(first()).toPromise();
return chromium.createDriverFactory(core, binaryPath, logger);
};
52 changes: 27 additions & 25 deletions x-pack/plugins/reporting/server/browsers/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import del from 'del';
import os from 'os';
import path from 'path';
import * as Rx from 'rxjs';
import { GenericLevelLogger } from '../lib/level_logger';
import { ChromiumArchivePaths } from './chromium';
import { ensureBrowserDownloaded } from './download';
Expand All @@ -24,36 +23,39 @@ export function installBrowser(
chromiumPath: string = path.resolve(__dirname, '../../chromium'),
platform: string = process.platform,
architecture: string = os.arch()
): { binaryPath$: Rx.Subject<string> } {
const binaryPath$ = new Rx.Subject<string>();

): Promise<string | undefined> {
const paths = new ChromiumArchivePaths();
const pkg = paths.find(platform, architecture);

if (!pkg) {
throw new Error(`Unsupported platform: ${platform}-${architecture}`);
}

const backgroundInstall = async () => {
const binaryPath = paths.getBinaryPath(pkg);
const binaryChecksum = await md5(binaryPath).catch(() => '');

if (binaryChecksum !== pkg.binaryChecksum) {
await ensureBrowserDownloaded(logger);
await del(chromiumPath);

const archive = path.join(paths.archivesPath, pkg.archiveFilename);
logger.info(`Extracting [${archive}] to [${chromiumPath}]`);
await extract(archive, chromiumPath);
return new Promise(async (resolve) => {
try {
const binaryPath = paths.getBinaryPath(pkg);
const binaryChecksum = await md5(binaryPath).catch((error) => {
logger.warning(error);
});

if (binaryChecksum !== pkg.binaryChecksum) {
logger.warning(
`Found browser binary checksum for ${pkg.platform}/${pkg.architecture} ` +
`is ${binaryChecksum} but ${pkg.binaryChecksum} was expected. Re-installing...`
);
await del(chromiumPath);
await ensureBrowserDownloaded(logger);
const archive = path.join(paths.archivesPath, pkg.archiveFilename);
logger.info(`Extracting [${archive}] to [${chromiumPath}]`);
await extract(archive, chromiumPath);
}

logger.info(`Browser executable: ${binaryPath}`);

resolve(binaryPath);
} catch (err) {
// Avoid crashing the server if unable to download the browsers (usually in a dev environment)
logger.error(err);
}

logger.info(`Browser executable: ${binaryPath}`);
binaryPath$.next(binaryPath); // subscribers wait for download and extract to complete
};

backgroundInstall();

return {
binaryPath$,
};
});
}
8 changes: 7 additions & 1 deletion x-pack/plugins/reporting/server/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { DEFAULT_SPACE_ID } from '../../spaces/common/constants';
import { SpacesPluginSetup } from '../../spaces/server';
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
import { ReportingConfig, ReportingSetup } from './';
import { chromium } from './browsers';
import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory';
import { ReportingConfigType } from './config';
import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib';
Expand All @@ -46,7 +47,6 @@ export interface ReportingInternalSetup {
}

export interface ReportingInternalStart {
browserDriverFactory: HeadlessChromiumDriverFactory;
store: ReportingStore;
savedObjects: SavedObjectsServiceStart;
uiSettings: UiSettingsServiceStart;
Expand All @@ -66,6 +66,7 @@ export class ReportingCore {
private exportTypesRegistry = getExportTypesRegistry();
private executeTask: ExecuteReportTask;
private monitorTask: MonitorReportsTask;
private browserDriverFactory: HeadlessChromiumDriverFactory;
private config?: ReportingConfig; // final config, includes dynamic values based on OS type
private executing: Set<string>;

Expand All @@ -74,6 +75,7 @@ export class ReportingCore {
constructor(private logger: LevelLogger, context: PluginInitializerContext<ReportingConfigType>) {
this.kibanaVersion = context.env.packageInfo.version;
const syncConfig = context.config.get<ReportingConfigType>();
this.browserDriverFactory = chromium.createDriverFactory(this, syncConfig.capture, this.logger);
this.deprecatedAllowedRoles = syncConfig.roles.enabled ? syncConfig.roles.allow : false;
this.executeTask = new ExecuteReportTask(this, syncConfig, this.logger);
this.monitorTask = new MonitorReportsTask(this, syncConfig, this.logger);
Expand All @@ -85,6 +87,10 @@ export class ReportingCore {
this.executing = new Set();
}

public getBrowserDriverFactory() {
return this.browserDriverFactory;
}

public getKibanaVersion() {
return this.kibanaVersion;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ import { LayoutParams, PreserveLayout } from '../../lib/layouts';
import { getScreenshots$, ScreenshotResults } from '../../lib/screenshots';
import { ConditionalHeaders } from '../common';

export async function generatePngObservableFactory(reporting: ReportingCore) {
export function generatePngObservableFactory(reporting: ReportingCore) {
const config = reporting.getConfig();
const captureConfig = config.get('capture');
const { browserDriverFactory } = await reporting.getPluginStartDeps();
const browserDriverFactory = reporting.getBrowserDriverFactory();

return function generatePngObservable(
logger: LevelLogger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,10 @@ afterEach(() => (generatePngObservableFactory as jest.Mock).mockReset());

test(`passes browserTimezone to generatePng`, async () => {
const encryptedHeaders = await encryptHeaders({});
const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock;
const generatePngObservable = generatePngObservableFactory(mockReporting) as jest.Mock;
generatePngObservable.mockReturnValue(Rx.of({ buffer: Buffer.from('') }));

const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
const runTask = runTaskFnFactory(mockReporting, getMockLogger());
const browserTimezone = 'UTC';
await runTask(
'pngJobId',
Expand Down Expand Up @@ -117,10 +117,10 @@ test(`passes browserTimezone to generatePng`, async () => {
});

test(`returns content_type of application/png`, async () => {
const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
const runTask = runTaskFnFactory(mockReporting, getMockLogger());
const encryptedHeaders = await encryptHeaders({});

const generatePngObservable = await generatePngObservableFactory(mockReporting);
const generatePngObservable = generatePngObservableFactory(mockReporting);
(generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('foo') }));

const { content_type: contentType } = await runTask(
Expand All @@ -134,10 +134,10 @@ test(`returns content_type of application/png`, async () => {

test(`returns content of generatePng`, async () => {
const testContent = 'raw string from get_screenhots';
const generatePngObservable = await generatePngObservableFactory(mockReporting);
const generatePngObservable = generatePngObservableFactory(mockReporting);
(generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) }));

const runTask = await runTaskFnFactory(mockReporting, getMockLogger());
const runTask = runTaskFnFactory(mockReporting, getMockLogger());
const encryptedHeaders = await encryptHeaders({});
await runTask(
'pngJobId',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const runTaskFnFactory: RunTaskFnFactory<RunTaskFn<TaskPayloadPNG>> =
const apmGetAssets = apmTrans?.startSpan('get_assets', 'setup');
let apmGeneratePng: { end: () => void } | null | undefined;

const generatePngObservable = await generatePngObservableFactory(reporting);
const generatePngObservable = generatePngObservableFactory(reporting);
const jobLogger = parentLogger.clone([PNG_JOB_TYPE, 'execute', jobId]);
const process$: Rx.Observable<TaskRunResult> = Rx.of(1).pipe(
mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)),
Expand Down
Loading

0 comments on commit cbbce1b

Please sign in to comment.