Start, Restart, Stop and see console output in one page

master
SonoMichele 2020-11-17 22:39:35 +01:00
parent 1010621929
commit 9f13296054
9 changed files with 331 additions and 52 deletions

View File

@ -3,3 +3,16 @@
@import "tailwindcss/components";
@import "tailwindcss/utilities";
.alert-success {
@apply bg-nord14;
}
.alert-warning {
@apply bg-nord12;
}
.alert-error {
@apply bg-nord11;
}

View File

@ -15,18 +15,12 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
import signal
import subprocess
from flask import Flask, render_template, Response, redirect, url_for
from shelljob import proc
import redis
from src.config import Config
r = redis.Redis(host='0.0.0.0', port=6379, db=0)
SERVER_GROUP = proc.Group()
SERVER_PROCESS = None
@ -35,55 +29,17 @@ def create_app(config_class=Config):
app = Flask(__name__)
app.config.from_object(config_class)
from src.views import main
from src.views import main, console
app.register_blueprint(main.main)
app.register_blueprint(console.console, url_prefix='/console')
# this thing streams the console output and is used by the index route
# @app.route('/stream')
# def stream():
# def read_process():
# global SERVER_GROUP
# global SERVER_PROCESS
# while SERVER_GROUP.is_pending():
# lines = SERVER_GROUP.readlines()
# for proc, line in lines:
# # yield 'data:' + line.decode('utf-8') + '\n\n'
# if ']' not in line: # the stream spams lines starting with this pattern, so I exclude it and the user doesn't see it
# yield 'data:' + line + '\n\n'
# return Response(read_process(), mimetype='text/event-stream')
# @app.route('/')
# def index():
# return render_template('index.html')
# @app.route('/start')
# def start():
# global SERVER_PROCESS
# if SERVER_PROCESS is None:
# # i need encoding=utf-8 or the input doesn't work and if I use shell=True the command ignores parameters idk why
# SERVER_PROCESS = SERVER_GROUP.run('minetestserver --terminal --logfile log.txt', encoding='utf-8')
# # SERVER_PROCESS = subprocess.Popen('minetestserver --terminal', text=True, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# r.set('running', '1')
# return str(SERVER_PROCESS.pid)
# return f'Already running, {SERVER_PROCESS.pid}.'
# @app.route('/stop')
# def stop():
# global SERVER_PROCESS
# if r.get('running') and r.get('running').decode('utf-8') == '1':
# SERVER_PROCESS.send_signal(signal.SIGTERM)
# SERVER_PROCESS = None
# r.set('running', '0')
# return 'Stopped'
# return 'Already stopped.'
# this doesn't work
# @app.route('/test')
# def test():
# # this piece of code runs commands on the server console
# global SERVER_PROCESS
# SERVER_PROCESS.stdin.writelines("/kick SonoMichele\n")
# SERVER_PROCESS.stdin.flush()
# return redirect(url_for('index'))
# return redirect(url_for('main.home'))
return app

84
src/static/console.js Normal file
View File

@ -0,0 +1,84 @@
// # Minetest Web Gui is a webapp for managing a minetest server via a gui
// # Copyright (C) 2020 SonoMichele (Michele Viotto)
// # This program is free software: you can redistribute it and/or modify
// # it under the terms of the GNU General Public License as published by
// # the Free Software Foundation, either version 3 of the License, or
// # (at your option) any later version.
// # This program is distributed in the hope that it will be useful,
// # but WITHOUT ANY WARRANTY; without even the implied warranty of
// # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// # GNU General Public License for more details.
// # You should have received a copy of the GNU General Public License
// # along with this program. If not, see <http://www.gnu.org/licenses/>.
function updateScroll(){
var element = document.getElementById("console_output");
element.scrollTop = element.scrollHeight;
}
//from here
function isFunction(functionToCheck) {
return functionToCheck && {}.toString.call(functionToCheck) === '[object Function]';
}
function debounce(func, wait) {
var timeout;
var waitFunc;
return function() {
if (isFunction(wait)) {
waitFunc = wait;
}
else {
waitFunc = function() { return wait };
}
var context = this, args = arguments;
var later = function() {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, waitFunc());
};
}
// reconnectFrequencySeconds doubles every retry
var reconnectFrequencySeconds = 1;
var evtSource;
var reconnectFunc = debounce(function() {
setupEventSource();
// Double every attempt to avoid overwhelming server
reconnectFrequencySeconds *= 2;
// Max out at ~1 minute as a compromise between user experience and server load
if (reconnectFrequencySeconds >= 64) {
reconnectFrequencySeconds = 64;
}
}, function() { return reconnectFrequencySeconds * 1000 });
function setupEventSource() {
evtSource = new EventSource("/console/stream");
evtSource.onmessage = function(e) {
document.getElementById("console_output").innerHTML += e.data + "<br/>";
updateScroll();
};
evtSource.onopen = function(e) {
// Reset reconnect frequency upon successful connection
reconnectFrequencySeconds = 1;
};
evtSource.onerror = function(e) {
evtSource.close();
reconnectFunc();
};
}
setupEventSource();
// to here is a copy paste from https://stackoverflow.com/a/54385402
// I changed only some things in setupEventSource()

File diff suppressed because one or more lines are too long

View File

@ -26,7 +26,7 @@
href="{{ url_for('static', filename='style.css') }}"
/>
</head>
<body class="bg-nord0 min-h-screen">
<body class="bg-nord0 min-h-screen p-20">
{% block content %}
{% endblock content %}
{% block footer %}

View File

@ -1,3 +1,109 @@
{% extends "base.html" %}
{% block content %}
{% endblock content %}
<div class="flex justify-center w-full">
<div class="flex flex-col justify-center items-center sm:w-full xl:w-1/2">
<div class="text-nord6 font-bold text-4xl mb-16">
<h1>Minetest Server Web Gui</h1>
</div>
<div id="notifications" class="w-full">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div id="flash-msg-{{ loop.index }}" class="flex mb-8 p-4 rounded shadow text-nord1 {{ category }}">
<div class="max-w-sm mr-5">
{{ message }}
</div>
<div>
<button onclick="hide('flash-msg-{{ loop.index }}')" class="focus:outline-none"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg></button>
</div>
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<div id="console" class="w-full bg-nord1 text-nord4 rounded shadow-inner mb-8">
<div id="console_output" class="p-2 h-64 overflow-y-auto">
</div>
<!-- I think I need help with this thing of executing commands from here -->
<!-- <div class="w-full p-2">
<form action="" class="flex items-center">
<span>Execute command:</span><input type="text" class="ml-2 block bg-nord2 shadow-inner w-64 rounded p-1">
</form>
</div> -->
</div>
<div class="flex flex-wrap justify-evenly w-full text-nord0">
<div class="mr-2 mb-2">
<button onclick="start_server()" class="py-3 w-32 rounded bg-nord14 font-bold uppercase">Start</button>
</div>
<div class="mr-2 mb-2">
<button onclick="stop_server()" class="py-3 w-32 rounded bg-nord11 font-bold uppercase">Stop</button>
</div>
<div class="mr-2 mb-2">
<button onclick="restart_server()" class="py-3 w-32 rounded bg-nord12 font-bold uppercase">Restart</button>
</div>
</div>
</div>
</div>
{% endblock content %}
{% block scripts %}
<script src="{{ url_for('static', filename='console.js') }}"></script>
<script>
function hide(id) {
document.getElementById(id).classList.add('hidden')
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function start_server() {
let http = new XMLHttpRequest();
http.open("GET", "{{ url_for('console.start') }}");
http.send();
http.onreadystatechange=function() {
if (this.readyState == 4 && this.status == 200) {
json = JSON.parse(http.responseText);
r = Math.floor((Math.random() * 1000) + 1); // random number for notifications so I can close them
document.getElementById('notifications').innerHTML += `<div id="notification-${r}" class="flex justify-between w-full mb-8 p-4 rounded shadow text-nord1 ${json.category}">
<div class="max-w-sm mr-5">
${json.msg}
</div>
<div>
<button onclick="hide('notification-${r}')" class="focus:outline-none"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg></button>
</div>
</div>`
}
}
}
function stop_server() {
let http = new XMLHttpRequest();
http.open("GET", "{{ url_for('console.stop') }}");
http.send();
http.onreadystatechange=function() {
if (this.readyState == 4 && this.status == 200) {
json = JSON.parse(http.responseText);
r = Math.floor((Math.random() * 1000) + 1); // random number for notifications so I can close them
document.getElementById('notifications').innerHTML += `<div id="notification-${r}" class="flex justify-between w-full mb-8 p-4 rounded shadow text-nord1 ${json.category}">
<div class="max-w-sm mr-5">
${json.msg}
</div>
<div>
<button onclick="hide('notification-${r}')" class="focus:outline-none"><svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg></button>
</div>
</div>`
}
}
}
async function restart_server() {
stop_server();
await sleep(1000);
start_server();
}
</script>
{% endblock scripts %}

View File

@ -0,0 +1,21 @@
# Minetest Web Gui is a webapp for managing a minetest server via a gui
# Copyright (C) 2020 SonoMichele (Michele Viotto)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# console view package
from src.views.console.routes import console

View File

@ -0,0 +1,82 @@
# Minetest Web Gui is a webapp for managing a minetest server via a gui
# Copyright (C) 2020 SonoMichele (Michele Viotto)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import signal
from flask import Blueprint, Response, flash
from src import SERVER_GROUP, SERVER_PROCESS
console = Blueprint('console', __name__)
# this thing streams the console output and is used by the index route
@console.route('/stream')
def stream():
def read_process():
global SERVER_GROUP
global SERVER_PROCESS
while SERVER_GROUP.is_pending():
lines = SERVER_GROUP.readlines()
for _, line in lines:
#if ']' not in line: # the stream spams lines starting with this pattern, so I exclude it and the user doesn't see it
yield 'data:' + line + '\n\n'
# _, line = SERVER_GROUP.readline()
# print(line.decode('utf-8'))
# yield 'data: ' + line.decode('utf-8')
return Response(read_process(), mimetype='text/event-stream')
@console.route('/start')
def start():
global SERVER_PROCESS
if SERVER_PROCESS is None:
# i need encoding=utf-8 or the input doesn't work and if I use shell=True the command ignores parameters idk why
SERVER_PROCESS = SERVER_GROUP.run(["minetestserver --logfile log.txt"], encoding='utf-8', shell=True)
# SERVER_PROCESS = subprocess.Popen('minetestserver --terminal', text=True, shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
return {
'status': 'started',
'pid': SERVER_PROCESS.pid,
'category': 'alert-success',
'msg': f'The server has been started and is running with PID {SERVER_PROCESS.pid}'
}
return {
'status': 'already_running',
'pid': SERVER_PROCESS.pid,
'category': 'alert-warning',
'msg': f'The server is already running with PID {SERVER_PROCESS.pid}'
}
@console.route('/stop')
def stop():
global SERVER_PROCESS
if SERVER_PROCESS is not None:
SERVER_PROCESS.send_signal(signal.SIGTERM)
SERVER_PROCESS = None
return {
'status': 'stopped',
'category': 'alert-success',
'msg': f'The server has been stopped'
}
return {
'status': 'already_stopped',
'category': 'alert-warning',
'msg': f'The server is not running'
}

View File

@ -1,4 +1,21 @@
from flask import Blueprint, render_template
# Minetest Web Gui is a webapp for managing a minetest server via a gui
# Copyright (C) 2020 SonoMichele (Michele Viotto)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from flask import Blueprint, render_template, flash
main = Blueprint('main', __name__)