diff --git a/src/rpc/channels.ts b/src/rpc/channels.ts index 46f522777a..7be70fa96d 100644 --- a/src/rpc/channels.ts +++ b/src/rpc/channels.ts @@ -23,9 +23,10 @@ export type Binary = string; export interface Channel extends EventEmitter { } -export type SerializedValue = undefined | boolean | number | string | ComplexSerializedValue; - -export type ComplexSerializedValue = { +export type SerializedValue = { + n?: number, + b?: boolean, + s?: string, v?: 'null' | 'undefined' | 'NaN' | 'Infinity' | '-Infinity' | '-0', d?: string, r?: { @@ -863,7 +864,7 @@ export type FrameEvalOnSelectorParams = { arg: SerializedArgument, }; export type FrameEvalOnSelectorResult = { - value?: SerializedValue, + value: SerializedValue, }; export type FrameEvalOnSelectorAllParams = { selector: string, @@ -872,7 +873,7 @@ export type FrameEvalOnSelectorAllParams = { arg: SerializedArgument, }; export type FrameEvalOnSelectorAllResult = { - value?: SerializedValue, + value: SerializedValue, }; export type FrameAddScriptTagParams = { url?: string, @@ -941,7 +942,7 @@ export type FrameEvaluateExpressionParams = { arg: SerializedArgument, }; export type FrameEvaluateExpressionResult = { - value?: SerializedValue, + value: SerializedValue, }; export type FrameEvaluateExpressionHandleParams = { expression: string, @@ -1119,7 +1120,7 @@ export type WorkerEvaluateExpressionParams = { arg: SerializedArgument, }; export type WorkerEvaluateExpressionResult = { - value?: SerializedValue, + value: SerializedValue, }; export type WorkerEvaluateExpressionHandleParams = { expression: string, @@ -1154,7 +1155,7 @@ export type JSHandleEvaluateExpressionParams = { arg: SerializedArgument, }; export type JSHandleEvaluateExpressionResult = { - value?: SerializedValue, + value: SerializedValue, }; export type JSHandleEvaluateExpressionHandleParams = { expression: string, @@ -1179,7 +1180,7 @@ export type JSHandleGetPropertyResult = { }; export type JSHandleJsonValueParams = {}; export type JSHandleJsonValueResult = { - value?: SerializedValue, + value: SerializedValue, }; // ----------- ElementHandle ----------- @@ -1219,7 +1220,7 @@ export type ElementHandleEvalOnSelectorParams = { arg: SerializedArgument, }; export type ElementHandleEvalOnSelectorResult = { - value?: SerializedValue, + value: SerializedValue, }; export type ElementHandleEvalOnSelectorAllParams = { selector: string, @@ -1228,7 +1229,7 @@ export type ElementHandleEvalOnSelectorAllParams = { arg: SerializedArgument, }; export type ElementHandleEvalOnSelectorAllResult = { - value?: SerializedValue, + value: SerializedValue, }; export type ElementHandleBoundingBoxParams = {}; export type ElementHandleBoundingBoxResult = { @@ -1642,7 +1643,7 @@ export type ElectronApplicationEvaluateExpressionParams = { arg: SerializedArgument, }; export type ElectronApplicationEvaluateExpressionResult = { - value?: SerializedValue, + value: SerializedValue, }; export type ElectronApplicationEvaluateExpressionHandleParams = { expression: string, diff --git a/src/rpc/client/jsHandle.ts b/src/rpc/client/jsHandle.ts index 312c2079f9..13812e7035 100644 --- a/src/rpc/client/jsHandle.ts +++ b/src/rpc/client/jsHandle.ts @@ -17,7 +17,7 @@ import { JSHandleChannel, JSHandleInitializer, SerializedArgument, SerializedValue, Channel } from '../channels'; import { ElementHandle } from './elementHandle'; import { ChannelOwner } from './channelOwner'; -import { serializeAsCallArgument, parseEvaluationResultValue } from '../../common/utilityScriptSerializers'; +import { parseSerializedValue, serializeValue } from '../serializers'; type NoHandles = Arg extends JSHandle ? never : (Arg extends object ? { [Key in keyof Arg]: NoHandles } : Arg); type Unboxed = @@ -99,14 +99,14 @@ export function serializeArgument(arg: any): SerializedArgument { handles.push(channel); return handles.length - 1; }; - const value = serializeAsCallArgument(arg, value => { + const value = serializeValue(arg, value => { if (value instanceof JSHandle) return { h: pushHandle(value._channel) }; return { fallThrough: value }; - }); + }, new Set()); return { value, handles }; } -export function parseResult(arg: SerializedValue): any { - return parseEvaluationResultValue(arg as any, []); +export function parseResult(value: SerializedValue): any { + return parseSerializedValue(value, undefined); } diff --git a/src/rpc/protocol.pdl b/src/rpc/protocol.pdl index b2bdb403eb..491cbbac89 100644 --- a/src/rpc/protocol.pdl +++ b/src/rpc/protocol.pdl @@ -12,14 +12,11 @@ # See the License for the specific language governing permissions and # limitations under the License. -union SerializedValue - undefined - boolean - number - string - ComplexSerializedValue - -type ComplexSerializedValue +# Exactly one of the optional fields must be present. +type SerializedValue + n?: number + b?: boolean + s?: string v?: enum null undefined @@ -27,16 +24,21 @@ type ComplexSerializedValue Infinity -Infinity -0 + # String representation of the Date. d?: string + # Regular expression pattern and flags. r?: object p: string f: string a?: SerializedValue[] + # Object with keys and values. o?: object[] k: string v: SerializedValue + # An index in the handles array from SerializedArgument. h?: number +# Represents a value with handle references. type SerializedArgument value: SerializedValue handles: Channel[] @@ -756,7 +758,7 @@ interface Frame isFunction: boolean arg: SerializedArgument returns - value?: SerializedValue + value: SerializedValue command evalOnSelectorAll parameters @@ -765,7 +767,7 @@ interface Frame isFunction: boolean arg: SerializedArgument returns - value?: SerializedValue + value: SerializedValue command addScriptTag parameters @@ -846,7 +848,7 @@ interface Frame isFunction: boolean arg: SerializedArgument returns - value?: SerializedValue + value: SerializedValue command evaluateExpressionHandle parameters @@ -1031,7 +1033,7 @@ interface Worker isFunction: boolean arg: SerializedArgument returns - value?: SerializedValue + value: SerializedValue command evaluateExpressionHandle parameters @@ -1057,7 +1059,7 @@ interface JSHandle isFunction: boolean arg: SerializedArgument returns - value?: SerializedValue + value: SerializedValue command evaluateExpressionHandle parameters @@ -1081,7 +1083,7 @@ interface JSHandle command jsonValue returns - value?: SerializedValue + value: SerializedValue interface ElementHandle extends JSHandle command evalOnSelector @@ -1091,7 +1093,7 @@ interface ElementHandle extends JSHandle isFunction: boolean arg: SerializedArgument returns - value?: SerializedValue + value: SerializedValue command evalOnSelectorAll parameters @@ -1100,7 +1102,7 @@ interface ElementHandle extends JSHandle isFunction: boolean arg: SerializedArgument returns - value?: SerializedValue + value: SerializedValue command boundingBox returns @@ -1458,7 +1460,7 @@ interface ElectronApplication isFunction: boolean arg: SerializedArgument returns - value?: SerializedValue + value: SerializedValue command evaluateExpressionHandle parameters diff --git a/src/rpc/serializers.ts b/src/rpc/serializers.ts index e4349b6840..b6702c9066 100644 --- a/src/rpc/serializers.ts +++ b/src/rpc/serializers.ts @@ -21,18 +21,20 @@ import * as util from 'util'; import { TimeoutError } from '../errors'; import * as types from '../types'; import { helper, assert } from '../helper'; -import { SerializedError, AXNode } from './channels'; -import { serializeAsCallArgument, parseEvaluationResultValue } from '../common/utilityScriptSerializers'; +import { SerializedError, AXNode, SerializedValue } from './channels'; export function serializeError(e: any): SerializedError { if (helper.isError(e)) return { error: { message: e.message, stack: e.stack, name: e.name } }; - return { value: serializeAsCallArgument(e, value => ({ fallThrough: value })) }; + return { value: serializeValue(e, value => ({ fallThrough: value }), new Set()) }; } export function parseError(error: SerializedError): Error { - if (!error.error) - return parseEvaluationResultValue(error.value as any, []); + if (!error.error) { + if (error.value === undefined) + throw new Error('Serialized error must have either an error or a value'); + return parseSerializedValue(error.value, undefined); + } if (error.error.name === 'TimeoutError') { const e = new TimeoutError(error.error.message); e.stack = error.error.stack || ''; @@ -169,3 +171,117 @@ export function axNodeFromProtocol(axNode: AXNode): types.SerializedAXNode { delete (result as any).valueString; return result; } + +export function parseSerializedValue(value: SerializedValue, handles: any[] | undefined): any { + if (value.n !== undefined) + return value.n; + if (value.s !== undefined) + return value.s; + if (value.b !== undefined) + return value.b; + if (value.v !== undefined) { + if (value.v === 'undefined') + return undefined; + if (value.v === 'null') + return null; + if (value.v === 'NaN') + return NaN; + if (value.v === 'Infinity') + return Infinity; + if (value.v === '-Infinity') + return -Infinity; + if (value.v === '-0') + return -0; + } + if (value.d !== undefined) + return new Date(value.d); + if (value.r !== undefined) + return new RegExp(value.r.p, value.r.f); + if (value.a !== undefined) + return value.a.map((a: any) => parseSerializedValue(a, handles)); + if (value.o !== undefined) { + const result: any = {}; + for (const { k, v } of value.o) + result[k] = parseSerializedValue(v, handles); + return result; + } + if (value.h !== undefined) { + if (handles === undefined) + throw new Error('Unexpected handle'); + return handles[value.h]; + } + throw new Error('Unexpected value'); +} + +export type HandleOrValue = { h: number } | { fallThrough: any }; +export function serializeValue(value: any, handleSerializer: (value: any) => HandleOrValue, visited: Set): SerializedValue { + const handle = handleSerializer(value); + if ('fallThrough' in handle) + value = handle.fallThrough; + else + return handle; + + if (visited.has(value)) + throw new Error('Argument is a circular structure'); + if (typeof value === 'symbol') + return { v: 'undefined' }; + if (Object.is(value, undefined)) + return { v: 'undefined' }; + if (Object.is(value, null)) + return { v: 'null' }; + if (Object.is(value, NaN)) + return { v: 'NaN' }; + if (Object.is(value, Infinity)) + return { v: 'Infinity' }; + if (Object.is(value, -Infinity)) + return { v: '-Infinity' }; + if (Object.is(value, -0)) + return { v: '-0' }; + if (typeof value === 'boolean') + return { b: value }; + if (typeof value === 'number') + return { n: value }; + if (typeof value === 'string') + return { s: value }; + if (isError(value)) { + const error = value; + if ('captureStackTrace' in global.Error) { + // v8 + return { s: error.stack || '' }; + } + return { s: `${error.name}: ${error.message}\n${error.stack}` }; + } + if (isDate(value)) + return { d: value.toJSON() }; + if (isRegExp(value)) + return { r: { p: value.source, f: value.flags } }; + if (Array.isArray(value)) { + const a = []; + visited.add(value); + for (let i = 0; i < value.length; ++i) + a.push(serializeValue(value[i], handleSerializer, visited)); + visited.delete(value); + return { a }; + } + if (typeof value === 'object') { + const o: { k: string, v: SerializedValue }[] = []; + visited.add(value); + for (const name of Object.keys(value)) + o.push({ k: name, v: serializeValue(value[name], handleSerializer, visited) }); + visited.delete(value); + return { o }; + } + throw new Error('Unexpected value'); +} + +function isRegExp(obj: any): obj is RegExp { + return obj instanceof RegExp || Object.prototype.toString.call(obj) === '[object RegExp]'; +} + +function isDate(obj: any): obj is Date { + return obj instanceof Date || Object.prototype.toString.call(obj) === '[object Date]'; +} + +function isError(obj: any): obj is Error { + return obj instanceof Error || (obj && obj.__proto__ && obj.__proto__.name === 'Error'); +} diff --git a/src/rpc/server/electronDispatcher.ts b/src/rpc/server/electronDispatcher.ts index 0192b563c5..dd70887c54 100644 --- a/src/rpc/server/electronDispatcher.ts +++ b/src/rpc/server/electronDispatcher.ts @@ -16,13 +16,12 @@ import { Dispatcher, DispatcherScope, lookupDispatcher } from './dispatcher'; import { Electron, ElectronApplication, ElectronEvents, ElectronPage } from '../../server/electron'; -import { ElectronApplicationChannel, ElectronApplicationInitializer, PageChannel, JSHandleChannel, ElectronInitializer, ElectronChannel, SerializedArgument, ElectronLaunchParams } from '../channels'; +import { ElectronApplicationChannel, ElectronApplicationInitializer, PageChannel, JSHandleChannel, ElectronInitializer, ElectronChannel, SerializedArgument, ElectronLaunchParams, SerializedValue } from '../channels'; import { BrowserContextDispatcher } from './browserContextDispatcher'; import { BrowserContextBase } from '../../browserContext'; import { PageDispatcher } from './pageDispatcher'; import { parseArgument, serializeResult } from './jsHandleDispatcher'; import { createHandle } from './elementHandlerDispatcher'; -import { SerializedValue } from '../../common/utilityScriptSerializers'; import { envArrayToObject } from '../serializers'; export class ElectronDispatcher extends Dispatcher implements ElectronChannel { diff --git a/src/rpc/server/elementHandlerDispatcher.ts b/src/rpc/server/elementHandlerDispatcher.ts index 9b88d7c919..9a61f8e073 100644 --- a/src/rpc/server/elementHandlerDispatcher.ts +++ b/src/rpc/server/elementHandlerDispatcher.ts @@ -17,11 +17,10 @@ import { ElementHandle } from '../../dom'; import * as js from '../../javascript'; import * as types from '../../types'; -import { ElementHandleChannel, FrameChannel, Binary, SerializedArgument } from '../channels'; +import { ElementHandleChannel, FrameChannel, Binary, SerializedArgument, SerializedValue } from '../channels'; import { DispatcherScope, lookupNullableDispatcher } from './dispatcher'; import { JSHandleDispatcher, serializeResult, parseArgument } from './jsHandleDispatcher'; import { FrameDispatcher } from './frameDispatcher'; -import { SerializedValue } from '../../common/utilityScriptSerializers'; export function createHandle(scope: DispatcherScope, handle: js.JSHandle): JSHandleDispatcher { return handle.asElement() ? new ElementHandleDispatcher(scope, handle.asElement()!) : new JSHandleDispatcher(scope, handle); diff --git a/src/rpc/server/frameDispatcher.ts b/src/rpc/server/frameDispatcher.ts index 3a6d40f2e5..cdd1c2423c 100644 --- a/src/rpc/server/frameDispatcher.ts +++ b/src/rpc/server/frameDispatcher.ts @@ -16,12 +16,11 @@ import { Frame, kAddLifecycleEvent, kRemoveLifecycleEvent, kNavigationEvent, NavigationEvent } from '../../frames'; import * as types from '../../types'; -import { ElementHandleChannel, FrameChannel, FrameInitializer, JSHandleChannel, ResponseChannel, SerializedArgument, FrameWaitForFunctionParams } from '../channels'; +import { ElementHandleChannel, FrameChannel, FrameInitializer, JSHandleChannel, ResponseChannel, SerializedArgument, FrameWaitForFunctionParams, SerializedValue } from '../channels'; import { Dispatcher, DispatcherScope, lookupNullableDispatcher, existingDispatcher } from './dispatcher'; import { convertSelectOptionValues, ElementHandleDispatcher, createHandle, convertInputFiles } from './elementHandlerDispatcher'; import { parseArgument, serializeResult } from './jsHandleDispatcher'; import { ResponseDispatcher, RequestDispatcher } from './networkDispatchers'; -import { SerializedValue } from '../../common/utilityScriptSerializers'; export class FrameDispatcher extends Dispatcher implements FrameChannel { private _frame: Frame; diff --git a/src/rpc/server/jsHandleDispatcher.ts b/src/rpc/server/jsHandleDispatcher.ts index e82141b483..ef6907a945 100644 --- a/src/rpc/server/jsHandleDispatcher.ts +++ b/src/rpc/server/jsHandleDispatcher.ts @@ -15,10 +15,10 @@ */ import * as js from '../../javascript'; -import { JSHandleChannel, JSHandleInitializer, SerializedArgument } from '../channels'; +import { JSHandleChannel, JSHandleInitializer, SerializedArgument, SerializedValue } from '../channels'; import { Dispatcher, DispatcherScope } from './dispatcher'; -import { parseEvaluationResultValue, serializeAsCallArgument, SerializedValue } from '../../common/utilityScriptSerializers'; import { createHandle } from './elementHandlerDispatcher'; +import { parseSerializedValue, serializeValue } from '../serializers'; export class JSHandleDispatcher extends Dispatcher implements JSHandleChannel { @@ -63,9 +63,9 @@ export class JSHandleDispatcher extends Dispatcher (arg as JSHandleDispatcher)._object)); + return parseSerializedValue(arg.value, arg.handles.map(a => (a as JSHandleDispatcher)._object)); } export function serializeResult(arg: any): SerializedValue { - return serializeAsCallArgument(arg, value => ({ fallThrough: value })); + return serializeValue(arg, value => ({ fallThrough: value }), new Set()); } diff --git a/src/rpc/server/pageDispatcher.ts b/src/rpc/server/pageDispatcher.ts index 50a9e398bf..5b10fa8b9f 100644 --- a/src/rpc/server/pageDispatcher.ts +++ b/src/rpc/server/pageDispatcher.ts @@ -20,7 +20,7 @@ import { Frame } from '../../frames'; import { Request } from '../../network'; import { Page, Worker } from '../../page'; import * as types from '../../types'; -import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel, WorkerInitializer, WorkerChannel, JSHandleChannel, Binary, SerializedArgument, PagePdfParams, SerializedError, PageAccessibilitySnapshotResult } from '../channels'; +import { BindingCallChannel, BindingCallInitializer, ElementHandleChannel, PageChannel, PageInitializer, ResponseChannel, WorkerInitializer, WorkerChannel, JSHandleChannel, Binary, SerializedArgument, PagePdfParams, SerializedError, PageAccessibilitySnapshotResult, SerializedValue } from '../channels'; import { Dispatcher, DispatcherScope, lookupDispatcher, lookupNullableDispatcher } from './dispatcher'; import { parseError, serializeError, headersArrayToObject, axNodeToProtocol } from '../serializers'; import { ConsoleMessageDispatcher } from './consoleMessageDispatcher'; @@ -32,7 +32,6 @@ import { serializeResult, parseArgument } from './jsHandleDispatcher'; import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatcher'; import { FileChooser } from '../../fileChooser'; import { CRCoverage } from '../../chromium/crCoverage'; -import { SerializedValue } from '../../common/utilityScriptSerializers'; export class PageDispatcher extends Dispatcher implements PageChannel { private _page: Page; diff --git a/utils/generate_channels.js b/utils/generate_channels.js index e769d29584..c3d84a5cbd 100755 --- a/utils/generate_channels.js +++ b/utils/generate_channels.js @@ -51,7 +51,8 @@ function titleCase(name) { return name[0].toUpperCase() + name.substring(1); } -function inlineType(type, item, indent) { +function inlineType(item, indent) { + let type = item.words[1]; const array = type.endsWith('[]'); if (array) type = type.substring(0, type.length - 2); @@ -93,7 +94,7 @@ function properties(item, indent) { const optional = name.endsWith('?'); if (optional) name = name.substring(0, name.length - 1); - result.push(`${indent}${name}${optional ? '?' : ''}: ${inlineType(prop.words[1], prop, indent)},`); + result.push(`${indent}${name}${optional ? '?' : ''}: ${inlineType(prop, indent)},`); } return result.join('\n'); } @@ -140,15 +141,7 @@ for (const item of list) { } for (const item of list) { - if (item.words[0] === 'union') { - if (item.words.length !== 2) - raise(item); - result.push(`export type ${item.words[1]} = ${item.list.map(clause => { - if (clause.words.length !== 1) - raise(clause); - return inlineType(clause.words[0], clause, ' '); - }).join(' | ')};`); - } else if (item.words[0] === 'type') { + if (item.words[0] === 'type') { if (item.words.length !== 2) raise(item); result.push(`export type ${item.words[1]} = {`);