ForgeTotal.js 5
de

Total.js 5

Ein weiterlaufendes Python-Programm mit PYPELINE() aufrufen

Total.js 5 · total5@0.0.19 pythonpypelineintegrationsai Mit Total.js 5 ausgeführt

PYPELINE(name, [options]) startet pypelines/name.py mit python3 und hält eine Verbindung zu ihm, sodass die Anwendung Python-Bibliotheken (Daten, Modelle, Bildverarbeitung) nutzen kann, ohne für jede Anfrage einen neuen Prozess zu starten. Es gibt ein Objekt mit send(data) zum Senden einer Nachricht, on('data', fn) zum Empfangen der Antworten und close() zum Stoppen des Skripts zurück. Ein Name, der mit ~ beginnt, ist stattdessen der Pfad einer Datei (~/opt/job.py), und ein Text, der Code ist (er hat Zeilenumbrüche oder sieht nach Python aus), wird als Inline-Programm mit python3 -c ausgeführt.

  • Das Protokoll. Das Skript wird als python3 -u script.py <socket> gestartet: Das letzte Argument ist der Pfad eines Unix-Sockets, auf dem die Anwendung lauscht und mit dem sich das Skript verbindet. Jede Nachricht ist in beiden Richtungen ein Frame: 4 Bytes mit der Länge (Big Endian), gefolgt vom Body, der standardmäßig JSON ist.
  • Optionen: type ('json' als Standard, 'text' oder irgendetwas anderes für rohe Buffer), autorestart (false, um das Skript nicht neu zu starten, wenn es endet; standardmäßig wird es erneut gestartet), threads und socket: false (kein Socket, nur das Programm starten).
  • Nachrichten, die gesendet werden, bevor sich das Skript verbunden hat, warten und werden zugestellt, sobald es sich verbindet.
  • Ereignisse: open (das Skript hat sich verbunden), data (eine Nachricht), restart und close.
  • Der Pfad des Sockets hat ein Limit. Der Socket ist eine Datei im Ordner tmp der Anwendung, und das System begrenzt den gesamten Pfad (etwa 104 Zeichen unter macOS und 108 unter Linux). In einem Projekt, das tief auf der Platte liegt, schlägt der Server mit listen EINVAL fehl, das Skript verbindet sich nie und die Nachrichten warten für immer. Der Pfad ist die Eigenschaft pypeline.socket: Setze sie direkt nach PYPELINE() (die Datei wird danach geprüft und der Server danach gestartet) auf einen kurzen, wie es das Beispiel tut.
  • Das Python-Programm ist ein normales Programm, das mit den Rechten der Anwendung läuft: Es ist nur so sicher wie der Code der Datei, und Python 3 muss installiert sein (python3 im PATH).
pypelines/shout.py
import json
import socket
import struct
import sys

# The last argument is the socket that the application listens on
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(sys.argv[-1])

def read(count):
    data = b''
    while len(data) < count:
        chunk = sock.recv(count - len(data))
        if not chunk:
            raise SystemExit(0)
        data += chunk
    return data

while True:
    size = struct.unpack('>I', read(4))[0]
    message = json.loads(read(size).decode('utf-8'))
    answer = json.dumps({ 'id': message['id'], 'upper': message['text'].upper(), 'words': len(message['text'].split()) }).encode('utf-8')
    sock.sendall(struct.pack('>I', len(answer)) + answer)
controllers/py.js
exports.install = function() {
	ROUTE('POST /shout/ @json', shout);
};

const pypeline = PYPELINE('shout');

// A short path for the socket: the folder of a project can be too deep for a Unix socket
pypeline.socket = require('os').tmpdir() + '/forge-shout-' + process.pid + '.socket';
const pending = {};
let counter = 0;

// Every answer says which message it is for
pypeline.on('data', function(answer) {
	const callback = pending[answer.id];
	if (callback) {
		delete pending[answer.id];
		callback(answer);
	}
});

function shout($) {
	const id = ++counter;
	const timer = setTimeout(() => $.invalid('The script did not answer'), 5000);
	pending[id] = function(answer) {
		clearTimeout(timer);
		$.json(answer);
	};
	pypeline.send({ id: id, text: $.body.text });
}