Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
hschneider committed Feb 12, 2024
1 parent 8da3826 commit e78c2d5
Show file tree
Hide file tree
Showing 6 changed files with 139 additions and 16 deletions.
76 changes: 63 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.<br />msg: Message<br />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.<br />d: Data-package<br />e: Event-name |
| parseFunctionCall(d) | Extracts function-name (f) and parameter-data (p) from a message data package. Returns (f, p).<br />d: Data-package. |
| async run(onReceiveMessage) | Starts the sockethandler main loop. <br />onReceiveMessage: Callback function for incoming messages. |
| runThread(f, t, d): | Starts a background task. <br />f: Task-function<br />t: Task-name<br />d: Data-package |
| sendMessage(e, d=None) | Send a message to Neutralino. <br />e: Event-name,<br />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

- <u>[NeutralinoJS Home](https://neutralino.js.org)</u>
- <u>[Neutralino Build Automation for macOS, Windows, Linux](https://github.com/hschneider/neutralino-build-scripts)</u>

Expand Down
14 changes: 13 additions & 1 deletion extensions/python/NeutralinoExtension.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions extensions/python/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
#
Expand Down
1 change: 1 addition & 0 deletions resources/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<h1>NeutralinoJs with PythonExtension</h1>
<div style="margin-top:20px">
<a href="#" onclick="PYTHON.run('ping', 'Neutralino says PING!');">Send PING to Python</a><br>
<a href="#" id="link-long-run">Start long-running background-task in Rust</a><br>
<a id="link-quit" href="#" onclick="PYTHON.stop();" style="display:none">Quit</a>
</div>
<div style="margin-top:20px">
Expand Down
7 changes: 7 additions & 0 deletions resources/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ function test() {
msg.innerHTML += "Test from Xojo ...." + '<br>';
}

// Start single instance of long running task.
//
document.getElementById('link-long-run')
.addEventListener('click', () => {
PYTHON.run('longRun')
});

// Init Neutralino
//
Neutralino.init();
Expand Down
42 changes: 40 additions & 2 deletions resources/js/python-extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
//
Expand Down Expand Up @@ -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!")
}
}
}

0 comments on commit e78c2d5

Please sign in to comment.