commit all the files
This commit is contained in:
parent
986a7af5fd
commit
2b21f69005
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
FROM debian:bullseye-slim
|
||||||
|
WORKDIR /opt/program
|
||||||
|
# Set some environment variables. PYTHONUNBUFFERED keeps Python from buffering our standard
|
||||||
|
# output stream, which means that logs can be delivered to the user quickly. PYTHONDONTWRITEBYTECODE
|
||||||
|
# keeps Python from writing the .pyc files which are unnecessary in this case. We also update
|
||||||
|
# PATH so that the serve program is found when the container is invoked.
|
||||||
|
ENV PYTHONUNBUFFERED=TRUE
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=TRUE
|
||||||
|
ENV PATH="/opt/program:${PATH}"
|
||||||
|
|
||||||
|
# Get basic dependencies
|
||||||
|
RUN apt-get -y update && apt-get install -y --no-install-recommends \
|
||||||
|
build-essential \
|
||||||
|
python3-pip \
|
||||||
|
python3-dev \
|
||||||
|
nginx \
|
||||||
|
ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# overwrite the link to system python and upgrade pip
|
||||||
|
RUN ln -s /usr/bin/python3 /usr/bin/python \
|
||||||
|
&& pip3 install --upgrade pip
|
||||||
|
|
||||||
|
COPY requirements.txt /tmp/
|
||||||
|
RUN pip3 install -r /tmp/requirements.txt && \
|
||||||
|
rm -rf /root/.cache
|
||||||
|
|
||||||
|
|
||||||
|
# Set up the program in the image
|
||||||
|
COPY ./ /opt/program
|
||||||
|
RUN chmod +x /opt/program/serve.py
|
||||||
|
EXPOSE 9992
|
||||||
|
CMD ./serve.py
|
19
README.md
19
README.md
@ -1,3 +1,20 @@
|
|||||||
# mc-status-page
|
# mc-status-page
|
||||||
|
|
||||||
Logs minecraft status to postgres and creates webpage based on current server status using mcstatus.
|
Logs minecraft status to postgres and creates webpage based on current server status using mcstatus.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Create `.env` with these keys:
|
||||||
|
```
|
||||||
|
DB_HOST=
|
||||||
|
DB_NAME=
|
||||||
|
DB_USER=
|
||||||
|
DB_PASS=
|
||||||
|
DB_PROJ=
|
||||||
|
DB_TABLE=
|
||||||
|
MINE_IP=
|
||||||
|
```
|
||||||
|
|
||||||
|
where `MINE_IP` is the IP Address of your Minecraft server (including port), and the rest correspond to your PostgreSQL database.
|
||||||
|
|
||||||
|
Then `docker-compose up -d` will run the site on port 9992.
|
||||||
|
95
app.py
Normal file
95
app.py
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
"""Flask app to display a table with the result of a postgreSQL query"""
|
||||||
|
|
||||||
|
from flask import Flask, render_template
|
||||||
|
import pandas as pd
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
import os
|
||||||
|
import psycopg2
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import datetime
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
warnings.filterwarnings("ignore")
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
def connect_to_database(args):
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host=args.dbhost,
|
||||||
|
database=args.dbname,
|
||||||
|
user=args.dbuser,
|
||||||
|
password=args.dbpass,
|
||||||
|
sslmode='require',
|
||||||
|
options=f'project={args.project}'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('Failed to connect to database: %s', e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
table_name = vars(args).get('table', 'minecraft')
|
||||||
|
# if args.clean doesn't exist, it will be False
|
||||||
|
if vars(args).get('clean', False):
|
||||||
|
# remove table if it exists
|
||||||
|
try:
|
||||||
|
cur.execute(f'DROP TABLE IF EXISTS {table_name}')
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('Failed to drop table: %s', e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Create table if it doesn't exist
|
||||||
|
try:
|
||||||
|
cur.execute(f'CREATE TABLE IF NOT EXISTS {table_name} (timestamp TIMESTAMP, server_name TEXT, version TEXT, protocol INT, players INT, max_players INT, latency INT)')
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('Failed to create table: %s', e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
# make a fake args object that acts the same way as argparse.Namespace
|
||||||
|
class Args:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
self.__dict__.update(kwargs)
|
||||||
|
|
||||||
|
args = Args(
|
||||||
|
dbhost=os.environ.get("DB_HOST", "localhost"),
|
||||||
|
dbname=os.environ.get("DB_NAME", "minecraft"),
|
||||||
|
dbuser=os.environ.get("DB_USER", "postgres"),
|
||||||
|
dbpass=os.environ.get("DB_PASS", ""),
|
||||||
|
project=os.environ.get("DB_PROJ", "default"),
|
||||||
|
table=os.environ.get("DB_TABLE", "minecraft"),
|
||||||
|
)
|
||||||
|
|
||||||
|
conn = connect_to_database(args)
|
||||||
|
table_name = vars(args).get("table", "minecraft")
|
||||||
|
df = pd.read_sql(
|
||||||
|
f"SELECT * FROM {table_name} ORDER BY timestamp DESC LIMIT 100;", conn
|
||||||
|
)
|
||||||
|
online = df["players"].tolist()[0]
|
||||||
|
df = df[['timestamp', 'players', 'latency']]
|
||||||
|
# format timestamp
|
||||||
|
df['timestamp'] = df['timestamp'].dt.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
# if the timestamp is more than 5 minutes old, the server is offline
|
||||||
|
latest_timestamp = datetime.datetime.strptime(df['timestamp'].tolist()[0], '%Y-%m-%d %H:%M:%S')
|
||||||
|
if (datetime.datetime.now() - latest_timestamp) > datetime.timedelta(minutes=5):
|
||||||
|
status = "The server is currently offline. Please try again later."
|
||||||
|
else:
|
||||||
|
if online > 0:
|
||||||
|
status = f"There are currently <b>{ online }</b> players online."
|
||||||
|
else:
|
||||||
|
status = "There are currently no players online."
|
||||||
|
return render_template("index.html", data=df.to_html(justify="center"), status=status)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(debug=True, host="0.0.0.0", port=9993)
|
34
docker-compose.yml
Normal file
34
docker-compose.yml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# docker-compose file for serving nginx without SSL
|
||||||
|
|
||||||
|
version: '3.7'
|
||||||
|
|
||||||
|
services:
|
||||||
|
nginx:
|
||||||
|
build: .
|
||||||
|
container_name: mcstatus-server
|
||||||
|
restart: always
|
||||||
|
ports:
|
||||||
|
- 9992:9992
|
||||||
|
volumes:
|
||||||
|
- ./nginx.conf:/etc/nginx/nginx.conf
|
||||||
|
- ./html:/usr/share/nginx/html
|
||||||
|
- ./logs:/var/log/nginx
|
||||||
|
environment:
|
||||||
|
- TZ=America/Denver
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
|
||||||
|
status:
|
||||||
|
build: .
|
||||||
|
container_name: mcstatus
|
||||||
|
restart: always
|
||||||
|
environment:
|
||||||
|
- TZ=America/Denver
|
||||||
|
env_file:
|
||||||
|
- ../.env
|
||||||
|
command: python mc.py
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
external:
|
||||||
|
name: mcstatus
|
137
mc.py
Normal file
137
mc.py
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
"""
|
||||||
|
Application to ping Minecraft server and write all data to postgresql database.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
from mcstatus import JavaServer as MinecraftServer
|
||||||
|
import psycopg2
|
||||||
|
from typing import Tuple, Dict
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# read environment variables from .env file
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
|
||||||
|
|
||||||
|
# get DB_HOST, DB_PROJ, DB_USER, DB_PASS, and DB_NAME from environment variables
|
||||||
|
DB_HOST = os.environ.get('DB_HOST', 'localhost')
|
||||||
|
DB_NAME = os.environ.get('DB_NAME', 'main')
|
||||||
|
DB_TABLE = os.environ.get('DB_TABLE', 'minecraft')
|
||||||
|
DB_PROJ = os.environ.get('DB_PROJ', 'default')
|
||||||
|
DB_USER = os.environ.get('DB_USER', 'user')
|
||||||
|
DB_PASS = os.environ.get('DB_PASS', 'password')
|
||||||
|
IP_ADD = os.environ.get('MINE_IP', 'localhost')
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
# Set up command line arguments
|
||||||
|
parser = argparse.ArgumentParser(description='Monitor Minecraft server activity and write it to postgresql database.')
|
||||||
|
parser.add_argument('--dbhost', default=DB_HOST, help='Postgresql database host')
|
||||||
|
parser.add_argument('--dbname', default=DB_NAME, help='Postgresql database name')
|
||||||
|
parser.add_argument('--dbuser', default=DB_USER, help='Postgresql database user')
|
||||||
|
parser.add_argument('--dbpass', default=DB_PASS, help='Postgresql database password')
|
||||||
|
parser.add_argument('--project', default=DB_PROJ, help='Project name')
|
||||||
|
parser.add_argument('--table', default=DB_TABLE, help='Table name')
|
||||||
|
parser.add_argument('--ip', default=IP_ADD, help='IP Address of Minecraft Server')
|
||||||
|
parser.add_argument('--interval', default=60, type=int, help='Interval in seconds between measurements')
|
||||||
|
parser.add_argument('--clean', action='store_true', help='Clean database before starting (default: False)')
|
||||||
|
args = parser.parse_args()
|
||||||
|
return args
|
||||||
|
|
||||||
|
# Connect to database
|
||||||
|
def connect_to_database(args):
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = psycopg2.connect(
|
||||||
|
host=args.dbhost,
|
||||||
|
database=args.dbname,
|
||||||
|
user=args.dbuser,
|
||||||
|
password=args.dbpass,
|
||||||
|
sslmode='require',
|
||||||
|
options=f'project={args.project}'
|
||||||
|
)
|
||||||
|
cur = conn.cursor()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('Failed to connect to database: %s', e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
table_name = vars(args).get('table', DB_TABLE)
|
||||||
|
# if args.clean doesn't exist, it will be False
|
||||||
|
if vars(args).get('clean', False):
|
||||||
|
# remove table if it exists
|
||||||
|
try:
|
||||||
|
cur.execute(f'DROP TABLE IF EXISTS {table_name}')
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('Failed to drop table: %s', e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# Create table if it doesn't exist
|
||||||
|
try:
|
||||||
|
cur.execute(f'CREATE TABLE IF NOT EXISTS {table_name} (timestamp TIMESTAMP, server_name TEXT, version TEXT, protocol INT, players INT, max_players INT, latency INT)')
|
||||||
|
conn.commit()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('Failed to create table: %s', e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
return conn
|
||||||
|
|
||||||
|
|
||||||
|
def get_minecraft_server_status(host: str) -> Dict[str, str]:
|
||||||
|
# Get Minecraft server status
|
||||||
|
try:
|
||||||
|
server = MinecraftServer.lookup(host)
|
||||||
|
status = server.status()
|
||||||
|
logging.info('Successfully got Minecraft server status')
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('Failed to get Minecraft server status: %s', e)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
# extract status as dictionary
|
||||||
|
status = status.__dict__
|
||||||
|
return status
|
||||||
|
|
||||||
|
|
||||||
|
def main(args, conn=None):
|
||||||
|
cur = conn.cursor()
|
||||||
|
try:
|
||||||
|
# Get current time
|
||||||
|
timestamp = datetime.datetime.now()
|
||||||
|
|
||||||
|
# Get Minecraft server status
|
||||||
|
ip_add = vars(args).get('ip', 'localhost:25571')
|
||||||
|
status = get_minecraft_server_status(ip_add)
|
||||||
|
table_name = vars(args).get('table', DB_TABLE)
|
||||||
|
if status:
|
||||||
|
# Write to postgres the status
|
||||||
|
cur.execute(f'INSERT INTO {table_name} (timestamp, server_name, version, protocol, players, max_players, latency) VALUES (%s, %s, %s, %s, %s, %s, %s)', (timestamp, status['raw']['description'], status['raw']['version']['name'], status['raw']['version']['protocol'], status['raw']['players']['online'], status['raw']['players']['max'], status['latency']))
|
||||||
|
conn.commit()
|
||||||
|
# Sleep for interval
|
||||||
|
time.sleep(args.interval)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
logging.info('Exiting...')
|
||||||
|
sys.exit(0)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error('Failed to write to database: %s', e)
|
||||||
|
# sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# Main loop
|
||||||
|
args = parse_args()
|
||||||
|
conn = connect_to_database(args)
|
||||||
|
while True:
|
||||||
|
# if connection to server is lost, reconnect
|
||||||
|
if not conn:
|
||||||
|
logging.info('Reconnecting to database...')
|
||||||
|
conn = connect_to_database(args)
|
||||||
|
|
||||||
|
main(args, conn=conn)
|
73
nginx.conf
Normal file
73
nginx.conf
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
worker_processes 1;
|
||||||
|
daemon off; # Prevent forking
|
||||||
|
|
||||||
|
pid /tmp/nginx.pid;
|
||||||
|
error_log /var/log/nginx/error.log;
|
||||||
|
|
||||||
|
events {
|
||||||
|
# defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
http {
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
access_log /var/log/nginx/access.log combined;
|
||||||
|
|
||||||
|
upstream gunicorn {
|
||||||
|
server unix:/tmp/gunicorn.sock;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 9992 deferred;
|
||||||
|
client_max_body_size 5m;
|
||||||
|
|
||||||
|
keepalive_timeout 5;
|
||||||
|
proxy_read_timeout 1200s;
|
||||||
|
|
||||||
|
location ~ ^/ {
|
||||||
|
#
|
||||||
|
# Wide-open CORS config for nginx
|
||||||
|
#
|
||||||
|
if ($request_method = 'OPTIONS') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
#
|
||||||
|
# Om nom nom cookies
|
||||||
|
#
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||||
|
#
|
||||||
|
# Custom headers and headers various browsers *should* be OK with but aren't
|
||||||
|
#
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
|
||||||
|
#
|
||||||
|
# Tell client that this pre-flight info is valid for 20 days
|
||||||
|
#
|
||||||
|
add_header 'Access-Control-Max-Age' 1728000;
|
||||||
|
add_header 'Content-Type' 'text/plain charset=UTF-8';
|
||||||
|
add_header 'Content-Length' 0;
|
||||||
|
return 204;
|
||||||
|
}
|
||||||
|
if ($request_method = 'POST') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
|
||||||
|
}
|
||||||
|
if ($request_method = 'GET') {
|
||||||
|
add_header 'Access-Control-Allow-Origin' '*';
|
||||||
|
add_header 'Access-Control-Allow-Credentials' 'true';
|
||||||
|
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
|
||||||
|
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_pass http://gunicorn;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
return 404 "{}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
gunicorn
|
||||||
|
gevent
|
||||||
|
flask
|
||||||
|
mcstatus
|
||||||
|
psycopg2-binary
|
||||||
|
pandas
|
||||||
|
python-dotenv
|
96
serve.py
Executable file
96
serve.py
Executable file
@ -0,0 +1,96 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
# This file implements the API shell.
|
||||||
|
# You don't necessarily need to modify it for various algorithms.
|
||||||
|
# It starts nginx and gunicorn with the correct configurations and
|
||||||
|
# then waits until gunicorn exits.
|
||||||
|
#
|
||||||
|
# The flask server is specified to be the app object in wsgi.py
|
||||||
|
#
|
||||||
|
# We set the following parameters:
|
||||||
|
#
|
||||||
|
# Parameter Environment Variable Default Value
|
||||||
|
# --------- -------------------- -------------
|
||||||
|
# number of workers MODEL_SERVER_WORKERS the number of CPU cores
|
||||||
|
# timeout MODEL_SERVER_TIMEOUT 60 seconds
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import multiprocessing
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
cpu_count = multiprocessing.cpu_count()
|
||||||
|
|
||||||
|
model_server_timeout = os.environ.get("MODEL_SERVER_TIMEOUT", 60)
|
||||||
|
model_server_workers = int(os.environ.get("MODEL_SERVER_WORKERS", cpu_count))
|
||||||
|
|
||||||
|
|
||||||
|
def sigterm_handler(nginx_pid, gunicorn_pid):
|
||||||
|
try:
|
||||||
|
os.kill(nginx_pid, signal.SIGQUIT)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
os.kill(gunicorn_pid, signal.SIGTERM)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
def start_server():
|
||||||
|
print(f"Starting server with {model_server_workers} workers.")
|
||||||
|
|
||||||
|
# link the log streams to stdout/err so that
|
||||||
|
# they will be logged to the container logs
|
||||||
|
subprocess.check_call(["ln", "-sf", "/dev/stdout", "/var/log/nginx/access.log"])
|
||||||
|
subprocess.check_call(["ln", "-sf", "/dev/stderr", "/var/log/nginx/error.log"])
|
||||||
|
|
||||||
|
nginx = subprocess.Popen(["nginx", "-c", "/opt/program/nginx.conf"])
|
||||||
|
gunicorn = subprocess.Popen(
|
||||||
|
[
|
||||||
|
"gunicorn",
|
||||||
|
"--timeout",
|
||||||
|
str(model_server_timeout),
|
||||||
|
"-k",
|
||||||
|
"gevent",
|
||||||
|
"-b",
|
||||||
|
"unix:/tmp/gunicorn.sock",
|
||||||
|
"-w",
|
||||||
|
str(model_server_workers),
|
||||||
|
"wsgi:app",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# NOTE: https://docs.python.org/3/library/signal.html#signal.signal
|
||||||
|
# This sets a handler (second argument) for the signal SIGTERM (first argument)
|
||||||
|
# in the function `signal.signal` which we want to call (unclear why...)
|
||||||
|
|
||||||
|
# The handler should be a callable which takes two arguments (hence `a`, `b`),
|
||||||
|
# and it is called with two arguments: the signal number and current stack frame.
|
||||||
|
# Neither of these are relevant to us, so we define a dummy handler which kills
|
||||||
|
# the two processes we have started (nginx and gunicorn) regardless of `a`/`b`.
|
||||||
|
|
||||||
|
# Basically, we are hard-coding "kill these two processes if you get a SIGTERM"
|
||||||
|
def handler(a, b, c):
|
||||||
|
return sigterm_handler(nginx.pid, gunicorn.pid)
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, handler)
|
||||||
|
|
||||||
|
# If either subprocess exits, so do we.
|
||||||
|
pids = set([nginx.pid, gunicorn.pid])
|
||||||
|
while True:
|
||||||
|
pid, _ = os.wait()
|
||||||
|
if pid in pids:
|
||||||
|
break
|
||||||
|
|
||||||
|
sigterm_handler(nginx.pid, gunicorn.pid)
|
||||||
|
print("API server exiting")
|
||||||
|
|
||||||
|
|
||||||
|
# The main routine just invokes the start function.
|
||||||
|
if __name__ == "__main__":
|
||||||
|
start_server()
|
36
templates/index.html
Normal file
36
templates/index.html
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<!-- Flask template for displaying the results of a SQL query as a basic website -->
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Minecraft Server Status</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>Minecraft Server Status</h1>
|
||||||
|
<div align="center">
|
||||||
|
<p style="font-size: 150%;">
|
||||||
|
{{ status|safe }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
<!-- inline style to center table elements and create spacing in each column-->
|
||||||
|
<style>
|
||||||
|
table, th, td {
|
||||||
|
border: 1px solid black;
|
||||||
|
border-collapse: collapse;
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
font-size: 125%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div align="center">
|
||||||
|
{{ data|safe }}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user