ForgeTotal.js 5
ro

Total.js 5

Apelează un program Python care rămâne pornit cu PYPELINE()

Total.js 5 · total5@0.0.19 pythonpypelineintegrationsai Executată cu Total.js 5

PYPELINE(name, [options]) pornește pypelines/name.py cu python3 și păstrează o conexiune cu el, astfel încât aplicația să poată folosi biblioteci Python (date, modele, procesare de imagini) fără să pornească un proces nou pentru fiecare cerere. Returnează un obiect cu send(data) pentru a trimite un mesaj, on('data', fn) pentru a primi răspunsurile și close() pentru a opri scriptul. Un nume care începe cu ~ este în schimb calea unui fișier (~/opt/job.py), iar un text care este cod (are rupturi de rând sau seamănă cu Python) se rulează ca program inline cu python3 -c.

  • Protocolul. Scriptul este pornit ca python3 -u script.py <socket>: ultimul argument este calea unui socket Unix pe care aplicația ascultă, iar scriptul se conectează la el. Fiecare mesaj, în ambele sensuri, este un cadru: 4 octeți cu lungimea (big endian) urmați de corp, care implicit este JSON.
  • Opțiuni: type ('json' implicit, 'text' sau orice altceva pentru Buffer-e brute), autorestart (false pentru a nu reporni scriptul când se termină; implicit este pornit din nou), threads și socket: false (fără socket, doar pornește programul).
  • Mesajele trimise înainte ca scriptul să se conecteze așteaptă și sunt livrate când se conectează.
  • Evenimente: open (scriptul s-a conectat), data (un mesaj), restart și close.
  • Calea socketului are o limită. Socketul este un fișier în folderul tmp al aplicației, iar sistemul limitează calea întreagă (aproximativ 104 caractere pe macOS și 108 pe Linux). Într-un proiect aflat adânc pe disc, serverul eșuează cu listen EINVAL, scriptul nu se conectează niciodată, iar mesajele așteaptă la nesfârșit. Calea este proprietatea pypeline.socket: setează-o imediat după PYPELINE() (fișierul este verificat și serverul pornit după aceea) la una scurtă, așa cum face exemplul.
  • Programul Python este un program obișnuit care rulează cu drepturile aplicației: este sigur doar cât este codul fișierului, iar Python 3 trebuie să fie instalat (python3 în 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 });
}