# Wywołaj działający program Pythona przez PYPELINE()

> Uruchom skrypt Pythona z aplikacji, wysyłaj mu wiadomości JSON przez gniazdo i odbieraj jego odpowiedzi; skrypt jest restartowany, jeśli padnie.

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

`PYPELINE(name, [options])` uruchamia `pypelines/<name>.py` przez `python3` i utrzymuje z nim połączenie, dzięki czemu aplikacja może używać bibliotek Pythona (dane, modele, przetwarzanie obrazów) bez uruchamiania nowego procesu dla każdego żądania. Zwraca obiekt z `send(data)` do wysyłania wiadomości, `on('data', fn)` do odbierania odpowiedzi i `close()` do zatrzymania skryptu. Nazwa zaczynająca się od `~` jest zamiast tego ścieżką do pliku (`~/opt/job.py`), a tekst będący kodem (ma podziały wierszy lub wygląda jak Python) jest uruchamiany jako program wbudowany przez `python3 -c`.

- **Protokół.** Skrypt jest uruchamiany jako `python3 -u script.py <socket>`: **ostatni argument to ścieżka gniazda Unix**, na którym nasłuchuje aplikacja, a skrypt się z nim łączy. Każda wiadomość, w obu kierunkach, to ramka: 4 bajty z długością (big endian), a po nich ciało, które domyślnie jest JSON-em.
- **Opcje:** `type` (`'json'` domyślnie, `'text'` lub cokolwiek innego dla surowych `Buffer`ów), `autorestart` (`false`, aby nie restartować skryptu po zakończeniu; domyślnie jest uruchamiany ponownie), `threads` i `socket: false` (bez gniazda, tylko uruchom program).
- Wiadomości wysłane, zanim skrypt się połączył, czekają i są dostarczane, gdy się połączy.
- Zdarzenia: `open` (skrypt się połączył), `data` (wiadomość), `restart` i `close`.
- **Ścieżka gniazda ma limit.** Gniazdo to plik w folderze `tmp` aplikacji, a system ogranicza całą ścieżkę (około 104 znaków w macOS i 108 w Linuksie). W projekcie głęboko na dysku serwer kończy się błędem `listen EINVAL`, skrypt nigdy się nie łączy, a wiadomości czekają w nieskończoność. Ścieżką jest właściwość `pypeline.socket`: ustaw ją zaraz po `PYPELINE()` (plik jest sprawdzany, a serwer uruchamiany później) na krótką, jak robi to przykład.
- Program w Pythonie to **zwykły program działający z uprawnieniami aplikacji**: jest tak bezpieczny jak kod pliku i trzeba mieć zainstalowanego Pythona 3 (`python3` w `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 });
}
```
