Skip to content

Commit bc4e038

Browse files
committed
Complete Tutorial (cztomczak#256) and fix cztomczak#344.
Do not inherit client handlers nor javascript bindings in DevTools windows (cztomczak#344).
1 parent 850ac55 commit bc4e038

File tree

6 files changed

+194
-64
lines changed

6 files changed

+194
-64
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
Table of contents:
44
* [Introduction](#introduction)
55
* [Install](#install)
6+
* [Tutorial](#tutorial)
67
* [Examples](#examples)
78
* [Support](#support)
89
* [Releases](#releases)

api/JavascriptBindings.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
Table of contents:
88
* [Introduction](#introduction)
9-
* [Example usage](#example-usage)
109
* [Methods](#methods)
1110
* [\_\_init\_\_()](#__init__)
1211
* [IsValueAllowed](#isvalueallowed)

docs/Tutorial.md

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ The cef.ExceptHook helper function does the following:
157157
which exits the process with status 1, without calling
158158
cleanup handlers, flushing stdio buffers, etc.
159159

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

162162

163163
## Message loop
@@ -322,10 +322,17 @@ def set_client_handlers(browser):
322322
...
323323
class LoadHandler(object):
324324
def OnLoadingStateChange(self, browser, is_loading, **_):
325+
# Issue #344 will fix this in next release, so that client
326+
# handlers are not called for Developer Tools windows.
327+
if browser.GetUrl().startswith("chrome-devtools://"):
328+
return
329+
# This callback is called twice, once when loading starts
330+
# (is_loading=True) and second time when loading ends
331+
# (is_loading=False).
325332
if not is_loading:
326-
# Loading is complete
327-
js_print(browser, "Python: LoadHandler.OnLoadingStateChange:"
328-
"loading is complete")
333+
# Loading is complete. DOM is ready.
334+
js_print(browser, "Python", "OnLoadingStateChange",
335+
"Loading is complete")
329336
```
330337

331338

@@ -372,12 +379,43 @@ messaging:
372379
however pass Python functions when executing javascript
373380
callbacks mentioned earlier.
374381

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

386+
```
387+
set_javascript_bindings(browser)
378388
...
389+
def set_javascript_bindings(browser):
390+
bindings = cef.JavascriptBindings(
391+
bindToFrames=False, bindToPopups=False)
392+
bindings.SetFunction("html_to_data_uri", html_to_data_uri)
393+
browser.SetJavascriptBindings(bindings)
379394
...
380-
395+
def html_to_data_uri(html, js_callback=None):
396+
# This function is called in two ways:
397+
# 1. From Python: in this case value is returned
398+
# 2. From Javascript: in this case value cannot be returned because
399+
# inter-process messaging is asynchronous, so must return value
400+
# by calling js_callback.
401+
html = html.encode("utf-8", "replace")
402+
b64 = base64.b64encode(html).decode("utf-8", "replace")
403+
ret = "data:text/html;base64,{data}".format(data=b64)
404+
if js_callback:
405+
js_print(js_callback.GetFrame().GetBrowser(),
406+
"Python", "html_to_data_uri",
407+
"Called from Javascript. Will call Javascript callback now.")
408+
js_callback.Call(ret)
409+
else:
410+
return ret
411+
...
412+
<script>
413+
function js_callback_1(ret) {
414+
js_print("Javascript", "html_to_data_uri", ret);
415+
}
416+
html_to_data_uri("test", js_callback_1);
417+
</script>
418+
```
381419

382420
**Communication using http requests**
383421

examples/tutorial.py

Lines changed: 113 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,60 @@
55
import base64
66
import platform
77
import sys
8+
import threading
89

910
# HTML code. Browser will navigate to a Data uri created
1011
# from this html code.
1112
HTML_code = """
12-
test
13+
<!DOCTYPE html>
14+
<html>
15+
<head>
16+
<style type="text/css">
17+
body,html { font-family: Arial; font-size: 11pt; }
18+
div.msg { margin: 0.2em; line-height: 1.4em; }
19+
b { background: #ccc; font-weight: bold; font-size: 10pt;
20+
padding: 0.1em 0.2em; }
21+
b.Python { background: #eee; }
22+
i { font-family: Courier new; font-size: 10pt; border: #eee 1px solid;
23+
padding: 0.1em 0.2em; }
24+
</style>
25+
26+
<script>
27+
function js_print(lang, event, msg) {
28+
msg = "<b class="+lang+">"+lang+": "+event+":</b> " + msg;
29+
console = document.getElementById("console")
30+
console.innerHTML += "<div class=msg>"+msg+"</div>";
31+
}
32+
33+
function js_callback_1(ret) {
34+
js_print("Javascript", "html_to_data_uri", ret);
35+
}
36+
37+
function js_callback_2(msg, py_callback) {
38+
js_print("Javascript", "js_callback", msg);
39+
py_callback("String sent from Javascript");
40+
}
41+
42+
window.onload = function(){
43+
js_print("Javascript", "window.onload", "Called");
44+
js_print("Javascript", "python_property", python_property);
45+
html_to_data_uri("test", js_callback_1);
46+
external.test_multiple_callbacks(js_callback_2);
47+
};
48+
</script>
49+
</head>
50+
<body>
51+
<h1>Tutorial example</h1>
52+
<div id="console"></div>
53+
</body>
54+
</html>
1355
"""
1456

1557

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

3677

37-
def html_to_data_uri(html):
78+
def html_to_data_uri(html, js_callback=None):
79+
# This function is called in two ways:
80+
# 1. From Python: in this case value is returned
81+
# 2. From Javascript: in this case value cannot be returned because
82+
# inter-process messaging is asynchronous, so must return value
83+
# by calling js_callback.
3884
html = html.encode("utf-8", "replace")
3985
b64 = base64.b64encode(html).decode("utf-8", "replace")
40-
return "data:text/html;base64,{data}".format(data=b64)
86+
ret = "data:text/html;base64,{data}".format(data=b64)
87+
if js_callback:
88+
js_print(js_callback.GetFrame().GetBrowser(),
89+
"Python", "html_to_data_uri",
90+
"Called from Javascript. Will call Javascript callback now.")
91+
js_callback.Call(ret)
92+
else:
93+
return ret
4194

4295

4396
def set_global_handler():
@@ -59,45 +112,86 @@ def set_javascript_bindings(browser):
59112
external = External(browser)
60113
bindings = cef.JavascriptBindings(
61114
bindToFrames=False, bindToPopups=False)
115+
bindings.SetProperty("python_property", "This property was set in Python")
62116
bindings.SetFunction("html_to_data_uri", html_to_data_uri)
63-
bindings.SetProperty("test_property", "This property was set in Python")
64117
bindings.SetObject("external", external)
65118
browser.SetJavascriptBindings(bindings)
66119

67120

68-
def js_print(browser, msg):
69-
browser.ExecuteFunction("js_print", msg)
121+
def js_print(browser, lang, event, msg):
122+
# Execute Javascript function "js_print"
123+
browser.ExecuteFunction("js_print", lang, event, msg)
70124

71125

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

78143

79144
class LoadHandler(object):
80145
def OnLoadingStateChange(self, browser, is_loading, **_):
146+
# Issue #344 will fix this in next release, so that client
147+
# handlers are not called for Developer Tools windows.
148+
if browser.GetUrl().startswith("chrome-devtools://"):
149+
return
150+
# This callback is called twice, once when loading starts
151+
# (is_loading=True) and second time when loading ends
152+
# (is_loading=False).
81153
if not is_loading:
82-
# Loading is complete
83-
js_print(browser, "Python: LoadHandler.OnLoadingStateChange:"
84-
"loading is complete")
154+
# Loading is complete. DOM is ready.
155+
js_print(browser, "Python", "OnLoadingStateChange",
156+
"Loading is complete")
85157

86158

87159
class DisplayHandler(object):
88160
def OnConsoleMessage(self, browser, message, **_):
161+
# Issue #344 will fix this in next release, so that client
162+
# handlers are not called for Developer Tools windows.
163+
if browser.GetUrl().startswith("chrome-devtools://"):
164+
return
165+
# This will intercept js errors, see comments in OnAfterCreated
89166
if "error" in message.lower() or "uncaught" in message.lower():
90-
js_print(browser, "Python: LoadHandler.OnConsoleMessage: "
91-
"intercepted Javascript error: {error}"
92-
.format(error=message))
167+
# Prevent infinite recurrence in case something went wrong
168+
if "js_print is not defined" in message.lower():
169+
if hasattr(self, "js_print_is_not_defined"):
170+
print("Python: OnConsoleMessage: "
171+
"Intercepted Javascript error: "+message)
172+
return
173+
else:
174+
self.js_print_is_not_defined = True
175+
# Delay print by 0.5 sec, because js_print may not be
176+
# available yet due to DOM not ready.
177+
args = [browser, "Python", "OnConsoleMessage",
178+
"(Delayed) Intercepted Javascript error: <i>{error}</i>"
179+
.format(error=message)]
180+
threading.Timer(0.5, js_print, args).start()
93181

94182

95183
class External(object):
96184
def __init__(self, browser):
97185
self.browser = browser
98186

99-
def test_function(self):
100-
pass
187+
def test_multiple_callbacks(self, js_callback):
188+
"""Test both javascript and python callbacks."""
189+
js_print(self.browser, "Python", "test_multiple_callbacks",
190+
"Called from Javascript. Will call Javascript callback now.")
191+
192+
def py_callback(msg_from_js):
193+
js_print(self.browser, "Python", "py_callback", msg_from_js)
194+
js_callback.Call("String sent from Python", py_callback)
101195

102196

103197
if __name__ == '__main__':

src/browser.pyx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,8 @@ cdef PyBrowser GetPyBrowser(CefRefPtr[CefBrowser] cefBrowser,
101101
# - Popups inherit javascript bindings only when "bindToPopups"
102102
# constructor param was set to True.
103103

104-
if pyBrowser.IsPopup() and \
105-
not pyBrowser.GetUserData("__outerWindowHandle"):
104+
if pyBrowser.IsPopup()\
105+
and not pyBrowser.GetUserData("__outerWindowHandle"):
106106
openerHandle = pyBrowser.GetOpenerWindowHandle()
107107
for identifier, tempPyBrowser in g_pyBrowsers.items():
108108
if tempPyBrowser.GetWindowHandle() == openerHandle:
@@ -457,12 +457,10 @@ cdef class PyBrowser:
457457
window_info.SetAsPopup(
458458
<CefWindowHandle>self.GetOpenerWindowHandle(),
459459
PyToCefStringValue("DevTools"))
460-
cdef CefRefPtr[ClientHandler] client_handler =\
461-
<CefRefPtr[ClientHandler]?>new ClientHandler()
462460
cdef CefBrowserSettings settings
463461
cdef CefPoint inspect_element_at
464462
self.GetCefBrowserHost().get().ShowDevTools(
465-
window_info, <CefRefPtr[CefClient]?>client_handler, settings,
463+
window_info, <CefRefPtr[CefClient]?>NULL, settings,
466464
inspect_element_at)
467465

468466
cpdef py_void StopLoad(self):

src/helpers.pyx

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,38 @@ import time
1212
import codecs
1313

1414

15+
def ExceptHook(exc_type, exc_value, exc_trace):
16+
"""Global except hook to exit app cleanly on error.
17+
This hook does the following: in case of exception write it to
18+
the "error.log" file, display it to the console, shutdown CEF
19+
and exit application immediately by ignoring "finally" (_exit()).
20+
"""
21+
print("[CEF Python] ExceptHook: catched exception, will shutdown CEF")
22+
QuitMessageLoop()
23+
Shutdown()
24+
msg = "".join(traceback.format_exception(exc_type, exc_value,
25+
exc_trace))
26+
error_file = GetAppPath("error.log")
27+
encoding = GetAppSetting("string_encoding") or "utf-8"
28+
if type(msg) == bytes:
29+
msg = msg.decode(encoding=encoding, errors="replace")
30+
try:
31+
with codecs.open(error_file, mode="a", encoding=encoding) as fp:
32+
fp.write("\n[%s] %s\n" % (
33+
time.strftime("%Y-%m-%d %H:%M:%S"), msg))
34+
except:
35+
print("[CEF Python] WARNING: failed writing to error file: %s" % (
36+
error_file))
37+
# Convert error message to ascii before printing, otherwise
38+
# you may get error like this:
39+
# | UnicodeEncodeError: 'charmap' codec can't encode characters
40+
msg = msg.encode("ascii", errors="replace")
41+
msg = msg.decode("ascii", errors="replace")
42+
print("\n"+msg)
43+
# noinspection PyProtectedMember
44+
os._exit(1)
45+
46+
1547
cpdef str GetModuleDirectory():
1648
"""Get path to the cefpython module (so/pyd)."""
1749
if platform.system() == "Linux" and os.getenv("CEFPYTHON3_PATH"):
@@ -58,35 +90,3 @@ cpdef str GetAppPath(file_=None):
5890
path = re.sub(r"[/\\]+$", "", path)
5991
return path
6092
return str(file_)
61-
62-
63-
def ExceptHook(exc_type, exc_value, exc_trace):
64-
"""Global except hook to exit app cleanly on error.
65-
This hook does the following: in case of exception write it to
66-
the "error.log" file, display it to the console, shutdown CEF
67-
and exit application immediately by ignoring "finally" (_exit()).
68-
"""
69-
print("[CEF Python] ExceptHook: catched exception, will shutdown CEF")
70-
QuitMessageLoop()
71-
Shutdown()
72-
msg = "".join(traceback.format_exception(exc_type, exc_value,
73-
exc_trace))
74-
error_file = GetAppPath("error.log")
75-
encoding = GetAppSetting("string_encoding") or "utf-8"
76-
if type(msg) == bytes:
77-
msg = msg.decode(encoding=encoding, errors="replace")
78-
try:
79-
with codecs.open(error_file, mode="a", encoding=encoding) as fp:
80-
fp.write("\n[%s] %s\n" % (
81-
time.strftime("%Y-%m-%d %H:%M:%S"), msg))
82-
except:
83-
print("[CEF Python] WARNING: failed writing to error file: %s" % (
84-
error_file))
85-
# Convert error message to ascii before printing, otherwise
86-
# you may get error like this:
87-
# | UnicodeEncodeError: 'charmap' codec can't encode characters
88-
msg = msg.encode("ascii", errors="replace")
89-
msg = msg.decode("ascii", errors="replace")
90-
print("\n"+msg)
91-
# noinspection PyProtectedMember
92-
os._exit(1)

0 commit comments

Comments
 (0)