ForgeTotal.js 5
pl

Total.js 5

Wywołaj działający program Pythona przez PYPELINE()

Total.js 5 · total5@0.0.19 pythonpypelineintegrationsai Uruchomiono z Total.js 5

PYPELINE(name, [options]) uruchamia pypelines/name.py przez python3 i utrzymuje z nim połączenie, dzięki czemu aplikacja może używać bibliotek Pythona (dane, modele, przetwarzanie obrazów) bez uruchamiania nowego procesu dla każdego żądania. Zwraca obiekt z send(data) do wysyłania wiadomości, on('data', fn) do odbierania odpowiedzi i close() do zatrzymania skryptu. Nazwa zaczynająca się od ~ jest zamiast tego ścieżką do pliku (~/opt/job.py), a tekst będący kodem (ma podziały wierszy lub wygląda jak Python) jest uruchamiany jako program wbudowany przez python3 -c.

  • Protokół. Skrypt jest uruchamiany jako python3 -u script.py <socket>: ostatni argument to ścieżka gniazda Unix, na którym nasłuchuje aplikacja, a skrypt się z nim łączy. Każda wiadomość, w obu kierunkach, to ramka: 4 bajty z długością (big endian), a po nich ciało, które domyślnie jest JSON-em.
  • Opcje: type ('json' domyślnie, 'text' lub cokolwiek innego dla surowych Bufferów), autorestart (false, aby nie restartować skryptu po zakończeniu; domyślnie jest uruchamiany ponownie), threads i socket: false (bez gniazda, tylko uruchom program).
  • Wiadomości wysłane, zanim skrypt się połączył, czekają i są dostarczane, gdy się połączy.
  • Zdarzenia: open (skrypt się połączył), data (wiadomość), restart i close.
  • Ścieżka gniazda ma limit. Gniazdo to plik w folderze tmp aplikacji, a system ogranicza całą ścieżkę (około 104 znaków w macOS i 108 w Linuksie). W projekcie głęboko na dysku serwer kończy się błędem listen EINVAL, skrypt nigdy się nie łączy, a wiadomości czekają w nieskończoność. Ścieżką jest właściwość pypeline.socket: ustaw ją zaraz po PYPELINE() (plik jest sprawdzany, a serwer uruchamiany później) na krótką, jak robi to przykład.
  • Program w Pythonie to zwykły program działający z uprawnieniami aplikacji: jest tak bezpieczny jak kod pliku i trzeba mieć zainstalowanego Pythona 3 (python3 w 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 });
}