2015-11-21 3 views
0

J'ai trouvé le travail de Miguel Grinberg très inspirant et j'ai choisi, pour un projet universitaire, d'utiliser Flask avec la bibliothèque FlaskSocketIO pour faire de l'instrumentation Java (trucs assez compliqué mais pour les curieux je travaille sur this project).Python Flask Serveur: AssertionError ("write() before start_response()")

Fondamentalement, la partie python crée simplement une commande, l'exécutant dans un thread d'arrière-plan et poussant chaque journal à l'écran, grâce à cette grande bibliothèque Flask SocketIO. Tout ce qui est décrit fonctionne plutôt bien MAIS j'ai un problème quand je veux relancer la tâche en appelant une seconde fois la même URL, je reçois cette erreur et je n'ai trouvé aucun fil dessus donc je suppose que c'est trivial ou très méchant (mais rien au milieu!). Est-ce que quelqu'un a une idée?

File "/Library/Python/2.7/site-packages/eventlet/wsgi.py", line 485, in handle_one_response 
write(b'') 
File "/Library/Python/2.7/site-packages/eventlet/wsgi.py", line 380, in write 
raise AssertionError("write() before start_response()") 
AssertionError: write() before start_response() 

Et, bien sûr, voici mon code (inspiré de l'exemple de Miguel parce que je ne pouvais pas le mettre moi-même ...)

#!/usr/bin/env python 

async_mode = None 

if async_mode is None: 
    try: 
     import eventlet 
     async_mode = 'eventlet' 
    except ImportError: 
     pass 

    if async_mode is None: 
     try: 
      from gevent import monkey 
      async_mode = 'gevent' 
     except ImportError: 
      pass 

    if async_mode is None: 
     async_mode = 'threading' 

    print('async_mode is ' + async_mode) 

# monkey patching is necessary because this application uses a background 
# thread 
if async_mode == 'eventlet': 
    import eventlet 
    eventlet.monkey_patch() 
elif async_mode == 'gevent': 
    from gevent import monkey 
    monkey.patch_all() 

import time 
from threading import Thread 
import subprocess 
from os import chdir 

from flask import Flask, render_template 
from flask_socketio import SocketIO, emit 

import InstrumentationScripts as IS 


app = Flask(__name__) 
app.config['SECRET_KEY'] = 'secret!' 
socketio = SocketIO(app, async_mode=async_mode) 
thread = None 


def background_thread(): 
    time.sleep(1) 
    socketio.emit('my response', 
        {'data': "Thread Started", 'count': 0}, 
        namespace='/test') 
    cb = IS.CommandBuilder() 
    args = cb.createCommand().split() 
    chdir(cb.InstrumentationPepDirectory) 
    process = subprocess.Popen(args, stdout=subprocess.PIPE) 
    for out in iter(process.stdout.readline, b""): 
     out = '<pre>' + out + '</pre>' 
     socketio.emit('my response', {'data': out, 'count':0}, namespace='/test') 
     time.sleep(0.001) 
    socketio.emit('my response', 
        {'data': "Thread Finished", 'count': 0}, 
        namespace='/test') 


@app.route('/') 
def index(): 
    global thread 
    if thread is None: 
     thread = Thread(target=background_thread) 
     thread.daemon = True 
     thread.start() 
    return render_template('index.html') 


@socketio.on('connect', namespace='/test') 
def test_connect(): 
    emit('my response', {'data': 'Connected', 'count': 0}) 


if __name__ == '__main__': 
    socketio.run(app) 

Si vous devez avoir la ensemble du code, voici le repo

+0

Je ne suis pas sûr de comprendre comment relancer le thread bg. Vous avez copié l'exemple officiel, qui utilise un fil qui ne finit jamais. Dans votre cas, votre thread se termine, mais la variable globale qui pointe dessus n'est pas réinitialisée lorsque cela se produit. Au moins, il n'est pas clair pour moi comment cela se passe si c'est le cas.Aussi, je ne comprends pas ce que vous entendez par "appeler une seconde fois la même URL". Qui appelle? Quelle URL? – Miguel

+0

Je vois, je comprends un peu plus encore. Ce que je voulais dire par "appeler une seconde fois la même URL" est de refaire un appel sur le "/" afin que le 'index()' soit appelé à nouveau (juste en le rouvrant dans le navigateur). J'ai une très mauvaise connaissance du sujet multithread mais théoriquement je pourrais juste enlever la condition 'if' dans l'index et ne pas rendre mon fil global ne pourrais pas je? –

+0

C'est plus compliqué que ça, je pense. Le fil bg dans mon exemple d'application diffuse des messages à tous les clients, sans les adresser à un client spécifique. On dirait que ce que vous voulez est de démarrer un thread bg par client, et dans ce thread, vous n'émettez des messages qu'à ce client. Vous devez également arrêter les threads lorsque leur client associé s'éloigne (c'est-à-dire que vous obtenez l'événement de déconnexion). – Miguel

Répondre

0

Ainsi, grâce aux commentaires Miguel, je suis venu avec une solution et je pense qu'il peut être réutilisé pour d'autres problèmes similaires:

J'ai créé un Button pour démarrer le processus. Ce Button envoie via la valeur WebSocket au serveur (un Boolean). Lorsque le serveur reçoit ce signal, il démarre le thread et ajoute une valeur True à un dictionnaire global qui stocke tous les threads en cours d'exécution (avec leur statut: en cours d'exécution (True) ou non (False)).

Distinguer toutes les sessions, j'utilise cette request.sid comme mentionné dans l'exemple (attention, ce n'est pas request.sid des non récupérables socketio méthodes (au moins, je ne l'ai pas succès)).

Pour avoir une meilleure compréhension, le dictionnaire ressemble à quelque chose comme ceci:

dict = {"sessionId1":True, "sessionId2": False} 

Il indique que le fil de la session 1 est en cours d'exécution et celle de la session 2 pas (assez évident, non?).

Ensuite, je passe le request.sid au Thread comme argument afin qu'il puisse vérifier si le Thread a été coupé de l'extérieur. Ensuite, dans la boucle où j'affiche les entrées, je vérifie la Boolean et si elle est définie sur False, je coupe le processus et le thread (en tuant le processus et en mettant fin à la méthode).

C'était donc la théorie, voici le code de travail:

#!/usr/bin/env python 

async_mode = None 

if async_mode is None: 
    try: 
     import eventlet 
     async_mode = 'eventlet' 
    except ImportError: 
     pass 

    if async_mode is None: 
     try: 
      from gevent import monkey 
      async_mode = 'gevent' 
     except ImportError: 
      pass 

    if async_mode is None: 
     async_mode = 'threading' 

    print('async_mode is ' + async_mode) 

# monkey patching is necessary because this application uses a background 
# thread 
if async_mode == 'eventlet': 
    import eventlet 
    eventlet.monkey_patch() 
elif async_mode == 'gevent': 
    from gevent import monkey 
    monkey.patch_all() 

import time 
from threading import Thread 
import subprocess 
from os import chdir 
import os 
import signal 

from flask import Flask, render_template, send_file, request, session 
from flask_socketio import SocketIO, emit 

import InstrumentationScripts as IS 


#TODO Emit only to the client who asked for instrumentation 

app = Flask(__name__, static_url_path='') 
app.config['SECRET_KEY'] = 'secret!' 
socketio = SocketIO(app, async_mode=async_mode) 
thread = None 

thread_map = {} 


def background_thread(sid): 
    time.sleep(1) 
    socketio.emit('my response', 
        {'data': "Thread Started", 'count': 0}, 
        namespace='/test') 
    cb = IS.CommandBuilder() 
    args = cb.createCommand().split() 
    chdir(cb.InstrumentationPepDirectory) 
    process = subprocess.Popen(args, stdout=subprocess.PIPE) 
    for out in iter(process.stdout.readline, b""): 
     if (thread_map[sid]): 
      out = '<pre>' + out + '</pre>' 
      socketio.emit('my response', {'data': out, 'count':0}, namespace='/test') 
      time.sleep(0.001) 
     else: 
      os.kill(process.pid, signal.SIGUSR1) 
      print 'Java Process was killed' 
      break 
    socketio.emit('my response', 
        {'data': "Thread Finished", 'count': 0}, 
        namespace='/test') 
    thread_map[sid] = False 



@app.route('/') 
def index(): 
    return render_template('index.html') 


@socketio.on('connect', namespace='/test') 
def test_connect(): 
    emit('my response', {'data': 'Connected\n', 'count': 0}) 

@socketio.on('disconnect', namespace='/test') 
def test_disconnect(): 
    thread_map[request.sid] = False 
    print('Client disconnected') 

@socketio.on('startInstrumentation', namespace='/test') 
def test_start_stop(message): 
    if message['data']: 
     thread = Thread(target=background_thread, args=(request.sid,)) 
     thread.daemon = True 
     thread.setName(request.sid) 
     thread_map[request.sid] = True 
     thread.start() 
    else: 
     thread_map[request.sid] = False 

if __name__ == '__main__': 
    socketio.run(app, debug=True) 

Et voici le code HTML minimal:

<!DOCTYPE HTML> 
<html> 
<head> 
    <title>Flask-SocketIO Test</title> 
    <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.4.min.js"></script> 
    <script type="text/javascript" src="https://cdn.socket.io/socket.io-1.3.7.js"></script> 
    <script type="text/javascript" charset="utf-8"> 
     $(document).ready(function(){ 
      namespace = '/test'; // change to an empty string to use the global namespace 

      // the socket.io documentation recommends sending an explicit package upon connection 
      // this is specially important when using the global namespace 
      var socket = io.connect('http://' + document.domain + ':' + location.port + namespace); 

      // event handler for server sent data 
      // the data is displayed in the "Received" section of the page 
      socket.on('my response', function(msg) { 
       var log = $('#log'); 
       log.append(msg.data); 
       window.scrollTo(0,document.body.scrollHeight); 
      }); 

      // event handler for new connections 
      socket.on('connect', function() { 
       socket.emit('my event', {data: 'I\'m connected!'}); 
      }); 
      var clicked = false; 
      $('form#go').submit(function(event) { 
       clicked = !clicked; 
       socket.emit('startInstrumentation', {data: clicked}); 
       if (clicked){ 
        while ($('#log').firstChild) { 
         $('#log').removeChild($('#log').firstChild); 
        } 
       } 
       return false; 
      }); 
     }); 
    </script> 
</head> 
<body> 
    <h2>Receive:</h2> 
    <form id="go" method="POST" action='#'> 
     <input type="submit" value="Go!"> 
    </form> 
    <div id="log"></div> 
</body> 
</html> 

Hope it helps.