Skip to content

Commit

Permalink
Complete Tutorial (#256) and fix #344.
Browse files Browse the repository at this point in the history
Do not inherit client handlers nor javascript bindings
in DevTools windows (#344).
  • Loading branch information
cztomczak committed Apr 7, 2017
1 parent 850ac55 commit bc4e038
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 64 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Table of contents:
* [Introduction](#introduction)
* [Install](#install)
* [Tutorial](#tutorial)
* [Examples](#examples)
* [Support](#support)
* [Releases](#releases)
Expand Down
1 change: 0 additions & 1 deletion api/JavascriptBindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

Table of contents:
* [Introduction](#introduction)
* [Example usage](#example-usage)
* [Methods](#methods)
* [\_\_init\_\_()](#__init__)
* [IsValueAllowed](#isvalueallowed)
Expand Down
52 changes: 45 additions & 7 deletions docs/Tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ The cef.ExceptHook helper function does the following:
which exits the process with status 1, without calling
cleanup handlers, flushing stdio buffers, etc.

See CEF Python's ExceptHook source code [here](../../../search?utf8=%E2%9C%93&q=%22def+excepthook%28exc_type%22&type=).
See CEF Python's ExceptHook source code in src/[helpers.pyx](../src/helpers.pyx).


## Message loop
Expand Down Expand Up @@ -322,10 +322,17 @@ def set_client_handlers(browser):
...
class LoadHandler(object):
def OnLoadingStateChange(self, browser, is_loading, **_):
# Issue #344 will fix this in next release, so that client
# handlers are not called for Developer Tools windows.
if browser.GetUrl().startswith("chrome-devtools://"):
return
# This callback is called twice, once when loading starts
# (is_loading=True) and second time when loading ends
# (is_loading=False).
if not is_loading:
# Loading is complete
js_print(browser, "Python: LoadHandler.OnLoadingStateChange:"
"loading is complete")
# Loading is complete. DOM is ready.
js_print(browser, "Python", "OnLoadingStateChange",
"Loading is complete")
```


Expand Down Expand Up @@ -372,12 +379,43 @@ messaging:
however pass Python functions when executing javascript
callbacks mentioned earlier.

In tutorial.py example you will find example code that uses
javascript bindings and other APIs mentioned above.
In [tutorial.py](../examples/tutorial.py) example you will find
example usage of javascript bindings, javascript callbacks
and python callbacks. Here is some source code:

```
set_javascript_bindings(browser)
...
def set_javascript_bindings(browser):
bindings = cef.JavascriptBindings(
bindToFrames=False, bindToPopups=False)
bindings.SetFunction("html_to_data_uri", html_to_data_uri)
browser.SetJavascriptBindings(bindings)
...

def html_to_data_uri(html, js_callback=None):
# This function is called in two ways:
# 1. From Python: in this case value is returned
# 2. From Javascript: in this case value cannot be returned because
# inter-process messaging is asynchronous, so must return value
# by calling js_callback.
html = html.encode("utf-8", "replace")
b64 = base64.b64encode(html).decode("utf-8", "replace")
ret = "data:text/html;base64,{data}".format(data=b64)
if js_callback:
js_print(js_callback.GetFrame().GetBrowser(),
"Python", "html_to_data_uri",
"Called from Javascript. Will call Javascript callback now.")
js_callback.Call(ret)
else:
return ret
...
<script>
function js_callback_1(ret) {
js_print("Javascript", "html_to_data_uri", ret);
}
html_to_data_uri("test", js_callback_1);
</script>
```

**Communication using http requests**

Expand Down
132 changes: 113 additions & 19 deletions examples/tutorial.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,60 @@
import base64
import platform
import sys
import threading

# HTML code. Browser will navigate to a Data uri created
# from this html code.
HTML_code = """
test
<!DOCTYPE html>
<html>
<head>
<style type="text/css">
body,html { font-family: Arial; font-size: 11pt; }
div.msg { margin: 0.2em; line-height: 1.4em; }
b { background: #ccc; font-weight: bold; font-size: 10pt;
padding: 0.1em 0.2em; }
b.Python { background: #eee; }
i { font-family: Courier new; font-size: 10pt; border: #eee 1px solid;
padding: 0.1em 0.2em; }
</style>
<script>
function js_print(lang, event, msg) {
msg = "<b class="+lang+">"+lang+": "+event+":</b> " + msg;
console = document.getElementById("console")
console.innerHTML += "<div class=msg>"+msg+"</div>";
}
function js_callback_1(ret) {
js_print("Javascript", "html_to_data_uri", ret);
}
function js_callback_2(msg, py_callback) {
js_print("Javascript", "js_callback", msg);
py_callback("String sent from Javascript");
}
window.onload = function(){
js_print("Javascript", "window.onload", "Called");
js_print("Javascript", "python_property", python_property);
html_to_data_uri("test", js_callback_1);
external.test_multiple_callbacks(js_callback_2);
};
</script>
</head>
<body>
<h1>Tutorial example</h1>
<div id="console"></div>
</body>
</html>
"""


def main():
check_versions()
sys.excepthook = cef.ExceptHook # To shutdown all CEF processes on error
settings = {"cache_path": "webcache"}
cef.Initialize(settings=settings)
cef.Initialize()
set_global_handler()
browser = cef.CreateBrowserSync(url=html_to_data_uri(HTML_code),
window_title="Hello World!")
Expand All @@ -34,10 +75,22 @@ def check_versions():
assert cef.__version__ >= "56.1", "CEF Python v56.1+ required to run this"


def html_to_data_uri(html):
def html_to_data_uri(html, js_callback=None):
# This function is called in two ways:
# 1. From Python: in this case value is returned
# 2. From Javascript: in this case value cannot be returned because
# inter-process messaging is asynchronous, so must return value
# by calling js_callback.
html = html.encode("utf-8", "replace")
b64 = base64.b64encode(html).decode("utf-8", "replace")
return "data:text/html;base64,{data}".format(data=b64)
ret = "data:text/html;base64,{data}".format(data=b64)
if js_callback:
js_print(js_callback.GetFrame().GetBrowser(),
"Python", "html_to_data_uri",
"Called from Javascript. Will call Javascript callback now.")
js_callback.Call(ret)
else:
return ret


def set_global_handler():
Expand All @@ -59,45 +112,86 @@ def set_javascript_bindings(browser):
external = External(browser)
bindings = cef.JavascriptBindings(
bindToFrames=False, bindToPopups=False)
bindings.SetProperty("python_property", "This property was set in Python")
bindings.SetFunction("html_to_data_uri", html_to_data_uri)
bindings.SetProperty("test_property", "This property was set in Python")
bindings.SetObject("external", external)
browser.SetJavascriptBindings(bindings)


def js_print(browser, msg):
browser.ExecuteFunction("js_print", msg)
def js_print(browser, lang, event, msg):
# Execute Javascript function "js_print"
browser.ExecuteFunction("js_print", lang, event, msg)


class GlobalHandler(object):
def OnAfterCreated(self, browser, **_):
js_print(browser,
"Python: GlobalHandler._OnAfterCreated: browser id={id}"
.format(id=browser.GetIdentifier()))
# Issue #344 will fix this in next release, so that client
# handlers are not called for Developer Tools windows.
if browser.GetUrl().startswith("chrome-devtools://"):
return
# DOM is not yet loaded. Using js_print at this moment will
# throw an error: "Uncaught ReferenceError: js_print is not defined".
# We make this error on purpose. This error will be intercepted
# in DisplayHandler.OnConsoleMessage.
js_print(browser, "Python", "OnAfterCreated",
"This will probably never display as DOM is not yet loaded")
# Delay print by 0.5 sec, because js_print is not available yet
args = [browser, "Python", "OnAfterCreated",
"(Delayed) Browser id="+str(browser.GetIdentifier())]
threading.Timer(0.5, js_print, args).start()


class LoadHandler(object):
def OnLoadingStateChange(self, browser, is_loading, **_):
# Issue #344 will fix this in next release, so that client
# handlers are not called for Developer Tools windows.
if browser.GetUrl().startswith("chrome-devtools://"):
return
# This callback is called twice, once when loading starts
# (is_loading=True) and second time when loading ends
# (is_loading=False).
if not is_loading:
# Loading is complete
js_print(browser, "Python: LoadHandler.OnLoadingStateChange:"
"loading is complete")
# Loading is complete. DOM is ready.
js_print(browser, "Python", "OnLoadingStateChange",
"Loading is complete")


class DisplayHandler(object):
def OnConsoleMessage(self, browser, message, **_):
# Issue #344 will fix this in next release, so that client
# handlers are not called for Developer Tools windows.
if browser.GetUrl().startswith("chrome-devtools://"):
return
# This will intercept js errors, see comments in OnAfterCreated
if "error" in message.lower() or "uncaught" in message.lower():
js_print(browser, "Python: LoadHandler.OnConsoleMessage: "
"intercepted Javascript error: {error}"
.format(error=message))
# Prevent infinite recurrence in case something went wrong
if "js_print is not defined" in message.lower():
if hasattr(self, "js_print_is_not_defined"):
print("Python: OnConsoleMessage: "
"Intercepted Javascript error: "+message)
return
else:
self.js_print_is_not_defined = True
# Delay print by 0.5 sec, because js_print may not be
# available yet due to DOM not ready.
args = [browser, "Python", "OnConsoleMessage",
"(Delayed) Intercepted Javascript error: <i>{error}</i>"
.format(error=message)]
threading.Timer(0.5, js_print, args).start()


class External(object):
def __init__(self, browser):
self.browser = browser

def test_function(self):
pass
def test_multiple_callbacks(self, js_callback):
"""Test both javascript and python callbacks."""
js_print(self.browser, "Python", "test_multiple_callbacks",
"Called from Javascript. Will call Javascript callback now.")

def py_callback(msg_from_js):
js_print(self.browser, "Python", "py_callback", msg_from_js)
js_callback.Call("String sent from Python", py_callback)


if __name__ == '__main__':
Expand Down
8 changes: 3 additions & 5 deletions src/browser.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ cdef PyBrowser GetPyBrowser(CefRefPtr[CefBrowser] cefBrowser,
# - Popups inherit javascript bindings only when "bindToPopups"
# constructor param was set to True.

if pyBrowser.IsPopup() and \
not pyBrowser.GetUserData("__outerWindowHandle"):
if pyBrowser.IsPopup()\
and not pyBrowser.GetUserData("__outerWindowHandle"):
openerHandle = pyBrowser.GetOpenerWindowHandle()
for identifier, tempPyBrowser in g_pyBrowsers.items():
if tempPyBrowser.GetWindowHandle() == openerHandle:
Expand Down Expand Up @@ -457,12 +457,10 @@ cdef class PyBrowser:
window_info.SetAsPopup(
<CefWindowHandle>self.GetOpenerWindowHandle(),
PyToCefStringValue("DevTools"))
cdef CefRefPtr[ClientHandler] client_handler =\
<CefRefPtr[ClientHandler]?>new ClientHandler()
cdef CefBrowserSettings settings
cdef CefPoint inspect_element_at
self.GetCefBrowserHost().get().ShowDevTools(
window_info, <CefRefPtr[CefClient]?>client_handler, settings,
window_info, <CefRefPtr[CefClient]?>NULL, settings,
inspect_element_at)

cpdef py_void StopLoad(self):
Expand Down
64 changes: 32 additions & 32 deletions src/helpers.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,38 @@ import time
import codecs


def ExceptHook(exc_type, exc_value, exc_trace):
"""Global except hook to exit app cleanly on error.
This hook does the following: in case of exception write it to
the "error.log" file, display it to the console, shutdown CEF
and exit application immediately by ignoring "finally" (_exit()).
"""
print("[CEF Python] ExceptHook: catched exception, will shutdown CEF")
QuitMessageLoop()
Shutdown()
msg = "".join(traceback.format_exception(exc_type, exc_value,
exc_trace))
error_file = GetAppPath("error.log")
encoding = GetAppSetting("string_encoding") or "utf-8"
if type(msg) == bytes:
msg = msg.decode(encoding=encoding, errors="replace")
try:
with codecs.open(error_file, mode="a", encoding=encoding) as fp:
fp.write("\n[%s] %s\n" % (
time.strftime("%Y-%m-%d %H:%M:%S"), msg))
except:
print("[CEF Python] WARNING: failed writing to error file: %s" % (
error_file))
# Convert error message to ascii before printing, otherwise
# you may get error like this:
# | UnicodeEncodeError: 'charmap' codec can't encode characters
msg = msg.encode("ascii", errors="replace")
msg = msg.decode("ascii", errors="replace")
print("\n"+msg)
# noinspection PyProtectedMember
os._exit(1)


cpdef str GetModuleDirectory():
"""Get path to the cefpython module (so/pyd)."""
if platform.system() == "Linux" and os.getenv("CEFPYTHON3_PATH"):
Expand Down Expand Up @@ -58,35 +90,3 @@ cpdef str GetAppPath(file_=None):
path = re.sub(r"[/\\]+$", "", path)
return path
return str(file_)


def ExceptHook(exc_type, exc_value, exc_trace):
"""Global except hook to exit app cleanly on error.
This hook does the following: in case of exception write it to
the "error.log" file, display it to the console, shutdown CEF
and exit application immediately by ignoring "finally" (_exit()).
"""
print("[CEF Python] ExceptHook: catched exception, will shutdown CEF")
QuitMessageLoop()
Shutdown()
msg = "".join(traceback.format_exception(exc_type, exc_value,
exc_trace))
error_file = GetAppPath("error.log")
encoding = GetAppSetting("string_encoding") or "utf-8"
if type(msg) == bytes:
msg = msg.decode(encoding=encoding, errors="replace")
try:
with codecs.open(error_file, mode="a", encoding=encoding) as fp:
fp.write("\n[%s] %s\n" % (
time.strftime("%Y-%m-%d %H:%M:%S"), msg))
except:
print("[CEF Python] WARNING: failed writing to error file: %s" % (
error_file))
# Convert error message to ascii before printing, otherwise
# you may get error like this:
# | UnicodeEncodeError: 'charmap' codec can't encode characters
msg = msg.encode("ascii", errors="replace")
msg = msg.decode("ascii", errors="replace")
print("\n"+msg)
# noinspection PyProtectedMember
os._exit(1)

0 comments on commit bc4e038

Please sign in to comment.