#!/usr/bin/python3
########################################################################################
# This file is part of Moubootaur Legends API.
# Copyright (C) 2019-2022 Jesusalva
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
# This library 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
# Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
########################################################################################
# This is the log master, to improve performance
import mysql.connector, signal, sys, threading, time, traceback
## Default values
HOST="127.0.0.1"; PORT=0; USER=""; PASS=""; DBXT=""; db=None;
sqli = []; running=True
SQL_PINGTIME=300.0; SQL_FLUSH=1.0
## Warnings
ERR=0
WRN=1
DBG=2
def stdout(mes, code=DBG):
if code == ERR:
color="31;1"
title="ERROR"
elif code == WRN:
color="33;1"
title="WARNING"
else:
color="32;1"
title="INFO"
print("\033[%sm[%s]\033[0m %s" % (color, title, mes))
return
## Rudimentary parser
with open("conf/import/sql_connection.conf", "r") as f:
for l in f:
r=l.replace("\"", "").replace(" ", "").replace("\t", "").replace("\n", "").replace("\r", "").split(":")
try:
if r[0] == "db_hostname":
HOST=str(r[1])
elif r[0] == "db_port":
PORT=int(r[1])
elif r[0] == "db_username":
USER=str(r[1])
elif r[0] == "db_password":
PASS=str(r[1])
elif r[0] == "db_database":
DBXT=str(r[1])
else:
pass
except:
traceback.print_exc()
## Check for fails
if USER == "":
stdout("Lacking user! Check conf/import/sql_connection.conf", ERR)
exit(1)
if PASS == "":
stdout("Lacking password! Check conf/import/sql_connection.conf", ERR)
exit(1)
if DBXT == "":
stdout("Lacking database! Check conf/import/sql_connection.conf", ERR)
exit(1)
## Init the database
def connect():
global db, HOST, USER, PASS, DBXT
stdout("Connecting to %s:%d (%s @ %s)" % (HOST, PORT, USER, DBXT))
db = mysql.connector.connect(
host=HOST,
port=str(PORT),
user=USER,
passwd=PASS,
database=DBXT
)
return
## Function to keep a database alive
def keep_alive():
global db
try:
db.ping(reconnect=True, attempts=10, delay=1)
except:
# SQL error
stdout("keep_alive: INTERNAL ERROR (ping timeout!)", ERR)
db.reconnect(attempts=12, delay=10)
return
## Start keep_alive in a thread and keep it running
def keep_alive_runner():
global db
sqlt_db1=threading.Thread(target=keep_alive, daemon=True)
sqlt_db1.start()
####################################################
## Run forever
time.sleep(0.05)
stall=0.05
while sqlt_db1.is_alive():
stdout("keep_alive: Waiting for ping")
time.sleep(2.0)
stall+=2.0
## Rebuild connection if the stall time exceeds the ping time
if stall > SQL_PINGTIME:
if sqlt_db1.is_alive():
stdout("keep_alive: DBXT Connection Restarted", WRN)
connect()
break
sql_keep_alive=threading.Timer(max(1.0, SQL_PINGTIME-stall), keep_alive_runner)
sql_keep_alive.daemon=True
sql_keep_alive.start()
return
## Read stdin for as long as possible
def run_forever():
global sqli, running
while running:
try:
bf = sys.stdin.readline()
if bf is None or bf == "":
continue
#stdout("Buffer set: %s" % str(bf))
sqli.append(str(bf).replace("\n","").replace("\r",""))
except:
traceback.print_exc()
time.sleep(SQL_FLUSH)
return
## Handle term signals
def EXIT_NOW(sn, frame):
global running, db
stdout("Exit Signal received!", ERR)
running = False
time.sleep(SQL_FLUSH)
db.close()
stdout("Terminated", WRN)
return
## Try to close Database and finish safely
#signal.signal(signal.SIGTERM, EXIT_NOW)
signal.signal(signal.SIGABRT, EXIT_NOW)
## Create the SQL connection and keep it alive
time.sleep(1.0)
connect()
keep_alive_runner()
## Watch for stdin
runner=threading.Thread(target=run_forever, daemon=True)
runner.start()
## Handle the input
stdout("Logmaster started", WRN)
bf=""
while running:
try:
## We have stuff to push
if len(sqli) > 0:
w = db.cursor()
for com in list(sqli):
try:
cmd=com.split("→")[0]
args=com.replace("%s→" % cmd, "")
## Command: SQL
## Description: Prepares a SQL statement. No escapping.
## Supports "?1", "?2" etc. for use with SAD
if cmd == "SQL":
bf=str(args)
## Command: SAD
## Description: Replaces "?" with escaped data.
elif cmd.startswith("SAD"):
bf=bf.replace("?%s" % cmd.replace("SAD", ""), args.replace("\\", "\\\\").replace('"','\\"').replace("'", "\\'").replace('\n','').replace('\r','').replace('\0',''))
## Command: SQLRUN
## Description: Executes the prepared SQL statement.
## Sanitization must be done using SAD commands.
elif cmd == "SQLRUN":
w.execute(bf)
#stdout("Query OK: %s" % bf)
bf=""
## Command: DISCORDID
## Description: Replaces "?" with the Discord ID
## For the associated argument.
## Requires the API (WIP)
elif cmd == "DISCORDID":
## FIXME: Query the API
bf=bf.replace("?%s" % cmd.replace("DISCORDID", ""), "")
pass
## Command: PING
## Description: Does nothing
elif cmd == "PING":
pass
## TODO: Integration with the API
else:
stdout("Unrecognized command: %s" % cmd, ERR)
except:
stdout("Statement failed: %s" % cmd, ERR)
traceback.print_exc()
sqli.remove(com)
db.commit()
w.close()
except KeyboardInterrupt:
running=False
stdout("Shutdown in progress!")
break
except:
traceback.print_exc()
## No need to flush ALL the time
if running:
time.sleep(SQL_FLUSH)
db.close()
stdout("Logmaster finished.")