ForgeTotal.js 5
sv

Total.js 5

Anropa ett Python-program som fortsätter köra med PYPELINE()

Total.js 5 · total5@0.0.19 pythonpypelineintegrationsai Körd med Total.js 5

PYPELINE(name, [options]) startar pypelines/name.py med python3 och håller en anslutning till det, så att applikationen kan använda Python-bibliotek (data, modeller, bildbehandling) utan att starta en ny process för varje förfrågan. Den returnerar ett objekt med send(data) för att skicka ett meddelande, on('data', fn) för att ta emot svaren och close() för att stoppa skriptet. Ett namn som börjar med ~ är i stället sökvägen till en fil (~/opt/job.py), och en text som är kod (den har radbrytningar eller ser ut som Python) körs som ett inline-program med python3 -c.

  • Protokollet. Skriptet startas som python3 -u script.py <socket>: det sista argumentet är sökvägen till en Unix-socket som applikationen lyssnar på, och skriptet ansluter till den. Varje meddelande, i båda riktningarna, är en ram: 4 byte med längden (big endian) följda av kroppen, som som standard är JSON.
  • Alternativ: type ('json' standard, 'text', eller något annat för råa Buffer), autorestart (false för att inte starta om skriptet när det slutar; som standard startas det igen), threads och socket: false (ingen socket, bara starta programmet).
  • Meddelanden som skickas innan skriptet har anslutit väntar och levereras när det ansluter.
  • Händelser: open (skriptet anslöt), data (ett meddelande), restart och close.
  • Socketens sökväg har en gräns. Socketen är en fil i mappen tmp i applikationen och systemet begränsar hela sökvägen (cirka 104 tecken på macOS och 108 på Linux). I ett projekt som ligger djupt på disken misslyckas servern med listen EINVAL, skriptet ansluter aldrig och meddelandena väntar för alltid. Sökvägen är egenskapen pypeline.socket: sätt den direkt efter PYPELINE() (filen kontrolleras och servern startas efteråt) till en kort, som exemplet gör.
  • Python-programmet är ett vanligt program som körs med applikationens rättigheter: det är bara så säkert som filens kod, och Python 3 måste vara installerat (python3 i 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 });
}