Chiama un programma Python che resta in esecuzione con PYPELINE()
PYPELINE(name, [options]) avvia pypelines/name.py con python3 e mantiene una connessione con esso, così l'applicazione può usare librerie Python (dati, modelli, elaborazione di immagini) senza avviare un nuovo processo per ogni richiesta. Restituisce un oggetto con send(data) per inviare un messaggio, on('data', fn) per ricevere le risposte e close() per fermare lo script. Un nome che inizia con ~ è invece il percorso di un file (~/opt/job.py), e un testo che è codice (ha a capo o sembra Python) viene eseguito come programma inline con python3 -c.
- Il protocollo. Lo script viene avviato come
python3 -u script.py <socket>: l'ultimo argomento è il percorso di un socket Unix su cui l'applicazione ascolta, e lo script vi si connette. Ogni messaggio, in entrambe le direzioni, è un frame: 4 byte con la lunghezza (big endian) seguiti dal corpo, che per impostazione predefinita è JSON. - Opzioni:
type('json'predefinito,'text', o qualsiasi altro perBuffergrezzi),autorestart(falseper non riavviare lo script quando termina; per impostazione predefinita viene riavviato),threadsesocket: false(nessun socket, solo avviare il programma). - I messaggi inviati prima che lo script si sia connesso aspettano e vengono consegnati quando si connette.
- Eventi:
open(lo script si è connesso),data(un messaggio),restarteclose. - Il percorso del socket ha un limite. Il socket è un file nella cartella
tmpdell'applicazione e il sistema limita l'intero percorso (circa 104 caratteri su macOS e 108 su Linux). In un progetto molto in profondità nel disco il server fallisce conlisten EINVAL, lo script non si connette mai e i messaggi aspettano per sempre. Il percorso è la proprietàpypeline.socket: impostala subito dopoPYPELINE()(il file viene controllato e il server avviato dopo) su uno corto, come fa l'esempio. - Il programma Python è un programma normale che gira con i diritti dell'applicazione: è sicuro solo quanto il codice del file, e Python 3 deve essere installato (
python3nelPATH).
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 });
}