# Chame um programa Python que continua a correr com PYPELINE()

> Arranque um script Python a partir da aplicação, envie-lhe mensagens JSON por um socket e receba as suas respostas, com o script reiniciado se morrer.

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

`PYPELINE(name, [options])` arranca `pypelines/<name>.py` com `python3` e mantém uma ligação a ele, de modo que a aplicação pode usar bibliotecas Python (dados, modelos, processamento de imagens) sem iniciar um novo processo para cada pedido. Devolve um objeto com `send(data)` para enviar uma mensagem, `on('data', fn)` para receber as respostas e `close()` para parar o script. Um nome que começa por `~` é, em vez disso, o caminho de um ficheiro (`~/opt/job.py`), e um texto que é código (tem quebras de linha ou parece Python) é executado como programa em linha com `python3 -c`.

- **O protocolo.** O script é iniciado como `python3 -u script.py <socket>`: o **último argumento é o caminho de um socket Unix** em que a aplicação escuta, e o script liga-se a ele. Cada mensagem, nos dois sentidos, é uma trama: 4 bytes com o comprimento (big endian) seguidos do corpo, que por omissão é JSON.
- **Opções:** `type` (`'json'` por omissão, `'text'`, ou qualquer outro para `Buffer`s em bruto), `autorestart` (`false` para não reiniciar o script quando termina; por omissão é iniciado de novo), `threads` e `socket: false` (sem socket, apenas iniciar o programa).
- As mensagens enviadas antes de o script se ter ligado esperam e são entregues quando ele se liga.
- Eventos: `open` (o script ligou-se), `data` (uma mensagem), `restart` e `close`.
- **O caminho do socket tem um limite.** O socket é um ficheiro na pasta `tmp` da aplicação e o sistema limita o caminho completo (cerca de 104 caracteres no macOS e 108 no Linux). Num projeto que está muito fundo no disco o servidor falha com `listen EINVAL`, o script nunca se liga e as mensagens esperam para sempre. O caminho é a propriedade `pypeline.socket`: defina-a logo a seguir a `PYPELINE()` (o ficheiro é verificado e o servidor iniciado depois) para um curto, como faz o exemplo.
- O programa Python é um **programa normal que corre com os direitos da aplicação**: só é tão seguro como o código do ficheiro, e é preciso ter o Python 3 instalado (`python3` no `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 });
}
```
