Chame um programa Python que continua a correr com PYPELINE()
PYPELINE(name, [options]) arranca pypelines/name.py com python3 e mantém uma ligação a ele, de modo que a aplicação pode usar bibliotecas Python (dados, modelos, processamento de imagens) sem iniciar um novo processo para cada pedido. Devolve um objeto com send(data) para enviar uma mensagem, on('data', fn) para receber as respostas e close() para parar o script. Um nome que começa por ~ é, em vez disso, o caminho de um ficheiro (~/opt/job.py), e um texto que é código (tem quebras de linha ou parece Python) é executado como programa em linha com python3 -c.
- O protocolo. O script é iniciado como
python3 -u script.py <socket>: o último argumento é o caminho de um socket Unix em que a aplicação escuta, e o script liga-se a ele. Cada mensagem, nos dois sentidos, é uma trama: 4 bytes com o comprimento (big endian) seguidos do corpo, que por omissão é JSON. - Opções:
type('json'por omissão,'text', ou qualquer outro paraBuffers em bruto),autorestart(falsepara não reiniciar o script quando termina; por omissão é iniciado de novo),threadsesocket: false(sem socket, apenas iniciar o programa). - As mensagens enviadas antes de o script se ter ligado esperam e são entregues quando ele se liga.
- Eventos:
open(o script ligou-se),data(uma mensagem),restarteclose. - O caminho do socket tem um limite. O socket é um ficheiro na pasta
tmpda aplicação e o sistema limita o caminho completo (cerca de 104 caracteres no macOS e 108 no Linux). Num projeto que está muito fundo no disco o servidor falha comlisten EINVAL, o script nunca se liga e as mensagens esperam para sempre. O caminho é a propriedadepypeline.socket: defina-a logo a seguir aPYPELINE()(o ficheiro é verificado e o servidor iniciado depois) para um curto, como faz o exemplo. - O programa Python é um programa normal que corre com os direitos da aplicação: só é tão seguro como o código do ficheiro, e é preciso ter o Python 3 instalado (
python3noPATH).
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)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 });
}