mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2024-08-30 18:23:09 +00:00
Merge branch 'websocket' into 'dev'
WebSockets See merge request crafty-controller/crafty-commander!25
This commit is contained in:
commit
33036eed92
@ -10,6 +10,7 @@ logger = logging.getLogger(__name__)
|
||||
from app.classes.shared.console import console
|
||||
from app.classes.shared.helpers import helper
|
||||
from app.classes.shared.tasks import tasks_manager
|
||||
from app.classes.web.websocket_helper import websocket_helper
|
||||
|
||||
try:
|
||||
import requests
|
||||
@ -43,7 +44,9 @@ class MainPrompt(cmd.Cmd):
|
||||
def do_exit(self, line):
|
||||
logger.info("Stopping all server daemons / threads")
|
||||
console.info("Stopping all server daemons / threads - This may take a few seconds")
|
||||
websocket_helper.disconnect_all()
|
||||
self._clean_shutdown()
|
||||
console.info('Waiting for main thread to stop')
|
||||
while True:
|
||||
if tasks_manager.get_main_thread_run_status():
|
||||
sys.exit(0)
|
||||
|
@ -6,6 +6,7 @@ import datetime
|
||||
from app.classes.shared.helpers import helper
|
||||
from app.classes.shared.console import console
|
||||
from app.classes.minecraft.server_props import ServerProps
|
||||
from app.classes.web.websocket_helper import websocket_helper
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -490,7 +491,7 @@ class db_shortcuts:
|
||||
@staticmethod
|
||||
def get_server_friendly_name(server_id):
|
||||
server_data = db_helper.get_server_data_by_id(server_id)
|
||||
friendly_name = "{}-{}".format(server_data.get('server_id', 0), server_data.get('server_name', None))
|
||||
friendly_name = "{} with ID: {}".format(server_data.get('server_name', None), server_data.get('server_id', 0))
|
||||
return friendly_name
|
||||
|
||||
@staticmethod
|
||||
@ -498,7 +499,8 @@ class db_shortcuts:
|
||||
|
||||
server_name = db_helper.get_server_friendly_name(server_id)
|
||||
|
||||
db_helper.add_to_audit_log(user_id, "Issued Command {} for Server: {}".format(command, server_name),
|
||||
# Example: Admin issued command start_server for server Survival
|
||||
db_helper.add_to_audit_log(user_id, "issued command {} for server {}".format(command, server_name),
|
||||
server_id, remote_ip)
|
||||
|
||||
Commands.insert({
|
||||
@ -533,6 +535,8 @@ class db_shortcuts:
|
||||
|
||||
audit_msg = "{} {}".format(str(user_data.username).capitalize(), log_msg)
|
||||
|
||||
websocket_helper.broadcast('notification', audit_msg)
|
||||
|
||||
Audit_Log.insert({
|
||||
Audit_Log.user_name: user_data.username,
|
||||
Audit_Log.user_id: user_id,
|
||||
@ -541,6 +545,16 @@ class db_shortcuts:
|
||||
Audit_Log.source_ip: source_ip
|
||||
}).execute()
|
||||
|
||||
@staticmethod
|
||||
def add_to_audit_log_raw(user_name, user_id, server_id, log_msg, source_ip):
|
||||
Audit_Log.insert({
|
||||
Audit_Log.user_name: user_name,
|
||||
Audit_Log.user_id: user_id,
|
||||
Audit_Log.server_id: server_id,
|
||||
Audit_Log.log_msg: log_msg,
|
||||
Audit_Log.source_ip: source_ip
|
||||
}).execute()
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -4,10 +4,12 @@ import json
|
||||
import time
|
||||
import logging
|
||||
import threading
|
||||
import asyncio
|
||||
|
||||
from app.classes.shared.helpers import helper
|
||||
from app.classes.shared.console import console
|
||||
from app.classes.web.tornado import webserver
|
||||
from app.classes.web.websocket_helper import websocket_helper
|
||||
|
||||
from app.classes.minecraft.stats import stats
|
||||
from app.classes.shared.controller import controller
|
||||
@ -24,6 +26,7 @@ except ModuleNotFoundError as e:
|
||||
console.critical("Import Error: Unable to load {} module".format(e, e.name))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class TasksManager:
|
||||
|
||||
def __init__(self):
|
||||
@ -41,6 +44,9 @@ class TasksManager:
|
||||
self.command_thread = threading.Thread(target=self.command_watcher, daemon=True, name="command_watcher")
|
||||
self.command_thread.start()
|
||||
|
||||
self.realtime_thread = threading.Thread(target=self.realtime, daemon=True, name="realtime")
|
||||
self.realtime_thread.start()
|
||||
|
||||
def get_main_thread_run_status(self):
|
||||
return self.main_thread_exiting
|
||||
|
||||
@ -75,10 +81,8 @@ class TasksManager:
|
||||
|
||||
db_helper.mark_command_complete(c.get('command_id', None))
|
||||
|
||||
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def _main_graceful_exit(self):
|
||||
try:
|
||||
os.remove(helper.session_file)
|
||||
@ -136,10 +140,43 @@ class TasksManager:
|
||||
logger.info("Scheduling Serverjars.com cache refresh service every 12 hours")
|
||||
schedule.every(12).hours.do(server_jar_obj.refresh_cache)
|
||||
|
||||
@staticmethod
|
||||
def realtime():
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
host_stats = db_helper.get_latest_hosts_stats()
|
||||
|
||||
while True:
|
||||
|
||||
if host_stats.get('cpu_usage') != \
|
||||
db_helper.get_latest_hosts_stats().get('cpu_usage') or \
|
||||
host_stats.get('mem_percent') != \
|
||||
db_helper.get_latest_hosts_stats().get('mem_percent'):
|
||||
# Stats are different
|
||||
|
||||
host_stats = db_helper.get_latest_hosts_stats()
|
||||
if len(websocket_helper.clients) > 0:
|
||||
# There are clients
|
||||
websocket_helper.broadcast('update_host_stats', {
|
||||
'cpu_usage': host_stats.get('cpu_usage'),
|
||||
'cpu_cores': host_stats.get('cpu_cores'),
|
||||
'cpu_cur_freq': host_stats.get('cpu_cur_freq'),
|
||||
'cpu_max_freq': host_stats.get('cpu_max_freq'),
|
||||
'mem_percent': host_stats.get('mem_percent'),
|
||||
'mem_usage': host_stats.get('mem_usage')
|
||||
})
|
||||
time.sleep(4)
|
||||
else:
|
||||
# Stats are same
|
||||
time.sleep(8)
|
||||
|
||||
def log_watcher(self):
|
||||
console.debug('in log_watcher')
|
||||
helper.check_for_old_logs(db_helper)
|
||||
schedule.every(6).hours.do(lambda: helper.check_for_old_logs(db_helper))
|
||||
|
||||
|
||||
|
||||
|
||||
tasks_manager = TasksManager()
|
||||
|
@ -173,7 +173,7 @@ class ServerHandler(BaseHandler):
|
||||
|
||||
if new_server_id:
|
||||
db_helper.add_to_audit_log(user_data['user_id'],
|
||||
"Created server {} named {}".format(server, server_name),
|
||||
"created a {} {} server named \"{}\"".format(server_parts[1], str(server_parts[0]).capitalize(), server_name), # Example: Admin created a 1.16.5 Bukkit server named "survival"
|
||||
new_server_id,
|
||||
self.get_remote_ip())
|
||||
else:
|
||||
|
@ -24,6 +24,7 @@ try:
|
||||
from app.classes.web.server_handler import ServerHandler
|
||||
from app.classes.web.ajax_handler import AjaxHandler
|
||||
from app.classes.web.api_handler import ServersStats, NodeStats
|
||||
from app.classes.web.websocket_handler import SocketHandler
|
||||
|
||||
except ModuleNotFoundError as e:
|
||||
logger.critical("Import Error: Unable to load {} module".format(e, e.name))
|
||||
@ -125,6 +126,7 @@ class webserver:
|
||||
(r'/ajax/(.*)', AjaxHandler),
|
||||
(r'/api/stats/servers', ServersStats),
|
||||
(r'/api/stats/node', NodeStats),
|
||||
(r'/ws', SocketHandler),
|
||||
]
|
||||
|
||||
app = tornado.web.Application(
|
||||
|
54
app/classes/web/websocket_handler.py
Normal file
54
app/classes/web/websocket_handler.py
Normal file
@ -0,0 +1,54 @@
|
||||
import json
|
||||
|
||||
import tornado.websocket
|
||||
from app.classes.shared.console import console
|
||||
from app.classes.shared.models import Users, db_helper
|
||||
from app.classes.web.websocket_helper import websocket_helper
|
||||
|
||||
|
||||
class SocketHandler(tornado.websocket.WebSocketHandler):
|
||||
|
||||
def get_remote_ip(self):
|
||||
remote_ip = self.request.headers.get("X-Real-IP") or \
|
||||
self.request.headers.get("X-Forwarded-For") or \
|
||||
self.request.remote_ip
|
||||
return remote_ip
|
||||
|
||||
def check_auth(self):
|
||||
user_data_cookie_raw = self.get_secure_cookie('user_data')
|
||||
|
||||
if user_data_cookie_raw and user_data_cookie_raw.decode('utf-8'):
|
||||
user_data_cookie = user_data_cookie_raw.decode('utf-8')
|
||||
user_id = json.loads(user_data_cookie)['user_id']
|
||||
query = Users.select().where(Users.user_id == user_id)
|
||||
if query.exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def open(self):
|
||||
if self.check_auth():
|
||||
self.handle()
|
||||
else:
|
||||
websocket_helper.send_message(self, 'notification', 'Not authenticated for WebSocket connection')
|
||||
self.close()
|
||||
db_helper.add_to_audit_log_raw('unknown', 0, 0, 'Someone tried to connect via WebSocket without proper authentication', self.get_remote_ip())
|
||||
websocket_helper.broadcast('notification', 'Someone tried to connect via WebSocket without proper authentication')
|
||||
|
||||
def handle(self):
|
||||
|
||||
websocket_helper.addClient(self)
|
||||
console.debug('Opened WebSocket connection')
|
||||
# websocket_helper.broadcast('notification', 'New client connected')
|
||||
|
||||
def on_message(self, rawMessage):
|
||||
|
||||
console.debug('Got message from WebSocket connection {}'.format(rawMessage))
|
||||
message = json.loads(rawMessage)
|
||||
console.debug('Event Type: {}, Data: {}'.format(message['event'], message['data']))
|
||||
|
||||
def on_close(self):
|
||||
websocket_helper.removeClient(self)
|
||||
console.debug('Closed WebSocket connection')
|
||||
# websocket_helper.broadcast('notification', 'Client disconnected')
|
||||
|
33
app/classes/web/websocket_helper.py
Normal file
33
app/classes/web/websocket_helper.py
Normal file
@ -0,0 +1,33 @@
|
||||
import json
|
||||
|
||||
from app.classes.shared.console import console
|
||||
|
||||
class WebSocketHelper:
|
||||
clients = set()
|
||||
|
||||
def addClient(self, client):
|
||||
self.clients.add(client)
|
||||
|
||||
def removeClient(self, client):
|
||||
self.clients.add(client)
|
||||
|
||||
def send_message(self, client, event_type, data):
|
||||
if client.check_auth():
|
||||
message = str(json.dumps({'event': event_type, 'data': data}))
|
||||
client.write_message(message)
|
||||
|
||||
def broadcast(self, event_type, data):
|
||||
console.debug('Sending: ' + str(json.dumps({'event': event_type, 'data': data})))
|
||||
for client in self.clients:
|
||||
try:
|
||||
self.send_message(client, event_type, data)
|
||||
except:
|
||||
pass
|
||||
|
||||
def disconnect_all(self):
|
||||
console.info('Disconnecting WebSocket clients')
|
||||
for client in self.clients:
|
||||
client.close()
|
||||
console.info('Disconnected WebSocket clients')
|
||||
|
||||
websocket_helper = WebSocketHelper()
|
@ -58,6 +58,12 @@
|
||||
|
||||
<div class="main-panel">
|
||||
|
||||
<div class="warnings">
|
||||
<div class="noscript-warning" style="padding: 20px; background-color: rgb(247, 151, 15);">
|
||||
<div><strong>Warning: </strong>Crafty doesn't work properly when JavaScript isn't enabled!</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% block content %}
|
||||
{% end %}
|
||||
|
||||
@ -71,6 +77,58 @@
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.notifications {
|
||||
position: fixed;
|
||||
width: 200px;
|
||||
top: 70px;
|
||||
right: 0px;
|
||||
}
|
||||
.notification {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem;
|
||||
padding-left: 0.7rem;
|
||||
width: 180px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
background: #282a40;
|
||||
transition: right 0.75s, opacity 0.75s, top 0.75s;
|
||||
right: -6rem;
|
||||
opacity: 0.1;
|
||||
margin-bottom: 1rem;
|
||||
z-index: 999;
|
||||
top: 0px;
|
||||
}
|
||||
.notification.active {
|
||||
right: 0rem;
|
||||
opacity: 1;
|
||||
}
|
||||
.notification.remove {
|
||||
right: 0rem;
|
||||
opacity: 0.1;
|
||||
top: -2rem;
|
||||
}
|
||||
.notification p {
|
||||
margin: 0px;
|
||||
width: calc(160.8px - 16px);
|
||||
z-index: inherit;
|
||||
}
|
||||
.notification span {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.46rem;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
line-height: 20px;
|
||||
font-size: 22px;
|
||||
user-select: none;
|
||||
z-index: inherit;
|
||||
}
|
||||
</style>
|
||||
<div class="notifications"></div>
|
||||
|
||||
|
||||
<script src="/static/assets/vendors/js/vendor.bundle.base.js"></script>
|
||||
<script src="/static/assets/js/shared/off-canvas.js"></script>
|
||||
<script src="/static/assets/js/shared/hoverable-collapse.js"></script>
|
||||
@ -79,6 +137,9 @@
|
||||
<script src="//cdnjs.cloudflare.com/ajax/libs/bootbox.js/5.4.0/bootbox.min.js"></script>
|
||||
|
||||
<script>
|
||||
|
||||
$('.noscript-warning').toggle();
|
||||
|
||||
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
|
||||
function getCookie(name) {
|
||||
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
|
||||
@ -103,6 +164,132 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
{% if request.protocol == 'https' %}
|
||||
let usingWebSockets = true;
|
||||
|
||||
let listenEvents = [];
|
||||
|
||||
try {
|
||||
|
||||
var wsInternal = new WebSocket('wss://' + location.host + '/ws');
|
||||
wsInternal.onopen = function() {
|
||||
console.log('opened WebSocket connection:', wsInternal)
|
||||
};
|
||||
wsInternal.onmessage = function (rawMessage) {
|
||||
var message = JSON.parse(rawMessage.data);
|
||||
|
||||
console.log('got message: ', message)
|
||||
|
||||
listenEvents
|
||||
.filter(listenedEvent => listenedEvent.event == message.event)
|
||||
.forEach(listenedEvent => listenedEvent.callback(message.data))
|
||||
};
|
||||
wsInternal.onerror = function (errorEvent) {
|
||||
console.error('WebSocket Error', errorEvent);
|
||||
};
|
||||
wsInternal.onclose = function (closeEvent) {
|
||||
console.log('Closed WebSocket', closeEvent);
|
||||
};
|
||||
|
||||
|
||||
webSocket = {
|
||||
on: function (event, callback) {
|
||||
console.log('registered ' + event + ' event');
|
||||
listenEvents.push({ event: event, callback: callback })
|
||||
},
|
||||
emit: function (event, data) {
|
||||
var message = {
|
||||
event: event,
|
||||
data: data
|
||||
}
|
||||
|
||||
wsInternal.send(JSON.stringify(message));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making websocket helpers', error);
|
||||
usingWebSockets = false;
|
||||
}
|
||||
{% else %}
|
||||
let usingWebSockets = false;
|
||||
warn('WebSockets are not supported in Crafty if not using the https protocol')
|
||||
var webSocket;
|
||||
{% end%}
|
||||
|
||||
function warn(message) {
|
||||
var closeEl = document.createElement('span');
|
||||
var strongEL = document.createElement('strong');
|
||||
var msgEl = document.createElement('div');
|
||||
|
||||
closeEl.innerHTML = '×';
|
||||
strongEL.textContent = 'Warning: ';
|
||||
msgEl.append(strongEL, message);
|
||||
|
||||
|
||||
closeEl.style.marginLeft = '15px';
|
||||
closeEl.style.fontWeight = 'bold';
|
||||
closeEl.style.float = 'right';
|
||||
closeEl.style.fontSize = '22px';
|
||||
closeEl.style.lineHeight = '20px';
|
||||
closeEl.style.cursor = 'pointer';
|
||||
|
||||
closeEl.addEventListener('click', function () {this.parentElement.style.display='none';});
|
||||
|
||||
var parentEl = document.createElement('div');
|
||||
|
||||
parentEl.style.padding = '20px';
|
||||
parentEl.style.backgroundColor = '#f7970f';
|
||||
|
||||
parentEl.appendChild(closeEl);
|
||||
parentEl.appendChild(msgEl);
|
||||
|
||||
document.querySelector('.warnings').appendChild(parentEl);
|
||||
}
|
||||
|
||||
function closeNotification(element) {
|
||||
element.parentElement.classList.add('remove');
|
||||
setTimeout(function () {
|
||||
element.parentElement.remove();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function notify(message) {
|
||||
console.log(`notify(${message}})`);
|
||||
var paragraphEl = document.createElement('p');
|
||||
var closeEl = document.createElement('span');
|
||||
|
||||
paragraphEl.textContent = message;
|
||||
|
||||
closeEl.innerHTML = '×';
|
||||
closeEl.addEventListener('click', function () {closeNotification(this)});
|
||||
|
||||
var parentEl = document.createElement('div');
|
||||
parentEl.appendChild(paragraphEl);
|
||||
parentEl.appendChild(closeEl);
|
||||
|
||||
parentEl.classList.add('notification');
|
||||
|
||||
document.querySelector('.notifications').appendChild(parentEl);
|
||||
|
||||
|
||||
setTimeout(function () {
|
||||
parentEl.classList.add('active');
|
||||
}, 200);
|
||||
|
||||
setTimeout(function (element) {
|
||||
closeNotification(element);
|
||||
}, 7500, closeEl);
|
||||
|
||||
`
|
||||
<div class="notification">
|
||||
<p>Hello, World! This text should overflow</p>
|
||||
<span>×</span>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
webSocket.on('notification', notify);
|
||||
|
||||
</script>
|
||||
|
||||
{% block js %}
|
||||
|
@ -34,12 +34,11 @@
|
||||
|
||||
</div>
|
||||
<div class="wrapper my-auto ml-auto ml-lg-4">
|
||||
<p class="mb-0 text-success" data-toggle="tooltip" data-placement="top" data-html="true" title="CPU Cores: {{ data.get('hosts_data').get('cpu_cores') }} <br /> CPU Cur Freq: {{ data.get('hosts_data').get('cpu_cur_freq') }} <br /> CPU Max Freq: {{ data.get('hosts_data').get('cpu_max_freq') }}" >
|
||||
{{ data.get('hosts_data').get('cpu_usage') }} {{ _('CPU Usage') }}
|
||||
|
||||
<p id="cpu_data" class="mb-0 text-success" data-toggle="tooltip" data-placement="top" data-html="true" title="CPU Cores: {{ data.get('hosts_data').get('cpu_cores') }} <br /> CPU Cur Freq: {{ data.get('hosts_data').get('cpu_cur_freq') }} <br /> CPU Max Freq: {{ data.get('hosts_data').get('cpu_max_freq') }}" >
|
||||
<span id="cpu_usage">{{ data.get('hosts_data').get('cpu_usage') }}</span> {{ _('CPU Usage') }}
|
||||
</p>
|
||||
<p class="mb-0 text-danger" data-toggle="tooltip" data-placement="top" title="Memory Usage: {{ data.get('hosts_data').get('mem_usage') }}" >
|
||||
{{ data.get('hosts_data').get('mem_percent') }}% {{ _('Memory Usage') }}
|
||||
<p id="mem_usage" class="mb-0 text-danger" data-toggle="tooltip" data-placement="top" title="Memory Usage: {{ data.get('hosts_data').get('mem_usage') }}" >
|
||||
<span id="mem_percent">{{ data.get('hosts_data').get('mem_percent') }}%</span> {{ _('Memory Usage') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -268,6 +267,20 @@ $( document ).ready(function() {
|
||||
message: '<div align="center"><i class="fas fa-spin fa-spinner"></i> Please be patient while we restart the server<br /> This screen will refresh in a moment </div>'
|
||||
});
|
||||
});
|
||||
if (webSocket) {
|
||||
cpu_data = document.getElementById('cpu_data');
|
||||
cpu_usage = document.getElementById('cpu_usage');
|
||||
mem_usage = document.getElementById('mem_usage');
|
||||
mem_percent = document.getElementById('mem_percent');
|
||||
|
||||
webSocket.on('update_host_stats', function (hostStats) {
|
||||
var cpuDataTitle = `CPU Cores: ${hostStats.cpu_cores} <br /> CPU Cur Freq: ${hostStats.cpu_cur_freq} <br /> CPU Max Freq: ${hostStats.cpu_max_freq}`;
|
||||
cpu_data.setAttribute('data-original-title', cpuDataTitle);
|
||||
cpu_usage.textContent = hostStats.cpu_usage;
|
||||
mem_usage.setAttribute('data-original-title', `Memory Usage: ${hostStats.mem_usage}`);
|
||||
mem_percent.textContent = hostStats.mem_percent + '%';
|
||||
});
|
||||
}
|
||||
|
||||
$( ".clone_button" ).click(function() {
|
||||
server_id = $(this).attr("data-id");
|
||||
|
Loading…
Reference in New Issue
Block a user