diff --git a/README.md b/README.md
index 1c922e6..1c72eb1 100644
--- a/README.md
+++ b/README.md
@@ -173,28 +173,78 @@ Below this link, you see
```
**PYTHON.stop()** is only required, when running Neutralino in cloud-mode. This will unload the Python extension gracefully.
+### Long-running tasks and their progress
+
+For details how to start a long-running background task in Python and how to poll its progress,
+see the comments in `extensions/python/main.py`and `resources/js/main.js`.
+
+before a new task is spawned, Python sends a **startPolling-message** to the frontend.
+As a result, the frontend sends a **poll-message** every 500 ms.
+
+All progress-messages of the long-running task are stored in a queue.
+Before the task ends, it pushes a **stopPolling-message** to the queue:
+
+```mermaid
+graph LR;
+ id[stopPolling]-->id2[Progress 3/3];
+ id2[Progress 3/3]-->id3[Progress 2/3];
+ id3[Progress 2/3]-->id4[Progress 1/3];
+```
+
+Each incoming **poll-message** forces Rust to stop listening on the WebSocket and processing
+the queue instead. When the **stopPolling-message** is sent back, the frontend stops polling.
+
## Classes overview
### NeutralinoExtension.py
-| Method | Description |
-|----------------------------------|---------------------------------------------------------------------------------------------------------------------------------|
-| NeutralinoExtension(debug=false) | Extension class. debug: Print data flow to the terminal. |
-| debugLog(msg, tag="info") | Write a message to the terminal. msg: Message, tag: The message type, "in" for incoming, "out" for outgoing, "info" for others. |
-| isEvent(e, eventName) | Checks if the incoming event data package contains a particular event. |
-| parseFunctionCall(d) | Extracts function-name (f) and parameter-data (p) from a message data package. Returns (f, p). |
-| async run(onReceiveMessage) | Starts the sockethandler main loop. onReceiveMessage: Callback function for incoming messages. |
-| sendMessage(event, data=None) | Send a message to Neutralino. event: Event-name, data: Data package as string or JSON dict. |
+NeutralinoExtension Class:
+
+| Method | Description |
+| -------------------------------- | ------------------------------------------------------------ |
+| NeutralinoExtension(debug=false) | Extension class. debug: Print data flow to the terminal. |
+| debugLog(msg, tag="info") | Write a message to the terminal.
msg: Message
tag: The message type, "in" for incoming, "out" for outgoing, "info" for others. |
+| isEvent(d, e) | Checks if the incoming event data package contains a particular event.
d: Data-package
e: Event-name |
+| parseFunctionCall(d) | Extracts function-name (f) and parameter-data (p) from a message data package. Returns (f, p).
d: Data-package. |
+| async run(onReceiveMessage) | Starts the sockethandler main loop.
onReceiveMessage: Callback function for incoming messages. |
+| runThread(f, t, d): | Starts a background task.
f: Task-function
t: Task-name
d: Data-package |
+| sendMessage(e, d=None) | Send a message to Neutralino.
e: Event-name,
d: Data-package as string or JSON dict. |
+
+| Property | Description |
+| ----------- | --------------------------------------------------------- |
+| debug | If true, data flow is printed to the terminal |
+| pollSigStop | If true, then polling for long running tasks is inactive. |
+
+Events sent from the extension to the frontend:
+
+| Event | Description |
+| ------------ | ------------------------------------------------- |
+| startPolling | Starts polling lon-running tasks on the frontend. |
+| stopPolling | Stops polling. |
### python-extension.js
-| Method | Description |
-|------------------------------|---------------------------------------------------------------------------------------------------|
-| PythonExtension(debug=false) | Extension class. debug: Print data flow to the dev-console. |
-| async run(f, p=null) | Call a Python function. f: Function-name, p: Parameter data package as string or JSON. |
-| async stop() | Stop and quit the Python extension and its parent app. Use this if Neutralino runs in Cloud-Mode. |
+PythonExtension Class:
+
+| Method | Description |
+| -------------------- | ------------------------------------------------------------ |
+| async run(f, p=null) | Call a Python function. f: Function-name, p: Parameter data package as string or JSON. |
+| async stop() | Stop and quit the Python extension and its parent app. Use this if Neutralino runs in Cloud-Mode. |
+
+| Property | Description |
+| ----------- | --------------------------------------------------------- |
+| debug | If true, data flow is printed to the dev-console. |
+| pollSigStop | If true, then polling for long running tasks is inactive. |
+
+Events, sent from the frontend to the extension:
+
+| Event | Description |
+| -------- | ------------------------------------------------------------ |
+| appClose | Notifies the extension, that the app will close. This quits the extension. |
+| poll | Forces the extsension to process the long-running task's message queue. |
## More about Neutralino
+
- [NeutralinoJS Home](https://neutralino.js.org)
- [Neutralino Build Automation for macOS, Windows, Linux](https://github.com/hschneider/neutralino-build-scripts)
diff --git a/extensions/python/NeutralinoExtension.py b/extensions/python/NeutralinoExtension.py
index 8027b53..9e41a2d 100644
--- a/extensions/python/NeutralinoExtension.py
+++ b/extensions/python/NeutralinoExtension.py
@@ -12,12 +12,13 @@
import uuid, json, time, asyncio, sys, os, signal, subprocess
from simple_websocket import *
from queue import Queue
+from threading import Thread
class NeutralinoExtension:
def __init__(self, debug=False):
- self.version = "1.2.1"
+ self.version = "1.2.2"
self.debug = debug
self.debugTermColors = True # Use terminal colors
@@ -126,6 +127,17 @@ async def run(self, onReceiveMessage):
self.debugLog('WebSocket closed.')
await self.socket.close()
+ def runThread(self, f, t, d):
+ """
+ Start a threaded background task.
+ fn: Task function
+ t: Task name
+ d: Data to process
+ """
+ thread = Thread(target=f, name=t, args=(d,))
+ thread.daemon = True
+ thread.start()
+
def parseFunctionCall(self, d):
"""
Extracts method and parameters from a data package.
diff --git a/extensions/python/main.py b/extensions/python/main.py
index 66fb467..cb3db8c 100644
--- a/extensions/python/main.py
+++ b/extensions/python/main.py
@@ -8,6 +8,17 @@
DEBUG = True # Print incoming event messages to the console
+def taskLongRun(d):
+ #
+ # Simulate a long running task.
+ # Progress messages are queued and polled every 500 ms from the fronted.
+
+ for i in range(5):
+ ext.sendMessage('pingResult', "Long-running task: %s / %s" % (i + 1, 5))
+ time.sleep(1)
+
+ ext.sendMessage("stopPolling")
+
def ping(d):
#
# Send some data to the Neutralino app
@@ -30,6 +41,10 @@ def processAppEvent(d):
if f == 'ping':
ping(d)
+ if f == 'longRun':
+ ext.sendMessage("startPolling")
+ ext.runThread(taskLongRun, 'taskLongRun', d)
+
# Activate extension
#
diff --git a/resources/index.html b/resources/index.html
index 4314d32..643073c 100644
--- a/resources/index.html
+++ b/resources/index.html
@@ -10,6 +10,7 @@