Call a Python program that keeps running with PYPELINE()
PYPELINE(name, [options]) starts pypelines/name.py with python3 and keeps a connection to it, so the application can use Python libraries (data, models, image processing) without starting a new process for every request. It returns an object with send(data) to send a message, on('data', fn) to receive the answers and close() to stop the script. A name that starts with ~ is the path of a file instead (~/opt/job.py), and a text that is code (it has line breaks or looks like Python) is run as an inline program with python3 -c.
- The protocol. The script is started as
python3 -u script.py <socket>: the last argument is the path of a Unix socket that the application listens on, and the script connects to it. Every message, in both directions, is a frame: 4 bytes with the length (big endian) followed by the body, which is JSON by default. - Options:
type('json'default,'text', or anything else for rawBuffers),autorestart(falseto not restart the script when it ends; by default it is started again),threadsandsocket: false(no socket, only start the program). - Messages sent before the script has connected wait and are delivered when it does.
- Events:
open(the script connected),data(a message),restartandclose. - The path of the socket has a limit. The socket is a file in the
tmpfolder of the application and the system limits the whole path (about 104 characters on macOS and 108 on Linux). In a project that is deep in the disk the server fails withlisten EINVAL, the script never connects and the messages wait forever. The path is the propertypypeline.socket: set it right afterPYPELINE()(the file is checked and the server started afterwards) to a short one, as the example does. - The Python program is a normal program that runs with the rights of the application: it is only as safe as the code of the file, and Python 3 has to be installed (
python3in thePATH).
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 });
}