Source code for firefly.server

import os
import sys
import json
import time
import subprocess
import signal
import socket
import requests
import http.server
import socketserver

import numpy as np

from flask import Flask, render_template, request, session, current_app
from flask_socketio import SocketIO, emit, join_room, leave_room

from firefly.data_reader import SimpleReader

#in principle, we could read in the data here...


app = Flask(__name__)

# Set this variable to "threading", "eventlet" ,"gevent" or "gevent_uwsgi" to test the
# different async modes, or leave it set to None for the application to choose
# the best option based on installed packages.
async_mode = "eventlet" #"eventlet" is WAY better than "threading"

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


namespace = '/Firefly'

default_room = 'default_Firefly_AMG_ABG'

rooms = {} #will be updated below

#number of seconds between updates
seconds = 0.01

#for the stream
fps = 30

#decimation
dec = 1

#check if the GUI is separated to see if we need to send a reload signal (currently not used)
GUIseparated = False

####### setting the room (to keep each session distinct)
@socketio.on('join', namespace=namespace)
def on_join(message):
    global rooms
    # join the room
    room = message['room']
    rooms[request.sid] = room
    print('======= in room', room)
    join_room(room)


# This will probalby never fire
@socketio.on('leave', namespace=namespace)
def on_leave(message):
    print(f'======= sid {request.sid} left room {rooms[request.sid]}', )
    leave_room(message['room'])

# this should fire when a user closes/refreshes their browser
@socketio.on('disconnect', namespace=namespace)
def disconnect():
    if (request.sid in rooms):
        print(f'======= sid {request.sid} disconnected from room {rooms[request.sid]}', )
    # remove this room from the dict
    rooms.pop(request.sid, None)

# will fire when user connects
@socketio.on('connect', namespace=namespace)
def connect():
    # if there is a room defined, emit that.  If there is no room defined, then the client will be prompted to enter one before joining
    emit('room_check',{'room': default_room}, namespace=namespace)


# testing the connection
@socketio.on('connection_test', namespace=namespace)
def connection_test(message):
    print('======= connected', message)
    # session['receive_count'] = session.get('receive_count', 0) + 1
    # emit('connection_response',{'data': message['data'], 'count': session['receive_count']}, namespace=namespace, to=rooms[request.sid])


######for viewer
#will receive data from viewer 
@socketio.on('viewer_input', namespace=namespace)
def viewer_input(message):
    if (request.sid in rooms):
        socketio.emit('update_viewerParams', message, namespace=namespace, to=rooms[request.sid])

#######for GUI
#will receive data from gui
@socketio.on('gui_input', namespace=namespace)
def gui_input(message):
    if (request.sid in rooms):
        socketio.emit('update_GUIParams', message, namespace=namespace, to=rooms[request.sid])


@socketio.on('separate_GUI', namespace=namespace)
def separate_GUI():
    global GUIseparated
    GUIseparated = True

#######for Streamer
#passing the rendered texture
#trying with post below because this only seems to work when on the same localhost
# @socketio.on('streamer_input', namespace=namespace)
# def streamer_input(blob):
#     socketio.emit('update_streamer', blob, namespace=namespace)


########reading in a directory of hdf5 or csv files
@socketio.on('input_otherType', namespace=namespace)
def input_otherType(filedir):
    print('======= showing loader')
    socketio.emit('show_loader', None, namespace=namespace, to=rooms[request.sid])
    socketio.sleep(0.1) #to make sure that the above emit is executed

    fdir = os.path.join(os.getcwd(),'static','data',filedir)
    #check the file types
    ftype = '.hdf5'
    try:
        for f in os.listdir(fdir):
            if ('.csv' in f):
                ftype = '.csv'
            if ('.hdf5' in f):
                ftype = '.hdf5'
    except:
        pass

    print('======= have input '+ftype+' data file(s) in', fdir)
    reader = SimpleReader(fdir, write_to_disk=False, extension=ftype, decimation_factor=dec)
    data = json.loads(reader.JSON)

    print('======= have data from file(s), sending to viewer ...')
    socketio.emit('input_data', {'status':'start', 'length':len(data)}, namespace=namespace, to=rooms[request.sid])
    socketio.sleep(0.1) #to make sure that the above emit is executed
    for fname in data:
        print(fname, len(data[fname]))
        output = {fname:data[fname], 'status':'data'}
        socketio.emit('input_data', output, namespace=namespace, to=rooms[request.sid])
        socketio.sleep(0.1) #to make sure that the above emit is executed
    socketio.emit('input_data', {'status':'done'}, namespace=namespace, to=rooms[request.sid])
    socketio.sleep(0.1) #to make sure that the above emit is executed

    print('======= done')


##############

#flask stuff
@app.route("/viewer")
def viewer():  
    return render_template("viewer.html")

@app.route("/gui")
def gui(): 
    return render_template("gui.html")

@app.route("/")
def default(): 
    return render_template("default.html")
@app.route("/default")
def default1(): 
    return render_template("default.html")
@app.route("/index")
def default2(): 
    return render_template("default.html")

@app.route("/combined")
def combined(): 
    return render_template("combined.html")

@app.route("/VR")
def cardboard(): 
    return render_template("VR.html")

@app.route('/data_input', methods = ['POST'])
def data_input():
    print('======= receiving data from server ...')
    jsondata = request.get_json()

    if jsondata is None: return

    sze = sys.getsizeof(jsondata)
    print("======= size of data", sze)

    try:  data = json.loads(jsondata)
    except: 
        return 'Error'

    if ('room' in data):
        ## need to remove it from the request because we don't want to send it to 
        ##  the firefly instance on the other side, just need to identify the socket.
        room = data.pop('room')
    else:
        room = default_room

    if (room):

        print('======= showing loader')
        socketio.emit('show_loader', None, namespace=namespace, to=room)
        socketio.sleep(0.1) #to make sure that the above emit is executed

        print('======= sending data to viewer ...')#,data.keys())
        socketio.emit('input_data', {'status':'start', 'length':len(data)}, namespace=namespace, to=room)
        socketio.sleep(0.1) #to make sure that the above emit is executed
        for fname in data:
            print(fname, len(data[fname]))
            output = {fname:data[fname], 'status':'data'}
            socketio.emit('input_data', output, namespace=namespace, to=room)
            socketio.sleep(0.1) #to make sure that the above emit is executed
        socketio.emit('input_data', {'status':'done'}, namespace=namespace, to=room)
        socketio.sleep(0.1) #to make sure that the above emit is executed

        print('======= done')
        return 'Done'
    else:
        print('User must specify a name for the websocket "room" connected to an active firefly instance.')
        return 'Error'

@app.route("/stream")
def streamer():  
    return render_template("streamer.html", input=json.dumps({'fps':fps}))

@app.route('/stream_input', methods = ['GET','POST'])
def stream_input():
    # get the image
    blob = request.files['image']  
    blob_binary = blob.read()
    #blob.save('tmp.jpg')

    # get the room
    room = request.form['room']

    socketio.emit('update_streamer', blob_binary, namespace=namespace,  to=room) #broadcast = True,

    return 'Done'

def reload():
    #currently not used
    if (GUIseparated):
        print('======= reloading GUI')
        socketio.emit('reload_GUI', None, namespace=namespace, to=rooms[request.sid]) 
    print('======= reloading viewer')
    socketio.emit('reload_viewer', None, namespace=namespace, to=rooms[request.sid])

# Helper functions to start/stop the server
[docs]def startFlaskServer( port=5500, directory=None, frames_per_second=30, decimation_factor=1, multiple_rooms=False): """Creates a global interpreter locked process to host a Flask server that can be accessed via localhost:<port>. :param port: port number to serve the :code:`.html` files on, defaults to 5500 :type port: int, optional :param frames_per_second: enforced FPS for stream quality, used only if localhost:<port>/stream is accessed, defaults to 30 :type frames_per_second: int, optional :param decimation_factor: factor to decimate data that is being passed through localhost:<port>/data_input, defaults to 1 :type decimation_factor: int, optional :param multiple_rooms: allow multiple rooms? If True, the user will be prompted in the browser to enter a string to define the room for the given session (which would allow multiple users to interact with separate Firefly instances on a server), defaults to False. :type multiple_rooms: bool, optional """ global default_room if (multiple_rooms): default_room = None if (directory is None or directory == "None"): directory = os.path.dirname(__file__) old_dir = os.getcwd() try: print(f"Launching Firefly at: http://localhost:{port}") os.chdir(directory) if (directory is not None and directory != "None"): print(f"from directory {directory}") app.static_folder = os.path.join(directory, 'static') app.template_folder = os.path.join(directory, 'templates') global fps, dec fps = frames_per_second dec = decimation_factor socketio.run(app, host='0.0.0.0', port=port)#, use_reloader=True) except: raise finally: os.chdir(old_dir)
[docs]def startHTTPServer(port=5500,directory=None): """Creates a global interpreter locked process to host either a Flask or HTTP server that can be accessed via localhost:<port>. :param port: port number to serve the :code:`.html` files on, defaults to 5500 :type port: int, optional :param directory: the directory of the Firefly source files to be served, if None, uses `os.dirname(__file__)` i.e. the directory of the `firefly` python distribution, defaults to None :type directory: str, optional """ if directory is None: directory = os.path.dirname(__file__) Handler = http.server.SimpleHTTPRequestHandler with socketserver.TCPServer(("", port), Handler) as httpd: ipaddress,port=httpd.server_address os.chdir(directory) print(f'Serving {os.getcwd()}/index.html at http://{ipaddress}:{port}') httpd.serve_forever()
[docs]def spawnFireflyServer( port=5500, method="flask", directory=None, frames_per_second=30, decimation_factor=1, max_time=10, multiple_rooms=False): """ Starts a Firefly server as a background process. Close the server by calling :func:`firefly.server.quitAllFireflyServers`. :param port: port number to serve the :code:`.html` files on, defaults to 5500 :type port: int, optional :param method: what sort of Firefly server to open, a Flask ("flask") server or an HTTP ("http"), defaults to "flask" :type method: str, optional :param directory: the directory of the Firefly source files to be served, if None, uses `os.dirname(__file__)` i.e. the directory of the `firefly` python distribution, defaults to None :type directory: str, optional :param frames_per_second: enforced FPS for stream quality, used only if localhost:<port>/stream is accessed, defaults to 30 :type frames_per_second: int, optional :param decimation_factor: factor to decimate data that is being passed through localhost:<port>/data_input, defaults to 1 :type decimation_factor: int, optional :param max_time: maximum amount of time to wait for a Firefly server to be available. :type max_time: float, optional :param multiple_rooms: allow multiple rooms? If True, the user will be prompted in the browser to enter a string to define the room for the given session (which would allow multiple users to interact with separate Firefly instances on a server), defaults to False. :type multiple_rooms: bool, optional :return: subprocess.Popen :rtype: subprocess handler :raises RuntimeError: if max_time elapses without a successful Firefly server being initialized. """ port = int(port) ## wrap passed arguments into a list of strings args = [ f"--port={port:d}", f"--fps={int(frames_per_second):d}", f"--dec={int(decimation_factor):d}", f"--method={method}", f"--directory={directory}"] if (multiple_rooms): args.append(f"--multiple_rooms") ## use this run_server.py (even if the other directory has one) ## since it can be run remotely run_server = os.path.join(os.path.dirname(__file__),'bin','firefly') process = subprocess.Popen([sys.executable, run_server]+args) init_time = time.time() ## check if port is in use with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: print( "Waiting up to %d seconds for background Firefly server to start..."%max_time, end="") while True: try: requests.post(f'http://localhost:{port:d}',json="test"); break except: ## need to re-check the connection each iteration if time.time()-init_time >= max_time: raise RuntimeError( "Hit max wait-time of %d seconds."%max_time+ " A Firefly server could not be opened in the background.") else: print(".",end=""); time.sleep(1) print(f"done! Your server is available at - http://localhost:{port}") return process
[docs]def quitAllFireflyServers(pid=None): """Quit python processes associated with hosting Flask web-servers. :param pid: process id to quit, defaults to None, quitting all processes :type pid: int, optional :return: return_code :rtype: int """ print("Server output:") print("--------------") ## quit indiscriminately if pid is None: return_code = os.system("ps aux | grep 'run_server.py' | awk '{print $2}' | xargs kill") ## quit only the pid we were passed, ideally from the subprocess.Popen().pid but ## you know I don't judge. else: return_code = os.kill(pid,signal.SIGINT) return return_code