# Ein weiterlaufendes Python-Programm mit PYPELINE() aufrufen

> Starte ein Python-Skript aus der Anwendung, sende ihm JSON-Nachrichten über einen Socket und empfange seine Antworten, wobei das Skript neu gestartet wird, wenn es stirbt.

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

`PYPELINE(name, [options])` startet `pypelines/<name>.py` mit `python3` und hält eine Verbindung zu ihm, sodass die Anwendung Python-Bibliotheken (Daten, Modelle, Bildverarbeitung) nutzen kann, ohne für jede Anfrage einen neuen Prozess zu starten. Es gibt ein Objekt mit `send(data)` zum Senden einer Nachricht, `on('data', fn)` zum Empfangen der Antworten und `close()` zum Stoppen des Skripts zurück. Ein Name, der mit `~` beginnt, ist stattdessen der Pfad einer Datei (`~/opt/job.py`), und ein Text, der Code ist (er hat Zeilenumbrüche oder sieht nach Python aus), wird als Inline-Programm mit `python3 -c` ausgeführt.

- **Das Protokoll.** Das Skript wird als `python3 -u script.py <socket>` gestartet: Das **letzte Argument ist der Pfad eines Unix-Sockets**, auf dem die Anwendung lauscht und mit dem sich das Skript verbindet. Jede Nachricht ist in beiden Richtungen ein Frame: 4 Bytes mit der Länge (Big Endian), gefolgt vom Body, der standardmäßig JSON ist.
- **Optionen:** `type` (`'json'` als Standard, `'text'` oder irgendetwas anderes für rohe `Buffer`), `autorestart` (`false`, um das Skript nicht neu zu starten, wenn es endet; standardmäßig wird es erneut gestartet), `threads` und `socket: false` (kein Socket, nur das Programm starten).
- Nachrichten, die gesendet werden, bevor sich das Skript verbunden hat, warten und werden zugestellt, sobald es sich verbindet.
- Ereignisse: `open` (das Skript hat sich verbunden), `data` (eine Nachricht), `restart` und `close`.
- **Der Pfad des Sockets hat ein Limit.** Der Socket ist eine Datei im Ordner `tmp` der Anwendung, und das System begrenzt den gesamten Pfad (etwa 104 Zeichen unter macOS und 108 unter Linux). In einem Projekt, das tief auf der Platte liegt, schlägt der Server mit `listen EINVAL` fehl, das Skript verbindet sich nie und die Nachrichten warten für immer. Der Pfad ist die Eigenschaft `pypeline.socket`: Setze sie direkt nach `PYPELINE()` (die Datei wird danach geprüft und der Server danach gestartet) auf einen kurzen, wie es das Beispiel tut.
- Das Python-Programm ist ein **normales Programm, das mit den Rechten der Anwendung läuft**: Es ist nur so sicher wie der Code der Datei, und Python 3 muss installiert sein (`python3` im `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 });
}
```
