From 850ac552d58b02038d80e4ca921105c823414ae0 Mon Sep 17 00:00:00 2001 From: cztomczak Date: Thu, 6 Apr 2017 22:16:06 +0200 Subject: [PATCH] Tutorial is almost complete (#256) Detect invalid arguments passed to cef.Initialize and cef.CreateBrowserSync. --- .gitignore | 1 + README.md | 16 +- api/API-categories.md | 2 +- api/ApplicationSettings.md | 6 +- api/JavascriptBindings.md | 6 - api/cefpython.md | 47 ++-- docs/Tutorial.md | 483 ++++++++++++++++++++++++++++-------- examples/Examples-README.md | 5 + examples/hello_world.py | 4 +- examples/tutorial.py | 104 ++++++++ src/cefpython.pyx | 9 + src/helpers.pyx | 55 ++-- 12 files changed, 574 insertions(+), 164 deletions(-) create mode 100644 examples/tutorial.py diff --git a/.gitignore b/.gitignore index 43cf7237..74815a1c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ build/ __pycache__/ *.pyc cefclient +webcache/ diff --git a/README.md b/README.md index 2dda8934..29ab8479 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,11 @@ also download packages for offline installation available on the pip install cefpython3==56.1 ``` +## Tutorial + +See the [Tutorial.md](docs/Tutorial.md) file. + + ## Examples See the [Examples-README.md](examples/Examples-README.md) file. @@ -53,8 +58,15 @@ See the [Examples-README.md](examples/Examples-README.md) file. ## Support - Ask questions, report problems and issues on the [Forum](https://groups.google.com/group/cefpython) -- Documentation is in the [docs/](docs) directory -- API reference is in the [api/](api) directory +- Supported examples are listed in the [Examples-README.md](examples/Examples-README.md) file +- Documentation is in the [docs/](docs) directory: + - [Build instructions](docs/Build-instructions.md) + - [Knowledge base](docs/Knowledge-Base.md) + - [Migration guide](docs/Migration-guide.md) + - [Tutorial](docs/Tutorial.md) +- API reference is in the [api/](api) directory: + - [API categories](api/API-categories.md#api-categories) + - [API index](api/API-index.md#api-index) - Additional documentation is in issues labelled [Knowledge Base](../../issues?q=is%3Aissue+is%3Aopen+label%3A%22Knowledge+Base%22) - To search documentation use GitHub "This repository" search at the top. To narrow results to documentation only select diff --git a/api/API-categories.md b/api/API-categories.md index cceb66fe..1f4fc992 100644 --- a/api/API-categories.md +++ b/api/API-categories.md @@ -36,7 +36,7 @@ * [WindowUtils](WindowUtils.md#windowutils-class) class -### Handlers (interfaces) +### Client handlers (interfaces) * [DisplayHandler](DisplayHandler.md#displayhandler-interface) * [DownloadHandler](DownloadHandler.md#downloadhandler) diff --git a/api/ApplicationSettings.md b/api/ApplicationSettings.md index f651941b..390b3905 100644 --- a/api/ApplicationSettings.md +++ b/api/ApplicationSettings.md @@ -447,9 +447,9 @@ default User-Agent string will be used. Also configurable using the (string) The location where user data such as spell checking dictionary files will be stored on disk. If empty then the default platform-specific user data -directory will be used ("~/.cef_user_data" directory on Linux, -"~/Library/Application Support/CEF/User Data" directory on Mac OS X, -"Local Settings\Application Data\CEF\User Data" directory under the user +directory will be used (`"~/.cef_user_data"` directory on Linux, +`"~/Library/Application Support/CEF/User Data"` directory on Mac OS X, +`"Local Settings\Application Data\CEF\User Data"` directory under the user profile directory on Windows). diff --git a/api/JavascriptBindings.md b/api/JavascriptBindings.md index b00aa952..7083cd88 100644 --- a/api/JavascriptBindings.md +++ b/api/JavascriptBindings.md @@ -29,12 +29,6 @@ In CEF 3 communication between javascript and python can only be asynchronous. I There are plans to support binding data by reference (a list, dict or object's properties). This would be possible with the use of CefRegisterExtension(). -## Example usage - -See the [wxpython.py](../src/windows/binaries_32bit/wxpython.py) example for an example usage of javascript bindings, javascript callbacks and python callbacks. - - - ## Methods diff --git a/api/cefpython.md b/api/cefpython.md index 3af53001..3409d8b4 100644 --- a/api/cefpython.md +++ b/api/cefpython.md @@ -55,12 +55,14 @@ All parameters are optional. This function can only be called on the UI thread. -The "window_title" parameter will be used only -when parent window provided in window_info was set to 0. +The "window_title" parameter will be used only when parent +window provided in window_info was set to 0. This is for use +with hello_world.py and tutorial.py examples which don't use +any third party GUI framework for creation of top-level window. After the call to CreateBrowserSync() the page is not yet loaded, -if you want your next lines of code to do some stuff on the webpage -you will have to implement LoadHandler.[OnLoadEnd]((LoadHandler.md#onloadend)) +if you want your next lines of code to do some stuff on the +webpage you will have to implement LoadHandler.[OnLoadingStateChange]((LoadHandler.md#onloadingstatechange)) callback. @@ -68,10 +70,10 @@ callback. | Parameter | Type | | --- | --- | -| excType | - | -| excValue | - | -| traceObject | - | -| __Return__ | string | +| exc_type | - | +| exc_value | - | +| exc_trace | - | +| __Return__ | void | Global except hook to exit app cleanly on error. CEF has a multiprocess architecture and when exiting you need to close all processes (main Browser @@ -99,6 +101,7 @@ to Initialize(). Returns None if key is not found. | | | | --- | --- | +| file_ (optional) | string | | __Return__ | string | Get path to where application resides. @@ -181,18 +184,7 @@ Returns true if called on the specified thread. CEF maintains multiple internal threads that are used for handling different types of tasks. The UI thread creates the browser window and is used for all interaction with the webkit rendering engine and V8 Javascript engine. The UI thread will be the same as the main application thread if CefInitialize() is called with an [ApplicationSettings](ApplicationSettings.md) 'multi_threaded_message_loop' option set to false. The IO thread is used for handling schema and network requests. The FILE thread is used for the application cache and other miscellaneous activities. -List of threads in the Browser process. These are constants defined in the cefpython module: - -* TID_UI: The main thread in the browser. This will be the same as the main application thread if cefpython.Initialize() is called with a ApplicationSettings.multi_threaded_message_loop value of false. -* TID_DB: Used to interact with the database. -* TID_FILE: Used to interact with the file system. -* TID_FILE_USER_BLOCKING: Used for file system operations that block user interactions. Responsiveness of this thread affects users. -* TID_PROCESS_LAUNCHER: Used to launch and terminate browser processes. -* TID_CACHE: Used to handle slow HTTP cache operations. -* TID_IO: Used to process IPC and network messages. - -List of threads in the Renderer process: -* TID_RENDERER: The main thread in the renderer. Used for all webkit and V8 interaction. +See PostTask() for a list of threads. ### MessageLoop @@ -244,10 +236,23 @@ Description from upstream CEF: | ... | *args | | __Return__ | void | -Post a task for execution on the thread associated with this task runner. Execution will occur asynchronously. Only Browser process threads are allowed, see IsThread() for a list of available threads and their descriptions. +Post a task for execution on the thread associated with this task runner. Execution will occur asynchronously. Only Browser process threads are allowed. An example usage is in the wxpython.py example on Windows, in implementation of LifespanHandler.OnBeforePopup(). +List of threads in the Browser process: +* cef.TID_UI: The main thread in the browser. This will be the same as the main application thread if cefpython.Initialize() is called with a ApplicationSettings.multi_threaded_message_loop value of false. +* cef.TID_DB: Used to interact with the database. +* cef.TID_FILE: Used to interact with the file system. +* cef.TID_FILE_USER_BLOCKING: Used for file system operations that block user interactions. Responsiveness of this thread affects users. +* cef.TID_PROCESS_LAUNCHER: Used to launch and terminate browser processes. +* cef.TID_CACHE: Used to handle slow HTTP cache operations. +* cef.TID_IO: Used to process IPC and network messages. + +List of threads in the Renderer process: +* cef.TID_RENDERER: The main thread in the renderer. Used for all webkit and V8 interaction. + + ### QuitMessageLoop diff --git a/docs/Tutorial.md b/docs/Tutorial.md index e659b263..adc50a1a 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -1,189 +1,472 @@ -# Tutorial (STILL A WORK IN PROGRESS.. #256) +# Tutorial -This tutorial is for v50+ versions of CEF Python, which are currently -available only for Linux. +With CEF Python you can embed a web browser control based +on Chromium in a Python application. You can also use it to +create a HTML 5 based GUI in an application that can act as +a replacement for standard GUI toolkits such as wxWidgets, +Qt or GTK. With this tutorial you will learn CEF Python +basics. This tutorial will discuss the two basic examples: +[hello_world.py](../examples/hello_world.py) +and [tutorial.py](../examples/tutorial.py). There are more +many more examples that you can find in the [Examples-README.md](../examples/Examples-README.md) +file, but these examples are out of scope for this tutorial. Table of contents: -* [Install and download examples](#install-and-download-examples) +* [Install and run example](#install-and-run-example) * [Hello world](#hello-world) -* [CEF's multiprocess architecture](#cefs-multiprocess-architecture) +* [Architecture](#architecture) * [Handling Python exceptions](#handling-python-exceptions) * [Message loop](#message-loop) * [Settings](#settings) -* [Handlers](#handlers) +* [Client handlers](#client-handlers) * [Javascript integration](#javascript-integration) -* [Plugins](#plugins) -* [Helper functions](#helper-functions) +* [Javascript exceptions and Python exceptions](#javascript-exceptions-and-python-exceptions) +* [Plugins and Flash support](#plugins-and-flash-support) * [Build executable](#build-executable) -* [What's next?](#whats-next) +* [Support and documentation](#support-and-documentation) -## Install and download examples +## Install and run example -The easy way to install CEF Python is through PyPI, using the pip tool, -which is bundled with all recent versions of Python. On Linux pip 8.1+ -is required. To check version and install CEF Python type: +You can install with pip. On Linux pip 8.1+ is required. Alternatively +you can download packages for offline installation from [GitHub Releases](../../../releases). -``` -pip --version -pip install cefpython3 -``` - -Alternatively you can download the setup package from -[GitHub Releases](../../../releases) and install it by following -the instructions in README.txt. - -Now let's download examples by cloning the GitHub repository. After -that, enter the "cefpython/examples/" directory. In that directory -you will find all the examples from this Tutorial, their names -start with a "tutorial_" prefix, except for the hello world example -which is just named "hello_world.py". +Run the commands below to install the cefpython3 package, clone +the github repository, enter the examples/ directory and run the +Hello World example: ``` +pip install cefpython3==56.1 git clone https://github.com/cztomczak/cefpython.git cd cefpython/examples/ +python hello_world.py ``` +The hello_world.py example's source code will be analyzed line +by line in the next section of this Tutorial. + +This tutorial in its further sections will also reference the +tutorial.py example which will show how to use more advanced +CEF Python features. The tutorial.py example is also available +in the examples/ directory. + ## Hello world The [hello_world.py](../examples/hello_world.py) example is the -most basic example. It doesn't depend on any third party GUI frameworks. +most basic example. It doesn't depend on any third party GUI framework. It creates a browser widget without providing any window information -(parent window not specified), thus CEF automatically takes care of creating -a top-level window for us, and in that window a Chromium widget is embedded. -When creating the browser, an "url" parameter is specified, which causes the -browser to initially navigate to the Google website. Let's explain the code -from this example: +(parent window is not specified), thus CEF automatically takes care of +creating a top-level window, and in that window a Chromium widget +is being embedded. When creating the browser, the "url" parameter is +specified, which causes the browser to initially navigate to +Google website. Let's analyze the code from that example: 1. `from cefpython3 import cefpython as cef` - Import the cefpython - module and bind it to a shorter name "cef". + module and make a short "cef" alias 2. `sys.excepthook = cef.ExceptHook` - Overwrite Python's default - exception handler so that all CEF processes are terminated when - Python exception occurs. To understand this better read the - "CEF's multiprocess architecture" and "Handling Python exceptions" - sections further down in this Tutorial. + exception handler so that all CEF sub-processes are terminated + when Python exception occurs. To understand this better read the + "Architecture" and "Handling Python exceptions" sections + further down in this Tutorial. 3. `cef.Initialize()` - Initialize CEF. This function must be called somewhere in the beginning of your code. It must be called before - any app window is created. It must be called only once during app's - lifetime and must have a corresponding Shutdown() call. -4. `cef.CreateBrowserSync(url="https://www.google.com/")` - Create - a browser synchronously, this function returns the Browser object. + any application window is created. It must be called only once + during app's lifetime and must have a corresponding Shutdown() + call. +4. `cef.CreateBrowserSync(url="https://www.google.com/", ...)` - Create + a browser synchronously, this function returns a Browser object. 5. `cef.MessageLoop()` - Run CEF message loop. All desktop GUI programs run some message loop that waits and dispatches events or messages. + Read more on message loop in the "Message loop" section further + down in this tutorial. 6. `cef.Shutdown()` - Shut down CEF. This function must be called for - CEF to shut down cleanly. It will free CEF system resources, it - will terminate all subprocesses, and it will flush to disk any + CEF to shut down cleanly. It will free system resources acquired + by CEF and terminate all sub-processes, and it will flush to disk any yet unsaved data like for example cookies and/or local storage. Call this function at the very end of your program execution. When using third party GUI frameworks such as Qt/wxWidgets, CEF should be shut down after these frameworks' shutdown procedures were called. For example in Qt, shut down CEF only after QApplication object was destroyed. -Documentation for the functions from this example can be found in -API docs (the api/ directory in GitHub's repository): +Documentation for the functions referenced in this example can +be found in API reference - the [api/](../api) root directory in GitHub's +repository: * [ExceptHook](../api/cefpython.md#excepthook) -* [Initialize()](../api/cefpython.md#initialize) -* [CreateBrowserSync()](../api/cefpython.md#createbrowsersync) +* [Initialize](../api/cefpython.md#initialize) +* [CreateBrowserSync](../api/cefpython.md#createbrowsersync) * [Browser](../api/Browser.md) object -* [MessageLoop()](../api/cefpython.md#messageloop) -* [Shutdown()](../api/cefpython.md#shutdown) +* [MessageLoop](../api/cefpython.md#messageloop) +* [Shutdown](../api/cefpython.md#shutdown) + + +## Architecture + +- CEF uses multi-process architecture + - The main application process is called the “Browser” + process. In CEF Python this is the same process in + which Python is running. + - CEF Python uses a separate executable called "subprocess" + for running sub-processes. Sub-processes will be created + for renderers, plugins, GPU, etc. + - In future CEF Python might allow to run Python also in + sub-processes, for example in the Renderer process which + would allow to access more CEF API ([Issue #320](../../../issues/320)) +- Most processes in CEF have multiple threads + - Handlers' callbacks and other interfaces callbacks may + be called on various threads, this is stated in API reference + - Some functions may only be used on particular threads, + this is stated in API reference + - CEF Python provides cef.[PostTask](../api/cefpython.md#posttask) + function for posting tasks between these various threads + - The "UI" thread is application main thread unless you + use ApplicationSettings.[multi_threaded_message_loop](../api/ApplicationSettings.md#multi_threaded_messge_loop) + option on Windows in which case the UI thread will no more + be application main thread + - Do not perform blocking operations on any CEF thread other + than the Browser process FILE thread. Otherwise this can + lead to serious performance issues. -## CEF's multiprocess architecture +## Handling Python exceptions -... +Due to CEF multi-process architecture Python exceptions need +special handling. When Python exception occurs then main process +is terminated. For CEF this means that the Browser process is +terminated, however there may still be running CEF sub-processes +like Renderer process, GPU process, etc. To terminate these +sub-processes cef.[Shutdown](../api/cefpython.md#shutdown) +must be called and if running CEF message loop then it must be +stopped first. In all CEF Python examples you can find such +a line that overwrites the default exception handler in Python: +``` +sys.excepthook = cef.ExceptHook # To shutdown all CEF processes on error +``` -## Handling Python exceptions -... +See Python docs for [sys.excepthook](https://docs.python.org/2/library/sys.html#sys.excepthook). + +The cef.ExceptHook helper function does the following: +1. Calls cef.[QuitMessageLoop](../api/cefpython.md#quitmessageloop) +2. Calls cef.[Shutdown](../api/cefpython.md#shutdown) +3. Writes exception to "error.log" file +4. Prints exception +5. Calls [os._exit(1)](https://docs.python.org/2/library/os.html#os._exit) - + 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=). ## Message loop Message loop is a programming construct that waits for and dispatches events or messages in a program. All desktop GUI -programs must run some kind of message loop. -The hello_world.py example doesn't depend on any third party GUI -framework and thus can run CEF message loop directly by calling call -cef.MessageLoop(). However in most of other examples that show how -to embed CEF Python browser inside GUI frameworks such as -Qt/wxPython/Tkinter, you can't call cef.MessageLoop(), because these -frameworks run a message loop of its own. For such cases CEF provides -cef.MessageLoopWork() which is for integrating CEF message loop into -existing application message loop. Usually cef.MessageLoopWork() is -called in a 10ms timer. +programs must run some kind of message loop. The hello_world.py +example doesn't depend on any third party GUI framework and thus +can run CEF message loop directly by calling cef.MessageLoop(). +However in other examples that embed CEF browser with GUI frameworks +such as Qt/wxPython/Tkinter you can't call cef.MessageLoop(), because +these frameworks run a message loop of its own. For such cases CEF +provides cef.MessageLoopWork() which is for integrating CEF message +loop into existing application message loop. Usually +cef.MessageLoopWork() is called in a 10ms timer. + +**Performance** Calling cef.MessageLoopWork() in a timer is not the best performant way to run CEF message loop, also there are known bugs on some -platforms when calling message loop work in a timer. CEF provides -ApplicationSettings.[external_message_pump](../api/ApplicationSettings.md#external_message_pump) -option for running an external message pump that you should use for -best performance and to get rid of some bugs. However this option is -still experimental, as during testing on Linux it actually made app -x2 slower - it's a bug in upstream CEF that was reported. See -[Issue #246](../../../issues/246) for more details. On Windows/Mac -external message pump should work good, but it wasn't yet tested -with CEF Python. +platforms when calling message loop work in a timer. There are two +options to increase performance depending on platform. On Windows +use a multi-threaded message loop for best performance. On Mac use +an external message pump for best performance. + +**Windows: multi-threaded message loop** On Windows for best performance a multi-threaded message loop should -be used, instead of cef.MessageLoopWork / external message pump. To do -so, set -ApplicationSettings.[multi_threaded_message_loop](../ApplicationSettings.md#multi_threaded_message_loop) +be used instead of cef.MessageLoopWork() or external message pump. To do +so, set ApplicationSettings.[multi_threaded_message_loop](../ApplicationSettings.md#multi_threaded_message_loop) to True and run a native message loop in your app. Don't call CEF's -message loop. Create browser using -cef.PostTask(cef.TID_UI, cef.CreateBrowserSync, ...). +message loop. Create browser using `cef.PostTask(cef.TID_UI, cef.CreateBrowserSync, ...)`. Note that when using multi-threaded message loop, CEF's UI thread is no more application's main thread, and that makes it a bit harder to correctly use CEF API. API docs explain on which threads a function may be called and in case of handlers' callbacks (and other interfaces) -it is stated on which thread a callback will be called. -See also [Issue #133](../../../issues/133). +it is stated on which thread a callback will be called. See also +[Issue #133](../../../issues/133). + +**Mac: external message pump** + +CEF provides ApplicationSettings.[external_message_pump](../api/ApplicationSettings.md#external_message_pump) +option for running an external message pump that you should use for +best performance and to get rid of some bugs that appear when using +cef.MessageLoopWork() in a timer. + +This option is currently marked experimental as it wasn't yet fully +tested. This option should work good on Mac - in upstream CEF it was +tested mainly on Mac. If you've successfully used this option on Mac +please let us know on the Forum. + +**Linux** + +External message pump option is not recommended to use on Linux, +as during testing it actually made app x2 slower - it's a bug in +upstream CEF. See [Issue #246](../../../issues/246) for more details. ## Settings -ApplicationSettings, BrowserSettings, CommandLineSwitches, Chromium -Preferences (not implemented yet) ... +CEF settings are provided in multiple ways. There are global +[application settings](../api/ApplicationSettings.md#application-settings) +and [command line switches](../api/CommandLineSwitches.md#command-line-switches) +that can be passed to cef.[Initialize](../api/cefpython.md#initialize). +There are also [browser settings](../api/BrowserSettings.md#browser-settings) +that can be passed to cef.[CreateBrowserSync](../api/cefpython.md#createbrowsersync). +Finally there are Chromium preferences, but these are not yet +implemented. See below for details on each of these settings. + +**Application settings** + +A dict of [application settings](../api/ApplicationSettings.md#application-settings) +can be passed to cef.[Initialize](../api/cefpython.md#initialize). +Here are some settings worth noting: +- [cache_path](../api/ApplicationSettings.md#cache_path) - set + a directory path so that web cache data is persisted, otherwise + an in-memory cache is used. Cookies and HTML 5 databases such + as local storage will only persist if this option is set. +- [context_menu](../api/ApplicationSettings.md#context_menu) - + customize context menu +- [locale](../api/ApplicationSettings.md#locale) - set language + for localized resources + +To enable debugging set these settings: +``` +settings = { + "debug": True, + "log_severity": cef.LOGSEVERITY_WARNING, + "log_file": "debug.log", +} +cef.Initialize(settings=settings) +``` + +Alternatively you can pass `--debug` flag on the command line +and these settings will be set automatically. + +**Browser settings** + +A dict of [browser settings](../api/BrowserSettings.md#browser-settings) +can be passed to cef.[CreateBrowserSync](../api/cefpython.md#createbrowsersync). + +**Command line switches** +A dict of [command line switches](../api/CommandLineSwitches.md) +can be passed to cef.[Initialize](../api/cefpython.md#initialize). +Examples switches: +- "enable-media-stream" - to enable media (WebRTC audio/video) streaming +- "proxy-server" - to set proxy server +- "disable-gpu" - use only CPU software rendering + +Example code: + +``` +switches = { + "enable-media-stream": "", + "proxy-server": "socks5://127.0.0.1:8888", + "disable-gpu": "", +} +cef.Initialize(switches=switches) +``` -## Handlers +Note that when setting switch that doesn't accept value then +must pass an empty string as value. + +**Chromium preferences** + +There are lots of more settings that can be set using Chromium +preferences (and even changed during runtime), however this API +wasn't yet exposed to CEF Python, see [Issue #244](../../../issues/244) +for details. + + +## Client handlers + +In CEF [client handlers](../api/API-categories.md#client-handlers-interfaces) +provide a way to be notified of Chromium events. There are client +handlers like DisplayHandler, LoadHandler, RequestHandler, etc. +These handlers are class interfaces for which you provide +implementation. We will refer to the methods of these objects +as "callbacks". You can set a client handler by calling +Browser.[SetClientHandler](../api/Browser.md#setclienthandler). +You are not required to implement whole interface, you can implement +only some callbacks. Some handlers due to cefpython limitations +have callbacks that can only be set globally by calling +cef.[SetGlobalClientCallback](../api/cefpython.md#setglobalclientcallback). +In API reference such global client callbacks are marked with an +underscore in its name. + +The [tutorial.py](../examples/tutorial.py) example shows how to +implement client handlers like [DisplayHandler](../api/DisplayHandler.md) +and [LoadHandler](../api/LoadHandler.md). It also shows how to +implement a global client callback LifespanHandler.[_OnAfterCreated](../api/LifespanHandler.md#_onaftercreated). Here is some source code: + +``` +set_client_handlers(browser) ... +def set_client_handlers(browser): + client_handlers = [LoadHandler(), DisplayHandler()] + for handler in client_handlers: + browser.SetClientHandler(handler) +... +class LoadHandler(object): + def OnLoadingStateChange(self, browser, is_loading, **_): + if not is_loading: + # Loading is complete + js_print(browser, "Python: LoadHandler.OnLoadingStateChange:" + "loading is complete") +``` ## Javascript integration +Python code is running in the main process (the Browser process), +while Javascript is running in the Renderer sub-process. Communication +between Python and Javascript is possible either using inter-process +asynchronous messaging or through http requests (both sync and +async possible). + +**Asynchronous inter-process messaging** + +Python and Javascript can communicate using inter-process +messaging: + - Use the [JavascriptBindings](../api/JavascriptBindings.md) + class methods to to expose Python functions, objects and properties + to Javascript: [SetFunction](../api/JavascriptBindings.md#setfunctions), + [SetObject](../api/JavascriptBindings.md#setobject) + and [SetProperty](../api/JavascriptBindings.md#setproperty) + - To initiate communication from the Python side call + Browser object methods: [ExecuteJavascript](../api/Browser.md#executejavascript) + or [ExecuteFunction](../api/Browser.md#executefunction). + Frame object also has the same methods. + - To initiate communication from the Javascript side first + you have to bind Python functions/objects using the + JavascriptBindings class mentioned earlier. Then you call + these functions/objects. + - You can pass Javascript callbacks to Python. Just pass a + javascript function as an argument when calling Python + function/object. On the Python side that javascript function + will be converted to [JavascriptCallback](../api/JavascriptCallback.md) + object. Execute the [Call](../api/JavascriptCallback.md#call) + method on it to call the javascript function asynchronously. + - You can pass Python callbacks to Javascript, however you + can do so only after the communication was initiated from + the Javascript side and a javascript callback was passed. + When executing JavascriptCallback.[Call](../api/JavascriptCallback.md#call) + method you can pass Python callbacks to Javascript. In + javascript these Python callbacks will act as native + javascript functions, so call them as usual. + - Note that when executing Browser.[ExecuteFunction](../api/Browser.md#executefunction) method you cannot pass Python functions + nor objects here. Such feature is not yet supported. You can + 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. + +... ... -## Plugins +**Communication using http requests** -... +Python and Javascript can also communicate using http requests +by running an internal web-server. See for example [SimpleHTTPServer](https://docs.python.org/2/library/simplehttpserver.html) +in Python docs. + +With http requests it is possible for synchronous +communication from Javascript to Python by performing +synchronous AJAX requests. + +To initiate communication from the Python side call +Browser object methods: [ExecuteJavascript](../api/Browser.md#executejavascript) +or [ExecuteFunction](../api/Browser.md#executefunction). +Frame object also has the same methods. + +You can also serve requests directly in CEF using for example +[ResourceHandler](../api/ResourceHandler.md) object. You can find +an example usage of this object in one of examples listed in +the [Examples-README.md](../examples/Examples-README.md) file. +On a side note, upstream CEF also supports custom scheme handlers, +however these APIs were not yet exposed to CEF Python. -## Helper functions -GetApplicationPath... -GetModulePath... -others... +## Javascript exceptions and Python exceptions + +There are cases when executing Javascript code may end up with +Python exception being thrown: + +1. When a Python function is invoked from Javascript and it fails, + a Python exception will be thrown +2. When Python executes a Javascript callback and it fails, + a Python exception will be thrown + +In other cases to see Javascript exceptions open Developer Tools +window using mouse context menu and switch to Console tab. + +There are multiple ways to catch/intercept javascript exceptions: + +1. In Javascript you can register "window.onerror" event to + catch all Javascript exceptions +2. In Python you can intercept Javascript exceptions using + DisplayHandler.[OnConsoleMessage](../api/DisplayHandler.md#onconsolemessage) +3. In upstream CEF there is also CefRenderProcessHandler::OnUncaughtException + callback for catching Javascript exceptions, however this + wasn't yet exposed to CEF Python + + +## Plugins and Flash support + +Latest CEF supports only [PPAPI plugins](https://www.chromium.org/developers/design-documents/pepper-plugin-implementation). +NPAPI plugins are no more supported. + +Instructions for enabling Flash support are available in [Issue #235](../../../issues/235) ("Flash support in CEF 51+"). + +For the old CEF Python v31 release instructions for enabling Flash +support are available on Wiki pages. + ## Build executable -Examples for building an executable are yet to be created: +There are no official examples for building executable using +python packaging tools, however some resources and tips are +available, see these issues in the tracker: + +* Pyinstaller - see [Issue #135](../../../issues/135) +* py2exe - see [Issue #35](../../../issues/35) +* py2app - see [Issue #337](../../../issues/337) +* cx_Freeze - see [Issue #338](../../../issues/338) + +If you have any problems building executable ask on the [Forum](https://groups.google.com/group/cefpython). + +**Files in the cefpython3 package** + +The cefpython3 package has the following components: +1. The CEF Python modules (cefpython_pyxx.pyd on Windows, + cefpython_pyxx.so on Linux/Mac) +2. The CEF dynamic library (libcef.dll on Windows, libcef.so on Linux, + “Chromium Embedded Framework.framework” on OS X). +3. Other dynamic libraries CEF depends on (libEGL, libGLES, + d3dcompiler, etc.) and some optional (widevinecdmadapter, etc.) +4. Support files (*.pak, *.dat, *.bin, etc). -* On Windows use py2exe ([#35](../../../issues/35)) - or pyinstaller ([#135](../../../issues/135)) -* On Mac use py2app or pyinstaller -* On Linux use pyinstaller or cx_freeze +See README.txt in the cefpython3 package which provides +extended details about all CEF binary files. -## What's next? +## Support and documentation -See more examples in the examples/ directory -See API docs in the api/ directory -Example usage of most of API is available in the unittests/ directory -See the Knowledge base document -Ask questions and report problems on the Forum +For support and documentation see the [Support](../README.md#support) +section in README. diff --git a/examples/Examples-README.md b/examples/Examples-README.md index 6e76fb4d..3b93066c 100644 --- a/examples/Examples-README.md +++ b/examples/Examples-README.md @@ -39,6 +39,11 @@ maintained: If there are any issues in examples read top comments in sources to see whether this is a known issue with available workarounds. +**Unit tests** + +There are also available unit tests and its usage of the API can +be of some use. See [main_test.py](../unittests/main_test.py). + ## More examples diff --git a/examples/hello_world.py b/examples/hello_world.py index bcacf379..0157267f 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -10,8 +10,8 @@ def main(): check_versions() sys.excepthook = cef.ExceptHook # To shutdown all CEF processes on error cef.Initialize() - cef.CreateBrowserSync(window_title="Hello World!", - url="https://www.google.com/") + cef.CreateBrowserSync(url="https://www.google.com/", + window_title="Hello World!") cef.MessageLoop() cef.Shutdown() diff --git a/examples/tutorial.py b/examples/tutorial.py new file mode 100644 index 00000000..be85b3d4 --- /dev/null +++ b/examples/tutorial.py @@ -0,0 +1,104 @@ +# Tutorial example. +# Tested with CEF Python v56.1+ + +from cefpython3 import cefpython as cef +import base64 +import platform +import sys + +# HTML code. Browser will navigate to a Data uri created +# from this html code. +HTML_code = """ +test +""" + + +def main(): + check_versions() + sys.excepthook = cef.ExceptHook # To shutdown all CEF processes on error + settings = {"cache_path": "webcache"} + cef.Initialize(settings=settings) + set_global_handler() + browser = cef.CreateBrowserSync(url=html_to_data_uri(HTML_code), + window_title="Hello World!") + set_client_handlers(browser) + set_javascript_bindings(browser) + cef.MessageLoop() + cef.Shutdown() + + +def check_versions(): + print("[tutorial.py] CEF Python {ver}".format(ver=cef.__version__)) + print("[tutorial.py] Python {ver} {arch}".format( + ver=platform.python_version(), arch=platform.architecture()[0])) + assert cef.__version__ >= "56.1", "CEF Python v56.1+ required to run this" + + +def html_to_data_uri(html): + html = html.encode("utf-8", "replace") + b64 = base64.b64encode(html).decode("utf-8", "replace") + return "data:text/html;base64,{data}".format(data=b64) + + +def set_global_handler(): + # A global handler is a special handler for callbacks that + # must be set before Browser is created using + # SetGlobalClientCallback() method. + global_handler = GlobalHandler() + cef.SetGlobalClientCallback("OnAfterCreated", + global_handler.OnAfterCreated) + + +def set_client_handlers(browser): + client_handlers = [LoadHandler(), DisplayHandler()] + for handler in client_handlers: + browser.SetClientHandler(handler) + + +def set_javascript_bindings(browser): + external = External(browser) + bindings = cef.JavascriptBindings( + bindToFrames=False, bindToPopups=False) + 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) + + +class GlobalHandler(object): + def OnAfterCreated(self, browser, **_): + js_print(browser, + "Python: GlobalHandler._OnAfterCreated: browser id={id}" + .format(id=browser.GetIdentifier())) + + +class LoadHandler(object): + def OnLoadingStateChange(self, browser, is_loading, **_): + if not is_loading: + # Loading is complete + js_print(browser, "Python: LoadHandler.OnLoadingStateChange:" + "loading is complete") + + +class DisplayHandler(object): + def OnConsoleMessage(self, browser, message, **_): + if "error" in message.lower() or "uncaught" in message.lower(): + js_print(browser, "Python: LoadHandler.OnConsoleMessage: " + "intercepted Javascript error: {error}" + .format(error=message)) + + +class External(object): + def __init__(self, browser): + self.browser = browser + + def test_function(self): + pass + + +if __name__ == '__main__': + main() diff --git a/src/cefpython.pyx b/src/cefpython.pyx index f50919a8..dc7a672e 100644 --- a/src/cefpython.pyx +++ b/src/cefpython.pyx @@ -593,9 +593,13 @@ def Initialize(applicationSettings=None, commandLineSwitches=None, **kwargs): if "settings" in kwargs: assert not applicationSettings, "Bad arguments" application_settings = kwargs["settings"] + del kwargs["settings"] if "switches" in kwargs: assert not command_line_switches, "Bad arguments" command_line_switches = kwargs["switches"] + del kwargs["switches"] + for kwarg in kwargs: + raise Exception("Invalid argument: "+kwarg) IF UNAME_SYSNAME == "Linux": # Fix Issue #231 - Discovery of the "icudtl.dat" file fails on Linux. @@ -772,10 +776,15 @@ def CreateBrowserSync(windowInfo=None, # Alternative names for existing parameters if "window_info" in kwargs: windowInfo = kwargs["window_info"] + del kwargs["window_info"] if "settings" in kwargs: browserSettings = kwargs["settings"] + del kwargs["settings"] if "url" in kwargs: navigateUrl = kwargs["url"] + del kwargs["url"] + for kwarg in kwargs: + raise Exception("Invalid argument: "+kwarg) Debug("CreateBrowserSync() called") assert IsThread(TID_UI), ( diff --git a/src/helpers.pyx b/src/helpers.pyx index 595d2cd0..dbfb22e8 100644 --- a/src/helpers.pyx +++ b/src/helpers.pyx @@ -32,7 +32,7 @@ cpdef str GetModuleDirectory(): g_GetAppPath_dir = None -cpdef str GetAppPath(file=None): +cpdef str GetAppPath(file_=None): """Get application path.""" # On Windows after downloading file and calling Browser.GoForward(), # current working directory is set to %UserProfile%. @@ -47,49 +47,46 @@ cpdef str GetAppPath(file=None): global g_GetAppPath_dir g_GetAppPath_dir = adir # If file is None return current directory without trailing slash. - if file is None: - file = "" + if file_ is None: + file_ = "" # Only when relative path. - if not file.startswith("/") and not file.startswith("\\") and ( - not re.search(r"^[\w-]+:", file)): - path = g_GetAppPath_dir + os.sep + file + if not file_.startswith("/") and not file_.startswith("\\") and ( + not re.search(r"^[\w-]+:", file_)): + path = g_GetAppPath_dir + os.sep + file_ if platform.system() == "Windows": path = re.sub(r"[/\\]+", re.escape(os.sep), path) path = re.sub(r"[/\\]+$", "", path) return path - return str(file) + return str(file_) -cpdef py_void ExceptHook(excType, excValue, traceObject): - """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 now") +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() - print("[CEF Python] ExceptHook: see the catched exception below:") - errorMsg = "".join(traceback.format_exception(excType, excValue, - traceObject)) - errorFile = GetAppPath("error.log") + 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: - appEncoding = g_applicationSettings["string_encoding"] - except: - appEncoding = "utf-8" - if type(errorMsg) == bytes: - errorMsg = errorMsg.decode(encoding=appEncoding, errors="replace") - try: - with codecs.open(errorFile, mode="a", encoding=appEncoding) as fp: + 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"), errorMsg)) + time.strftime("%Y-%m-%d %H:%M:%S"), msg)) except: print("[CEF Python] WARNING: failed writing to error file: %s" % ( - errorFile)) + error_file)) # Convert error message to ascii before printing, otherwise # you may get error like this: # | UnicodeEncodeError: 'charmap' codec can't encode characters - errorMsg = errorMsg.encode("ascii", errors="replace") - errorMsg = errorMsg.decode("ascii", errors="replace") - print("\n"+errorMsg) + msg = msg.encode("ascii", errors="replace") + msg = msg.decode("ascii", errors="replace") + print("\n"+msg) # noinspection PyProtectedMember os._exit(1)