# Roep een Python-programma dat blijft draaien aan met PYPELINE()

> Start een Python-script vanuit de applicatie, stuur het JSON-berichten via een socket en ontvang zijn antwoorden, waarbij het script opnieuw wordt gestart als het sterft.

Framework: total5
Product: Total.js 5 (total5@0.0.19)
Language: nl
Content-SHA256: eb7cb1ee0542458f0a05f5dad16a9d54dee1bded38261a313d9a925f403e9333
Snapshot: c0700e321579725c5cf4f5ace1ad7d2866d1b4f93c24a63c0b41b6b7998e976c
Sources: total5-installed@total5@0.0.19
Review: not-recorded

`PYPELINE(name, [options])` start `pypelines/<name>.py` met `python3` en houdt een verbinding ermee, zodat de applicatie Python-bibliotheken (data, modellen, beeldverwerking) kan gebruiken zonder voor elk verzoek een nieuw proces te starten. Het geeft een object terug met `send(data)` om een bericht te sturen, `on('data', fn)` om de antwoorden te ontvangen en `close()` om het script te stoppen. Een naam die met `~` begint is in plaats daarvan het pad van een bestand (`~/opt/job.py`), en een tekst die code is (met regeleinden of die eruitziet als Python) wordt uitgevoerd als inline programma met `python3 -c`.

- **Het protocol.** Het script wordt gestart als `python3 -u script.py <socket>`: het **laatste argument is het pad van een Unix-socket** waarop de applicatie luistert, en het script verbindt ermee. Elk bericht, in beide richtingen, is een frame: 4 bytes met de lengte (big endian) gevolgd door de body, die standaard JSON is.
- **Opties:** `type` (`'json'` standaard, `'text'`, of iets anders voor ruwe `Buffer`s), `autorestart` (`false` om het script niet te herstarten als het eindigt; standaard wordt het opnieuw gestart), `threads` en `socket: false` (geen socket, alleen het programma starten).
- Berichten die worden verstuurd voordat het script is verbonden, wachten en worden bezorgd zodra het verbindt.
- Gebeurtenissen: `open` (het script verbond), `data` (een bericht), `restart` en `close`.
- **Het pad van de socket heeft een limiet.** De socket is een bestand in de map `tmp` van de applicatie en het systeem beperkt het hele pad (ongeveer 104 tekens op macOS en 108 op Linux). In een project dat diep op de schijf staat, mislukt de server met `listen EINVAL`, verbindt het script nooit en wachten de berichten eindeloos. Het pad is de eigenschap `pypeline.socket`: stel die direct na `PYPELINE()` in (het bestand wordt gecontroleerd en de server daarna gestart) op een kort pad, zoals het voorbeeld doet.
- Het Python-programma is een **gewoon programma dat met de rechten van de applicatie draait**: het is zo veilig als de code van het bestand, en Python 3 moet geïnstalleerd zijn (`python3` in het `PATH`).

```python 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)
```

```js 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 });
}
```
