-
Notifications
You must be signed in to change notification settings - Fork 8.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Reporting] Remove Chome stdout/stderr observables, Add Browser Logger observable #44359
Changes from 4 commits
82b17d5
570f411
041d864
121e87e
1377459
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,10 +6,16 @@ | |
import fs from 'fs'; | ||
import os from 'os'; | ||
import path from 'path'; | ||
import { Browser, Page, LaunchOptions } from 'puppeteer'; | ||
import { | ||
Browser, | ||
Page, | ||
LaunchOptions, | ||
ConsoleMessage, | ||
Request as PuppeteerRequest, | ||
} from 'puppeteer'; | ||
import rimraf from 'rimraf'; | ||
import * as Rx from 'rxjs'; | ||
import { map, share, mergeMap, filter, partition, ignoreElements, tap } from 'rxjs/operators'; | ||
import { ignoreElements, mergeMap, tap } from 'rxjs/operators'; | ||
import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; | ||
|
||
import { puppeteerLaunch } from '../puppeteer'; | ||
|
@@ -25,10 +31,6 @@ interface IBrowserConfig { | |
[key: string]: any; | ||
} | ||
|
||
const compactWhitespace = (str: string) => { | ||
return str.replace(/\s+/, ' '); | ||
}; | ||
|
||
export class HeadlessChromiumDriverFactory { | ||
private binaryPath: binaryPath; | ||
private logger: Logger; | ||
|
@@ -71,9 +73,10 @@ export class HeadlessChromiumDriverFactory { | |
TZ: browserTimezone, | ||
}, | ||
} as LaunchOptions).catch((error: Error) => { | ||
logger.warning( | ||
`The Reporting plugin encountered issues launching Chromium in a self-test. You may have trouble generating reports: [${error}]` | ||
logger.error( | ||
`The Reporting plugin encountered issues launching Chromium in a self-test. You may have trouble generating reports.` | ||
); | ||
logger.error(error); | ||
logger.warning(`See Chromium's log output at "${getChromeLogLocation(this.binaryPath)}"`); | ||
return null; | ||
}); | ||
|
@@ -87,8 +90,6 @@ export class HeadlessChromiumDriverFactory { | |
browserTimezone: string; | ||
}): Rx.Observable<{ | ||
driver$: Rx.Observable<HeadlessChromiumDriver>; | ||
consoleMessage$: Rx.Observable<string>; | ||
message$: Rx.Observable<string>; | ||
exit$: Rx.Observable<never>; | ||
}> { | ||
return Rx.Observable.create(async (observer: InnerSubscriber<any, any>) => { | ||
|
@@ -157,109 +158,96 @@ export class HeadlessChromiumDriverFactory { | |
.subscribe(observer) | ||
); | ||
|
||
// Register with a few useful puppeteer event handlers: | ||
// https://pptr.dev/#?product=Puppeteer&version=v1.10.0&show=api-event-error | ||
// https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#class-page | ||
|
||
const stderr$ = Rx.fromEvent(page as NodeJS.EventEmitter, 'console').pipe( | ||
filter((line: any) => line._type === 'error'), | ||
map((line: any) => line._text), | ||
share() | ||
); | ||
|
||
const [consoleMessage$, message$] = partition( | ||
(msg: string) => !!msg.match(/\[\d+\/\d+.\d+:\w+:CONSOLE\(\d+\)\]/) | ||
)(stderr$); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing this code as it does not work with the puppeteer driver. I believe this was written at a time when we launched Chromium directly, without a helper. As of now, we no longer have a way to scan the browser's stdin / stderr as a stream. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this only works if you use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. As we have explored using |
||
|
||
const driver$ = Rx.of( | ||
new HeadlessChromiumDriver(page, { inspect: this.browserConfig.inspect }) | ||
); | ||
// taps the browser log streams and combine them to Kibana logs | ||
this.getBrowserLogger(page).subscribe(); | ||
this.getProcessLogger(browser).subscribe(); | ||
|
||
const processError$ = Rx.fromEvent(page, 'error').pipe( | ||
mergeMap(err => Rx.throwError(new Error(`Unable to spawn Chromium: [${err}]`))) | ||
); | ||
const driver$ = Rx.of(new HeadlessChromiumDriver(page, { inspect: this.browserConfig.inspect })); // prettier-ignore | ||
|
||
const processPageError$ = Rx.fromEvent(page, 'pageerror').pipe( | ||
mergeMap(err => Rx.throwError(new Error(`Uncaught exception within the page: [${err}]`))) | ||
); | ||
const exit$ = this.getPageExit(browser, page); | ||
|
||
const processRequestFailed$ = Rx.fromEvent(page, 'requestfailed').pipe( | ||
mergeMap((req: any) => { | ||
const failure = req.failure && req.failure(); | ||
if (failure) { | ||
return Rx.throwError( | ||
new Error(`Request to [${req.url()}] failed! [${failure.errorText}]`) | ||
); | ||
} | ||
return Rx.throwError(new Error(`Unknown failure! [${JSON.stringify(req)}]`)); | ||
}) | ||
); | ||
|
||
const processExit$ = Rx.fromEvent(browser, 'disconnected').pipe( | ||
mergeMap(code => | ||
Rx.throwError(new Error(`Chromium exited with: [${JSON.stringify({ code })}]`)) | ||
) | ||
); | ||
|
||
const nssError$ = message$.pipe( | ||
filter((line: string) => line.includes('error while loading shared libraries: libnss3.so')), | ||
mergeMap(() => Rx.throwError(new Error(`You must install nss for Reporting to work`))) | ||
); | ||
|
||
const fontError$ = message$.pipe( | ||
filter((line: string) => | ||
line.includes('Check failed: InitDefaultFont(). Could not find the default font') | ||
), | ||
mergeMap(() => | ||
Rx.throwError(new Error('You must install freetype and ttf-font for Reporting to work')) | ||
) | ||
); | ||
observer.next({ driver$, exit$ }); | ||
|
||
const noUsableSandbox$ = message$.pipe( | ||
filter((line: string) => line.includes('No usable sandbox! Update your kernel')), | ||
mergeMap(() => | ||
Rx.throwError( | ||
new Error( | ||
compactWhitespace(` | ||
Unable to use Chromium sandbox. This can be disabled at your own risk with | ||
'xpack.reporting.capture.browser.chromium.disableSandbox' | ||
`) | ||
) | ||
) | ||
) | ||
); | ||
|
||
const exit$ = Rx.merge( | ||
processError$, | ||
processPageError$, | ||
processRequestFailed$, | ||
processExit$, | ||
nssError$, | ||
fontError$, | ||
noUsableSandbox$ | ||
); | ||
|
||
observer.next({ | ||
driver$, | ||
consoleMessage$, | ||
message$, | ||
exit$, | ||
}); | ||
|
||
const factoryLogger = this.logger.clone(['chromium-driver-factory']); | ||
// unsubscribe logic makes a best-effort attempt to delete the user data directory used by chromium | ||
observer.add(() => { | ||
factoryLogger.debug(`deleting chromium user data directory at [${userDataDir}]`); | ||
this.logger.debug(`deleting chromium user data directory at [${userDataDir}]`); | ||
// the unsubscribe function isn't `async` so we're going to make our best effort at | ||
// deleting the userDataDir and if it fails log an error. | ||
rimraf(userDataDir, err => { | ||
if (err) { | ||
return factoryLogger.error( | ||
return this.logger.error( | ||
`error deleting user data directory at [${userDataDir}]: [${err}]` | ||
); | ||
} | ||
}); | ||
}); | ||
}); | ||
} | ||
|
||
getBrowserLogger(page: Page): Rx.Observable<ConsoleMessage> { | ||
return Rx.fromEvent(page as NodeJS.EventEmitter, 'console').pipe( | ||
tap((line: ConsoleMessage) => { | ||
if (line.type() === 'error') { | ||
this.logger.error(line.text(), ['headless-browser-console']); | ||
} else { | ||
this.logger.debug(line.text(), [line.type(), 'headless-browser-console']); | ||
} | ||
}) | ||
); | ||
} | ||
|
||
getProcessLogger(browser: Browser): Rx.Observable<string> { | ||
const childProcess = browser.process(); | ||
// NOTE: The browser driver can not observe stdout and stderr of the child process | ||
// Puppeteer doesn't give a handle to the original ChildProcess object | ||
// See https://github.com/GoogleChrome/puppeteer/issues/1292#issuecomment-521470627 | ||
|
||
// just log closing of the process | ||
const processClose$: Rx.Observable<string> = Rx.fromEvent(childProcess, 'close').pipe( | ||
tap(() => { | ||
this.logger.debug('child process closed', ['headless-browser-process']); | ||
}) | ||
); | ||
|
||
return processClose$; // ideally, this would also merge with observers for stdout and stderr | ||
} | ||
|
||
getPageExit(browser: Browser, page: Page): Rx.Observable<never> { | ||
const pageError$: Rx.Observable<never> = Rx.fromEvent(page, 'error').pipe( | ||
mergeMap((err: Error) => Rx.throwError(err)) | ||
); | ||
|
||
const uncaughtExceptionPageError$: Rx.Observable<never> = Rx.fromEvent(page, 'pageerror').pipe( | ||
mergeMap((err: Error) => Rx.throwError(err)) | ||
); | ||
|
||
const pageRequestFailed$: Rx.Observable<never> = Rx.fromEvent(page, 'requestfailed').pipe( | ||
mergeMap((req: PuppeteerRequest) => { | ||
const failure = req.failure && req.failure(); | ||
if (failure) { | ||
return Rx.throwError( | ||
new Error(`Request to [${req.url()}] failed! [${failure.errorText}]`) | ||
); | ||
} | ||
return Rx.throwError(new Error(`Unknown failure!`)); | ||
}) | ||
); | ||
|
||
const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( | ||
mergeMap(() => | ||
Rx.throwError( | ||
new Error( | ||
`Puppeteer was disconnected from the Chromium instance! Chromium has closed or crashed.` | ||
) | ||
) | ||
) | ||
); | ||
|
||
return Rx.merge( | ||
pageError$, | ||
uncaughtExceptionPageError$, | ||
pageRequestFailed$, | ||
browserDisconnect$ | ||
); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fixes an unrelated problem where an error object was getting concatenated with a string, making it unreadable