ForgeTotal.js 5
es

Total.js 5

Llama a un programa Python que sigue en marcha con PYPELINE()

Total.js 5 · total5@0.0.19 pythonpypelineintegrationsai Ejecutada con Total.js 5

PYPELINE(name, [options]) arranca pypelines/name.py con python3 y mantiene una conexión con él, de modo que la aplicación puede usar librerías de Python (datos, modelos, procesamiento de imágenes) sin iniciar un proceso nuevo para cada petición. Devuelve un objeto con send(data) para enviar un mensaje, on('data', fn) para recibir las respuestas y close() para detener el script. Un nombre que empieza por ~ es en cambio la ruta de un archivo (~/opt/job.py), y un texto que es código (tiene saltos de línea o parece Python) se ejecuta como programa en línea con python3 -c.

  • El protocolo. El script se inicia como python3 -u script.py <socket>: el último argumento es la ruta de un socket Unix en el que escucha la aplicación, y el script se conecta a él. Cada mensaje, en ambas direcciones, es una trama: 4 bytes con la longitud (big endian) seguidos del cuerpo, que por defecto es JSON.
  • Opciones: type ('json' por defecto, 'text', o cualquier otro para Buffers en bruto), autorestart (false para no reiniciar el script cuando termina; por defecto se inicia de nuevo), threads y socket: false (sin socket, solo iniciar el programa).
  • Los mensajes enviados antes de que el script se haya conectado esperan y se entregan cuando lo hace.
  • Eventos: open (el script se conectó), data (un mensaje), restart y close.
  • La ruta del socket tiene un límite. El socket es un archivo en la carpeta tmp de la aplicación y el sistema limita la ruta completa (unos 104 caracteres en macOS y 108 en Linux). En un proyecto que está muy dentro del disco el servidor falla con listen EINVAL, el script nunca se conecta y los mensajes esperan para siempre. La ruta es la propiedad pypeline.socket: fíjala justo después de PYPELINE() (el archivo se comprueba y el servidor se inicia después) a una corta, como hace el ejemplo.
  • El programa Python es un programa normal que se ejecuta con los derechos de la aplicación: solo es tan seguro como el código del archivo, y hay que tener Python 3 instalado (python3 en el 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 });
}