Included method for synchronisation between multiple clients

Added clear and download button
Several CSS improvements
This commit is contained in:
Christoph Stahl 2017-05-22 11:16:32 +02:00
parent ac0d1acb90
commit cd6164ac3e
5 changed files with 167 additions and 67 deletions

View file

@ -1,9 +1,11 @@
import sys import sys
import io import io
import argparse import argparse
from threading import Thread, Event from threading import Thread, Event, Lock
from multiprocessing import Queue from multiprocessing import Queue
import queue
from os import fdopen, path from os import fdopen, path
from . import argparser_wrapper from . import argparser_wrapper
@ -41,19 +43,39 @@ class FlaskThread(Thread):
def run(self): def run(self):
views.app.run(port=self.port, threaded=True, host=self.host) views.app.run(port=self.port, threaded=True, host=self.host)
class Output(): class OutputThread(Thread):
def __init__(self, queue, restart): def __init__(self, inqueue, restart):
self._queue = queue super().__init__()
self._restart = restart self.queue = inqueue
self.restart = restart
self.cache = [] self.cache = []
self.clients = []
self.sem = Lock()
self.stopped = False
def stop(self):
self.stopped = True
def run(self):
while not self.restart.is_set() or self.stopped:
item = self.queue.get()
self.cache.append(item)
self.sem.acquire()
for i, (outqueue, active) in enumerate(self.clients):
try:
outqueue.put(item, True, 1)
except queue.Full:
self.clients[i] = self.clients[i][0], False
self.sem.release()
self.clients = [client for client in self.clients if client[1] == True]
def add_client(self):
print("new connection. Current connections: %s" % len(self.clients))
new_queue = queue.Queue(10)
self.clients.append((new_queue, True))
return new_queue
def get_output(self):
def gen_output():
while not self._restart.is_set():
msg_type, line = self._queue.get()
self.cache.append((msg_type, line))
yield (msg_type, line)
return gen_output()
def start_module(name, is_module): def start_module(name, is_module):
views.app.restart.clear() views.app.restart.clear()
@ -63,7 +85,8 @@ def start_module(name, is_module):
views.app.queue = Queue() views.app.queue = Queue()
ioout = QueuedOut("out", views.app.queue) ioout = QueuedOut("out", views.app.queue)
ioerr = QueuedOut("err", views.app.queue) ioerr = QueuedOut("err", views.app.queue)
views.app.output = Output(views.app.queue, views.app.restart) views.app.output = OutputThread(views.app.queue, views.app.restart)
#Output(views.app.queue, views.app.restart)
views.app.actionQueue = Queue() # This holds only one Argparser Object views.app.actionQueue = Queue() # This holds only one Argparser Object
views.app.namespaceQueue = Queue() # This hold only one Namespace Object views.app.namespaceQueue = Queue() # This hold only one Namespace Object
@ -79,11 +102,13 @@ def start_module(name, is_module):
is_module = is_module is_module = is_module
) )
views.app.module_process.start() views.app.module_process.start()
views.app.output.start()
views.app.mutex_groups, views.app.actions, name, views.app.desc = views.app.actionQueue.get() views.app.mutex_groups, views.app.actions, name, views.app.desc = views.app.actionQueue.get()
if name: if name:
views.app.name = name views.app.name = name
views.app.module_process.join() views.app.module_process.join()
views.app.output.stop()
ioerr.write("Process stopped ({})\n".format(views.app.module_process.exitcode)) ioerr.write("Process stopped ({})\n".format(views.app.module_process.exitcode))
views.app.restart.wait() views.app.restart.wait()

View file

@ -1,12 +1,27 @@
.page {
height: 100vh;
}
#main-content {
height: 100%;
display: block;
}
#output { #output {
position: relative; flex:1;
background-color: #000000; background-color: #000000;
color: #FFFFFF; color: #FFFFFF;
font-weight: bold; font-weight: bold;
font-family: "Lucida Console", Monaco, monospace; font-family: "Lucida Console", Monaco, monospace;
white-space:pre; white-space:pre;
overflow-y: auto; overflow-y: scroll;
height: 100vh; height: 100%;
}
@media (min-width:40em) {
#main-content {
display: flex;
}
} }
ul { ul {
@ -25,6 +40,7 @@ li.subparser {
#arguments { #arguments {
padding: 0px; padding: 0px;
overflow: scroll;
} }
.tabs-panel { .tabs-panel {

View file

@ -228,6 +228,8 @@ window.onload = function() {
oboe('/output.json').node('output.*', function(e){ oboe('/output.json').node('output.*', function(e){
if(e.type === "err") { if(e.type === "err") {
printErr(e.line); printErr(e.line);
} else if (e.type === "sig") {
signal(e.line)
} else { } else {
printOut(e.line); printOut(e.line);
} }
@ -245,52 +247,78 @@ function printErr(data){
$("#output").scrollTop($("#output")[0].scrollHeight); $("#output").scrollTop($("#output")[0].scrollHeight);
} }
function signal(type) {
console.log(type);
if(type === "pause") {
pauseCallback();
} else if(type === "resume") {
resumeCallback();
} else if(type === "reload") {
reloadCallback();
} else if(type === "stop") {
stopCallback();
} else if(type === "start") {
startCallback();
} else if(type === "clear") {
clearCallback();
}
}
function resumeProcess() { function resumeProcess() {
$.get({url: '/resume'});
}
function resumeCallback() {
$("#resumeButton").css('display', 'none'); $("#resumeButton").css('display', 'none');
//$("#sendButton").css('display', 'inline-block'); //$("#sendButton").css('display', 'inline-block');
$("#pauseButton").css('display', 'inline-block');//removeClass("disabled").prop('disabled', false); $("#pauseButton").css('display', 'inline-block');//removeClass("disabled").prop('disabled', false);
$.get({url: '/resume'});
printErr("Process resumed") printErr("Process resumed")
} }
function pauseProcess() { function pauseProcess() {
//$("#sendButton").css('display', 'none'); //$("#sendButton").css('display', 'none');
$.get({url: '/pause'});
}
function pauseCallback() {
$("#resumeButton").css('display', 'inline-block'); $("#resumeButton").css('display', 'inline-block');
$("#pauseButton").css('display', 'none');//addClass("disabled").prop('disabled', true); $("#pauseButton").css('display', 'none');//addClass("disabled").prop('disabled', true);
$.get({url: '/pause'});
printErr("Process paused") printErr("Process paused")
} }
function reloadProcess() { function reloadProcess() {
$.get({url: '/reload'});
}
function reloadCallback() {
$("#sendButton").removeClass("disabled").prop('disabled', false); $("#sendButton").removeClass("disabled").prop('disabled', false);
$("#stopButton").addClass("disabled").prop('disabled', true);//css('display', 'inline-block'); $("#stopButton").addClass("disabled").prop('disabled', true);//css('display', 'inline-block');
$("#pauseButton").addClass("disabled").prop('disabled', true);//css('display', 'inline-block'); $("#pauseButton").addClass("disabled").prop('disabled', true);//css('display', 'inline-block');
$("#reloadButton").addClass("disabled").prop('disabled', true);//css('display', 'inline-block'); $("#reloadButton").addClass("disabled").prop('disabled', true);//css('display', 'inline-block');
$.get({url: '/reload', oboe('/output.json').node('output.*', function(e){
success: function() { if(e.type === "err") {
oboe('/output.json').node('output.*', function(e){ printErr(e.line);
if(e.type === "err") { } else if (e.type === "sig") {
printErr(e.line); signal(e.line)
} else { } else {
printOut(e.line); printOut(e.line);
} }
}); });
}});
} }
function stopProcess() { function stopProcess() {
$.get({url: '/stop'});
}
function stopCallback() {
$("#sendButton").css('display', 'inline-block').prop('disabled', true).addClass('disabled'); $("#sendButton").css('display', 'inline-block').prop('disabled', true).addClass('disabled');
$("#pauseButton").css('display', 'none'); $("#pauseButton").css('display', 'none');
$("#resumeButton").css('display', 'none'); $("#resumeButton").css('display', 'none');
$("#stopButton").addClass("disabled").prop('disabled', true);//css('display', 'none'); $("#stopButton").addClass("disabled").prop('disabled', true);//css('display', 'none');
$("#reloadButton").removeClass("disabled").prop('disabled', false);//css('display', 'inline-block'); $("#reloadButton").removeClass("disabled").prop('disabled', false);//css('display', 'inline-block');
$.get({url: '/stop'});
} }
function sendData() { function sendData() {
$("#sendButton").css('display', 'none');//addClass("disabled").prop('disabled', true);
$("#stopButton").removeClass("disabled").prop('disabled', false);//css('display', 'inline-block');
$("#pauseButton").css('display', 'inline-block').removeClass("disabled").prop('disabled', false);//css('display', 'inline-block');
console.log(calcParams()); console.log(calcParams());
$.post({ $.post({
url: '/arguments', url: '/arguments',
@ -299,6 +327,22 @@ function sendData() {
}); });
} }
function clearOutput() {
$.get({
url: 'clear'
});
}
function clearCallback() {
$("#output").empty();
}
function startCallback() {
$("#sendButton").css('display', 'none');//addClass("disabled").prop('disabled', true);
$("#stopButton").removeClass("disabled").prop('disabled', false);//css('display', 'inline-block');
$("#pauseButton").css('display', 'inline-block').removeClass("disabled").prop('disabled', false);//css('display', 'inline-block');
}
function addParam(params, object) { function addParam(params, object) {
params[$(object).data('name')] = []; params[$(object).data('name')] = [];
if($(object).attr('type') === "checkbox") { if($(object).attr('type') === "checkbox") {

View file

@ -1,42 +1,45 @@
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<title>{{ name }}</title> <title>{{ name }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='css/foundation.min.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='css/foundation.min.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='icons/foundation-icons.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='icons/foundation-icons.css') }}" />
<link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}" /> <link rel="stylesheet" href="{{ url_for('static', filename='css/app.css') }}" />
<script src="{{ url_for('static', filename='js/vendor/what-input.js') }}"></script> <script src="{{ url_for('static', filename='js/vendor/what-input.js') }}"></script>
<script src="{{ url_for('static', filename='js/vendor/jquery.js') }}"></script> <script src="{{ url_for('static', filename='js/vendor/jquery.js') }}"></script>
<script src="{{ url_for('static', filename='js/vendor/foundation.min.js') }}"></script> <script src="{{ url_for('static', filename='js/vendor/foundation.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/oboe-browser.min.js') }}"></script> <script src="{{ url_for('static', filename='js/oboe-browser.min.js') }}"></script>
</head> </head>
<body> <body>
<div class=page> <div class=page>
<div id="main-content">
<div class="columns large-4 medium-6 small-12" id="arguments">
<div class="top-bar" id="header"> <div class="top-bar" id="header">
<div class="top-bar-title"><strong>{{ name }} - {{ description }}</strong></div> <div class="top-bar-title"><strong>{{ name }} - {{ description }}</strong></div>
<div class="top-bar-right"> <div class="top-bar-right">
<div id="control-buttons"> <div id="control-buttons">
<div class="button-group"> <div class="button-group">
<button type="button" class="button success" id="sendButton" onclick="sendData()"> <i class="fi-play"></i> </button> <button type="button" class="button success" id="sendButton" onclick="sendData()"> <i class="fi-play"></i> </button>
<button type="button" style="display: none;" class="button secondary disabled" disabled id="pauseButton" onclick="pauseProcess()"> <i class="fi-pause"></i> </button> <button type="button" style="display: none;" class="button secondary disabled" disabled id="pauseButton" onclick="pauseProcess()"> <i class="fi-pause"></i> </button>
<button type="button" style="display: none;" class="button secondary" id="resumeButton" onclick="resumeProcess()"> <i class="fi-play"></i> </button> <button type="button" style="display: none;" class="button secondary" id="resumeButton" onclick="resumeProcess()"> <i class="fi-play"></i> </button>
<button type="button" class="button alert disabled" disabled id="stopButton" onclick="stopProcess()"> <i class="fi-stop"></i> </button> <button type="button" class="button alert disabled" disabled id="stopButton" onclick="stopProcess()"> <i class="fi-stop"></i> </button>
<button type="button" class="button disabled" disabled id="reloadButton" onclick="reloadProcess()"> <i class="fi-refresh"></i> </button> <button type="button" class="button disabled" disabled id="reloadButton" onclick="reloadProcess()"> <i class="fi-refresh"></i> </button>
<button type="button" class="button" onclick="window.open('/download')"> <i class="fi-download"></i> </button> <button type="button" class="button" onclick="window.open('/download')"> <i class="fi-download"></i> </button>
</div> <button type="button" class="button alert" id="clear-button" onclick="clearOutput()"> <i class="fi-x"></i></button>
</div>
</div> </div>
</div> </div>
</div> </div>
<div class="columns large-3 medium-5 small-12" id="arguments"> <ul id="actions" class="vertical menu">
<ul id="actions" class="vertical menu"> </ul>
</ul>
</div>
<div class="columns large-9 medium-7 small-12" id="output_wrap">
<div id="output"></div>
</div>
</div> </div>
<script src="{{ url_for('static', filename='js/app.js') }}"></script> <div class="columns large-8 medium-6 small-12" id="output_wrap">
</body> <div id="output"></div>
</div>
</div>
</div>
<script src="{{ url_for('static', filename='js/app.js') }}"></script>
</body>
</html> </html>

View file

@ -43,6 +43,7 @@ def fill_namespace():
setattr(namespace, action.name, value) setattr(namespace, action.name, value)
app.namespaceQueue.put(namespace) app.namespaceQueue.put(namespace)
app.output.queue.put(("sig", "start"))
return "OK" return "OK"
@ -56,16 +57,24 @@ def get_arguments():
def stop(): def stop():
os.kill(app.module_process.pid, signal.SIGCONT) os.kill(app.module_process.pid, signal.SIGCONT)
app.module_process.terminate() app.module_process.terminate()
app.output.queue.put(("sig", "stop"))
return "OK" return "OK"
@app.route("/resume") @app.route("/resume")
def resume(): def resume():
os.kill(app.module_process.pid, signal.SIGCONT) os.kill(app.module_process.pid, signal.SIGCONT)
app.output.queue.put(("sig", "resume"))
return "OK" return "OK"
@app.route("/pause") @app.route("/pause")
def pause(): def pause():
os.kill(app.module_process.pid, signal.SIGSTOP) os.kill(app.module_process.pid, signal.SIGSTOP)
app.output.queue.put(("sig", "pause"))
return "OK"
@app.route('/clear')
def clear():
app.output.queue.put(("sig", "clear"));
return "OK" return "OK"
@app.route("/reload") @app.route("/reload")
@ -73,11 +82,12 @@ def reload():
if app.module_process.is_alive(): if app.module_process.is_alive():
return 403, "Process is still running" return 403, "Process is still running"
app.restart.set() app.restart.set()
app.output.queue.put(("sig", "reload"))
return "OK" return "OK"
@app.route("/download", methods=['GET']) @app.route("/download", methods=['GET'])
def download(): def download():
output = "\n".join([line for msg_type, line in app.output.cache]) output = "\n".join([line for msg_type, line in app.output.cache if msg_type != 'sig'])
response = make_response(output) response = make_response(output)
response.headers["Content-Disposition"] = \ response.headers["Content-Disposition"] = \
"attachment; filename=%s_%s.log" \ "attachment; filename=%s_%s.log" \
@ -89,16 +99,20 @@ def download():
def output(): def output():
def generate(): def generate():
yield '{"output":[' yield '{"output":['
app.output.sem.acquire()
cache = app.output.cache cache = app.output.cache
output = app.output.add_client()
app.output.sem.release()
for msg_type, line in cache: for msg_type, line in cache:
yield json.dumps({'type' : msg_type, 'line': line}) yield json.dumps({'type': msg_type, 'line': line})
yield ',' yield ','
output = app.output.get_output() while not app.restart.is_set():
for msg_type, line in output: msg_type, line = output.get()
print("Send ({}): {} (length: {})".format(msg_type, line, len(line)), file=sys.__stdout__) print("Send ({}): {} (length: {})".format(msg_type, line, len(line)), file=sys.__stdout__)
yield json.dumps({'type' : msg_type, 'line': line}) yield json.dumps({'type' : msg_type, 'line': line})
yield ',' yield ','
yield '\{\}]}' yield '\{\}]}'
return Response(generate(), mimetype="application/json") return Response(generate(), mimetype="application/json")
@ -107,7 +121,5 @@ def output():
@app.route("/", methods=['GET']) @app.route("/", methods=['GET'])
def index(): def index():
if not app.actionQueue.empty():
app.mutex_groups, app.actions, app.name, app.desc = app.actionQueue.get()
return render_template("index.html", mutex_groups=app.mutex_groups, actions=app.actions, name=app.name, description=app.desc) return render_template("index.html", mutex_groups=app.mutex_groups, actions=app.actions, name=app.name, description=app.desc)