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 @@

NeutralinoJs with PythonExtension

Send PING to Python
+ Start long-running background-task in Rust
diff --git a/resources/js/main.js b/resources/js/main.js index ae6f596..9e03a68 100644 --- a/resources/js/main.js +++ b/resources/js/main.js @@ -15,6 +15,13 @@ function test() { msg.innerHTML += "Test from Xojo ...." + '
'; } +// Start single instance of long running task. +// +document.getElementById('link-long-run') + .addEventListener('click', () => { + PYTHON.run('longRun') +}); + // Init Neutralino // Neutralino.init(); diff --git a/resources/js/python-extension.js b/resources/js/python-extension.js index 82da552..42a01e5 100644 --- a/resources/js/python-extension.js +++ b/resources/js/python-extension.js @@ -2,12 +2,20 @@ // // Run PythonExtension functions by sending dispatched event messages. // -// (c)2023 Harald Schneider - marketmix.com +// (c)2023-2024 Harald Schneider - marketmix.com class PythonExtension { constructor(debug=false) { - this.version = '1.1.0'; + this.version = '1.1.2'; this.debug = debug; + + this.pollSigStop = true; + this.pollID = 0; + + // Init callback handlers for polling. + // + Neutralino.events.on("startPolling", this.onStartPolling); + Neutralino.events.on("stopPolling", this.onStopPolling); } async run(f, p=null) { // @@ -42,4 +50,34 @@ class PythonExtension { await Neutralino.extensions.dispatch(ext, event, ""); await Neutralino.app.exit(); } + + async onStartPolling(e) { + // + // This starts polling long-running tasks. + // Since this is called back from global context, + // we have to refer 'RUST' instead of 'this'. + + PYTHON.pollSigStop = false + PYTHON.pollID = setInterval(() => { + if(PYTHON.debug) { + console.log("EXT_RUST: Polling ...") + } + PYTHON.run("poll") + if(PYTHON.pollSigStop) { + clearInterval(PYTHON.pollID); + }; + }, 500); + } + + async onStopPolling(e) { + // + // Stops polling. + // Since this is called back from global context, + // we have to refer 'RUST' instead of 'this'. + + PYTHON.pollSigStop = true; + if(PYTHON.debug) { + console.log("EXT_RUST: Polling stopped!") + } + } } \ No newline at end of file