Skip to content

Commit

Permalink
review
Browse files Browse the repository at this point in the history
  • Loading branch information
ZekeLu committed Feb 15, 2023
1 parent c4bc3e8 commit feef44e
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 127 deletions.
146 changes: 70 additions & 76 deletions expose.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,109 +9,103 @@ import (
"github.com/chromedp/cdproto/runtime"
)

const (
deliverError = "deliverError"
deliverResult = "deliverResult"
addTargetBinding = "addTargetBinding"
)

// BindingCalledPayload ...
type BindingCalledPayload struct {
Type string `json:"type"`
Name string `json:"name"`
Seq int64 `json:"seq"`
Args string `json:"args"`
}
// ExposedFunc is the function type that can be exposed to the browser env.
type ExposedFunc func(args string) (string, error)

// BindingFunc expose function type
type BindingFunc func(args string) (string, error)

// AddScriptToEvaluateOnNewDocument ...
func AddScriptToEvaluateOnNewDocument(script string) Action {
return ActionFunc(func(ctx context.Context) error {
_, err := page.AddScriptToEvaluateOnNewDocument(script).Do(ctx)
return err
})
}

// ExposeAction are actions which expose local functions to browser env.
// ExposeAction are actions which expose Go functions to the browser env.
type ExposeAction Action

// Expose is an action to add a function called fnName on the browser page's window object.
// When called, the function executes BindingFunc in go env
// and returns a Promise which resolves to the return value of BindingFunc.
// Note. compared with puppeteer's exposeFunction.
// the BindingFunc takes exactly one argument, this argument should be string
// Note. Do not expose the same function name many times, it will only take effect for the first time.
func Expose(fnName string, fn BindingFunc) ExposeAction {
// Expose is an action to add a function called fnName on the browser page's
// window object. When called, the function executes fn in the Go env and
// returns a Promise which resolves to the return value of fn.
//
// Note:
// 1. This is the lite version of puppeteer's [page.exposeFunction].
// 2. It adds "chromedpExposeFunc" to the page's window object too.
// 3. The exposed function survives page navigation until the tab is closed?
// 4. (iframe?)
// 5. Avoid exposing multiple funcs with the same name.
// 6. Maybe you just need runtime.AddBinding.
//
// [page.exposeFunction]: https://github.com/puppeteer/puppeteer/blob/v19.2.2/docs/api/puppeteer.page.exposefunction.md
func Expose(fnName string, fn ExposedFunc) ExposeAction {
return ActionFunc(func(ctx context.Context) error {

// adds binding with the given name on the global objects of all inspected contexts
err := Run(ctx, runtime.AddBinding(fnName))
if err != nil {
return err
}

expression := fmt.Sprintf(`%s("%s","%s");`, addTargetBinding, "cdpExposedFun", fnName)

// inject bindingFunc wrapper into current window
err = Run(ctx, Evaluate(exposeJS, nil))
if err != nil {
return err
}

err = Run(ctx, Evaluate(expression, nil))
if err != nil {
return err
}

// we also want to make it effective after nav url
// it evaluates given script in every frame upon creation (before loading frame's scripts)
err = Run(ctx, AddScriptToEvaluateOnNewDocument(exposeJS))
if err != nil {
return err
}

err = Run(ctx, AddScriptToEvaluateOnNewDocument(expression))
expression := fmt.Sprintf(`chromedpExposeFunc.wrapBinding("exposedFun","%s");`, fnName)
err := Run(ctx,
runtime.AddBinding(fnName),
Evaluate(exposeJS, nil),
Evaluate(expression, nil),
// Make it effective after navigation.
addScriptToEvaluateOnNewDocument(exposeJS),
addScriptToEvaluateOnNewDocument(expression),
)
if err != nil {
return err
}

ListenTarget(ctx, func(ev interface{}) {
switch ev := ev.(type) {
case *runtime.EventBindingCalled:
var payload BindingCalledPayload
if ev.Payload == "" {
return
}

var payload struct {
Type string `json:"type"`
Name string `json:"name"`
Seq int64 `json:"seq"`
Args string `json:"args"`
}

err := json.Unmarshal([]byte(ev.Payload), &payload)
if err != nil {
return
}

if payload.Type != "cdpExposedFun" {
if payload.Type != "exposedFun" || payload.Name != fnName {
return
}

if payload.Name == fnName {
callFnName := deliverResult
result, err := fn(payload.Args)
result, err := fn(payload.Args)

if err != nil {
result = err.Error()
callFnName = deliverError
}
callback := "chromedpExposeFunc.deliverResult"
if err != nil {
result = err.Error()
callback = "chromedpExposeFunc.deliverError"
}

// Prevent the message from being processed by other functions
ev.Payload = ""
// Prevent the message from being processed by other functions
ev.Payload = ""

go func() {
err := Run(ctx,
CallFunctionOn(callback,
nil,
func(p *runtime.CallFunctionOnParams) *runtime.CallFunctionOnParams {
return p.WithExecutionContextID(ev.ExecutionContextID)
},
payload.Name,
payload.Seq,
result,
),
)

go func() {
Run(ctx, CallFunctionOn(callFnName, nil, func(p *runtime.CallFunctionOnParams) *runtime.CallFunctionOnParams {
return p.WithExecutionContextID(ev.ExecutionContextID)
}, payload.Name, payload.Seq, result))
}()
}
if err != nil {
c := FromContext(ctx)
c.Browser.errf("failed to deliver result to exposed func %s: %s", fnName, err)
}
}()
}
})

return nil
})
}

func addScriptToEvaluateOnNewDocument(script string) Action {
return ActionFunc(func(ctx context.Context) error {
_, err := page.AddScriptToEvaluateOnNewDocument(script).Do(ctx)
return err
})
}
5 changes: 1 addition & 4 deletions expose_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,6 @@ func TestExpose(t *testing.T) {
ctx, cancel := testAllocate(t, "")
defer cancel()

// creates a new page. about:blank
Run(ctx)

// expose md5SumFunc function as md5 to browser current page and every frame
if err := Run(ctx, Expose("md5", md5SumFunc)); err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -68,7 +65,7 @@ func TestExpose(t *testing.T) {

// 2. Navigate another page
if err := Run(ctx,
Navigate(testdataDir+"/expose.html"),
Navigate(testdataDir+"/child1.html"),
); err != nil {
t.Fatal(err)
}
Expand Down
75 changes: 37 additions & 38 deletions js/expose.js
Original file line number Diff line number Diff line change
@@ -1,44 +1,43 @@
function deliverError(name, seq, message, stack) {
const error = new Error(message);
error.stack = stack;
window["CDP_BINDING_" + name].callbacks.get(seq).reject(error);
window["CDP_BINDING_" + name].callbacks.delete(seq);
}
var chromedpExposeFunc = chromedpExposeFunc || {
bindings: {},
deliverError: function (name, seq, message) {
const error = new Error(message);
chromedpExposeFunc.bindings[name].callbacks.get(seq).reject(error);
chromedpExposeFunc.bindings[name].callbacks.delete(seq);
},
deliverResult: function (name, seq, result) {
chromedpExposeFunc.bindings[name].callbacks.get(seq).resolve(result);
chromedpExposeFunc.bindings[name].callbacks.delete(seq);
},
wrapBinding: function (type, name) {
// Store the binding function added by the call of runtime.AddBinding.
chromedpExposeFunc.bindings[name] = window[name];

function deliverResult(name, seq, result) {
window["CDP_BINDING_" + name].callbacks.get(seq).resolve(result);
window["CDP_BINDING_" + name].callbacks.delete(seq);
}
// Replace the binding function.
Object.assign(window, {
[name](args) {
if (typeof args != 'string') {
return Promise.reject(
new Error(
'function takes exactly one argument, this argument should be string'
)
);
}

function addTargetBinding(type, name) {
// This is the CDP binding.
window["CDP_BINDING_" + name] = window[name];

// We replace the CDP binding with a chromedp binding.
Object.assign(window, {
[name](args) {
if(typeof args != "string"){
return Promise.reject(new Error('function takes exactly one argument, this argument should be string'))
}
const binding = chromedpExposeFunc.bindings[name];

// This is the chromedp binding.
const callChromedp = window["CDP_BINDING_" + name];
binding.callbacks ??= new Map();

if (callChromedp.callbacks == undefined) {
callChromedp.callbacks = new Map()
}
if (callChromedp.lastSeq == undefined) {
callChromedp.lastSeq = 0
}
const seq = (binding.lastSeq ?? 0) + 1;
binding.lastSeq = seq;

const seq = callChromedp.lastSeq + 1
callChromedp.lastSeq = seq;

callChromedp(JSON.stringify({ type, name, seq, args }));
// Call the binding function to trigger runtime.EventBindingCalled.
binding(JSON.stringify({ type, name, seq, args }));

return new Promise((resolve, reject) => {
callChromedp.callbacks.set(seq, { resolve, reject });
});
},
});
}
return new Promise((resolve, reject) => {
binding.callbacks.set(seq, { resolve, reject });
});
},
});
},
};
9 changes: 0 additions & 9 deletions testdata/expose.html

This file was deleted.

0 comments on commit feef44e

Please sign in to comment.