# Apelează un program Python care rămâne pornit cu PYPELINE()

> Pornește un script Python din aplicație, trimite-i mesaje JSON printr-un socket și primește răspunsurile lui, cu scriptul repornit dacă se oprește.

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

`PYPELINE(name, [options])` pornește `pypelines/<name>.py` cu `python3` și păstrează o conexiune cu el, astfel încât aplicația să poată folosi biblioteci Python (date, modele, procesare de imagini) fără să pornească un proces nou pentru fiecare cerere. Returnează un obiect cu `send(data)` pentru a trimite un mesaj, `on('data', fn)` pentru a primi răspunsurile și `close()` pentru a opri scriptul. Un nume care începe cu `~` este în schimb calea unui fișier (`~/opt/job.py`), iar un text care este cod (are rupturi de rând sau seamănă cu Python) se rulează ca program inline cu `python3 -c`.

- **Protocolul.** Scriptul este pornit ca `python3 -u script.py <socket>`: **ultimul argument este calea unui socket Unix** pe care aplicația ascultă, iar scriptul se conectează la el. Fiecare mesaj, în ambele sensuri, este un cadru: 4 octeți cu lungimea (big endian) urmați de corp, care implicit este JSON.
- **Opțiuni:** `type` (`'json'` implicit, `'text'` sau orice altceva pentru `Buffer`-e brute), `autorestart` (`false` pentru a nu reporni scriptul când se termină; implicit este pornit din nou), `threads` și `socket: false` (fără socket, doar pornește programul).
- Mesajele trimise înainte ca scriptul să se conecteze așteaptă și sunt livrate când se conectează.
- Evenimente: `open` (scriptul s-a conectat), `data` (un mesaj), `restart` și `close`.
- **Calea socketului are o limită.** Socketul este un fișier în folderul `tmp` al aplicației, iar sistemul limitează calea întreagă (aproximativ 104 caractere pe macOS și 108 pe Linux). Într-un proiect aflat adânc pe disc, serverul eșuează cu `listen EINVAL`, scriptul nu se conectează niciodată, iar mesajele așteaptă la nesfârșit. Calea este proprietatea `pypeline.socket`: setează-o imediat după `PYPELINE()` (fișierul este verificat și serverul pornit după aceea) la una scurtă, așa cum face exemplul.
- Programul Python este un **program obișnuit care rulează cu drepturile aplicației**: este sigur doar cât este codul fișierului, iar Python 3 trebuie să fie instalat (`python3` în `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 });
}
```
