# Chiama un programma Python che resta in esecuzione con PYPELINE()

> Avvia uno script Python dall'applicazione, inviagli messaggi JSON tramite un socket e ricevi le sue risposte, con lo script riavviato se muore.

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

`PYPELINE(name, [options])` avvia `pypelines/<name>.py` con `python3` e mantiene una connessione con esso, così l'applicazione può usare librerie Python (dati, modelli, elaborazione di immagini) senza avviare un nuovo processo per ogni richiesta. Restituisce un oggetto con `send(data)` per inviare un messaggio, `on('data', fn)` per ricevere le risposte e `close()` per fermare lo script. Un nome che inizia con `~` è invece il percorso di un file (`~/opt/job.py`), e un testo che è codice (ha a capo o sembra Python) viene eseguito come programma inline con `python3 -c`.

- **Il protocollo.** Lo script viene avviato come `python3 -u script.py <socket>`: l'**ultimo argomento è il percorso di un socket Unix** su cui l'applicazione ascolta, e lo script vi si connette. Ogni messaggio, in entrambe le direzioni, è un frame: 4 byte con la lunghezza (big endian) seguiti dal corpo, che per impostazione predefinita è JSON.
- **Opzioni:** `type` (`'json'` predefinito, `'text'`, o qualsiasi altro per `Buffer` grezzi), `autorestart` (`false` per non riavviare lo script quando termina; per impostazione predefinita viene riavviato), `threads` e `socket: false` (nessun socket, solo avviare il programma).
- I messaggi inviati prima che lo script si sia connesso aspettano e vengono consegnati quando si connette.
- Eventi: `open` (lo script si è connesso), `data` (un messaggio), `restart` e `close`.
- **Il percorso del socket ha un limite.** Il socket è un file nella cartella `tmp` dell'applicazione e il sistema limita l'intero percorso (circa 104 caratteri su macOS e 108 su Linux). In un progetto molto in profondità nel disco il server fallisce con `listen EINVAL`, lo script non si connette mai e i messaggi aspettano per sempre. Il percorso è la proprietà `pypeline.socket`: impostala subito dopo `PYPELINE()` (il file viene controllato e il server avviato dopo) su uno corto, come fa l'esempio.
- Il programma Python è un **programma normale che gira con i diritti dell'applicazione**: è sicuro solo quanto il codice del file, e Python 3 deve essere installato (`python3` nel `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 });
}
```
