diff --git a/README.md b/README.md index 4cbd6da..3ebf6f4 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ Edit config.json to your needs. TODO For now there is only the file uplink.sql which will create the tables in your MariaDB/MySQL database. + The file is under files/uplink.sql + ## Usage ./uplink [-c | -d | -f] ./config.json @@ -98,10 +100,9 @@ There will be a Gtk+ frontend to uplink at some time but this project is at a ve - A lot... :) - Make all config vars as ARGS and vice versa. ARGS have higher priority. Chain: default -> config -> ARGS. - Bring back SQLite support. -- Switch to ORM (peewee, sqlalchemy?) to support PostgreSQL, MariaDB/MySQL, SQLite and more. +- Switch to ORM (peewee, sqlalchemy?) to support PostgreSQL, MariaDB/MySQL, SQLite and maybe more databases. - Gtk+/urwid frontend for visualizing the collected data. wxGlade? - A tool to generate reports for showing to your uplink provider. - Always keep platform independence in mind but not if uplink looses nice features on Linux. - Implement a speedtest feature to regularly run speedtest but independent to uptime checks with different interval. - A small embedded webserver to view what is going on. -- Real file based logging diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..49ec6fd --- /dev/null +++ b/__init__.py @@ -0,0 +1 @@ +from uplink import Uplink \ No newline at end of file diff --git a/bin/.gitkeep b/bin/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/bin/__init__.py b/bin/__init__.py new file mode 100644 index 0000000..62720d3 --- /dev/null +++ b/bin/__init__.py @@ -0,0 +1 @@ +import ../uplink/uplink diff --git a/files/.gitkeep b/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/config.example.json b/files/config.example.json similarity index 100% rename from config.example.json rename to files/config.example.json diff --git a/test.py b/files/test.py similarity index 100% rename from test.py rename to files/test.py diff --git a/uplink.playground.py b/files/uplink.playground.py old mode 100644 new mode 100755 similarity index 100% rename from uplink.playground.py rename to files/uplink.playground.py diff --git a/uplink.sql b/files/uplink.sql similarity index 100% rename from uplink.sql rename to files/uplink.sql diff --git a/uplink.py b/uplink.py index 567e916..fa50f39 100755 --- a/uplink.py +++ b/uplink.py @@ -35,8 +35,8 @@ from uplink.uplink import Uplink __version__ = "0.5.0-development" # TODO: CHECK ALL ERROR HANDLING!!! -# TODO: IDEA: Implement a small webserver inline to get statistics and graphs over the network? Or maybe better as a -# separate daemon +# TODO: IDEA: Implement a small webserver inline to get statistics and graphs over the network? +# Or maybe better as a separate daemon # TODO: Make use of more args passed to the script # TODO: Implement a speedtest that will also run regularly but in an individual interval @@ -79,7 +79,7 @@ def parse_args(): help="maximum number of logfiles in rotation (default: 5)") parser.add_argument("-m", "--logsize", default=10485760, type=int, - help="maximum logfile size in bytes (default: 10485760)") + help="maximum logfile size in bytes (default: 1048576)") parser.add_argument("-s", "--stdout", default=False, dest="stdout", action="store_true", help="log to stdout") @@ -127,7 +127,6 @@ def main(): root_logger.setLevel(args.loglevel) - # TODO: Cleanup config.json and only define what really is needed config_path = args.config try: with open(config_path, 'r', encoding='utf-8') as configfile: @@ -162,7 +161,7 @@ def main(): t.start() try: time.sleep(config["interval"]) - except KeyboardInterrupt as err: + except KeyboardInterrupt: logger.info(str("uplink: program terminated by user!")) sys.exit(0) else: diff --git a/uplink/__init__.py b/uplink/__init__.py index b5da72c..e69de29 100644 --- a/uplink/__init__.py +++ b/uplink/__init__.py @@ -1,5 +0,0 @@ -""" -uplink - -library for the uplink project -""" diff --git a/uplink/daemon.py b/uplink/daemon.py index ba57262..cb42453 100644 --- a/uplink/daemon.py +++ b/uplink/daemon.py @@ -1,4 +1,6 @@ """ +Generic linux daemon base class for python 3.x. + Original Source: https://web.archive.org/web/20131017130434/http://www.jejik.com/articles/2007/02/a_simple_unix_linux_daemon_in_python/ https://web.archive.org/web/20131017130434/http://www.jejik.com/files/examples/daemon3x.py @@ -9,40 +11,57 @@ https://franklingu.github.io/programming/2016/03/01/creating-daemon-process-pyth https://dpbl.wordpress.com/2017/02/12/a-tutorial-on-python-daemon/ """ +from logging import getLogger import sys import os import time import atexit import signal +logger = getLogger(__name__) + # TODO: Check why pid files are not being deleted when sending the TERM signal class Daemon: + """ + A generic daemon class. + + Usage: subclass the daemon class and override the run() method. + """ def __init__(self, pid_file): self.pid_file = pid_file def daemonize(self): + """ + Deamonize class. UNIX double fork mechanism. + """ + try: pid = os.fork() if pid > 0: + # exit first parent sys.exit(0) except OSError as err: - sys.stderr.write('fork #1 failed: {0}\n'.format(err)) + logger.error('fork #1 failed: {0}\n'.format(err)) sys.exit(1) + # decouple from parent environme os.chdir('/') os.setsid() os.umask(0) + # do second fork try: pid = os.fork() if pid > 0: + # exit from second parent sys.exit(0) except OSError as err: - sys.stderr.write('fork #2 failed: {0}\n'.format(err)) + logger.error('fork #2 failed: {0}\n'.format(err)) sys.exit(1) + # redirect standard file descriptors sys.stdout.flush() sys.stderr.flush() si = open(os.devnull, 'r') @@ -53,6 +72,7 @@ class Daemon: os.dup2(so.fileno(), sys.stdout.fileno()) os.dup2(se.fileno(), sys.stderr.fileno()) + # write pidfile atexit.register(self.del_pid) pid = str(os.getpid()) @@ -63,6 +83,11 @@ class Daemon: os.remove(self.pid_file) def start(self): + """ + Start the daemon. + """ + + # Check for a pidfile to see if the daemon already runs try: with open(self.pid_file, 'r') as pf: pid = int(pf.read().strip()) @@ -71,13 +96,19 @@ class Daemon: if pid: message = "pid_file {0} already exist. Daemon already running?\n" - sys.stderr.write(message.format(self.pid_file)) + logger.error(message.format(self.pid_file)) sys.exit(1) + # Start the daemon self.daemonize() self.run() def stop(self): + """ + Stop the daemon. + """ + + # Get the pid from the pidfile try: with open(self.pid_file, 'r') as pf: pid = int(pf.read().strip()) @@ -86,9 +117,11 @@ class Daemon: if not pid: message = "pid_file {0} does not exist. Daemon not running?\n" - sys.stderr.write(message.format(self.pid_file)) + logger.error(message.format(self.pid_file)) + # not an error in a restart return + # Try killing the daemon process try: while 1: os.kill(pid, signal.SIGTERM) @@ -99,14 +132,19 @@ class Daemon: if os.path.exists(self.pid_file): os.remove(self.pid_file) else: - print(str(err.args)) + logger.error(str(err.args)) sys.exit(1) def restart(self): + """ + Restart the daemon. + """ self.stop() self.start() def run(self): - """Override this method when you subclass Daemon. + """ + Override this method when you subclass Daemon. It will be called after the process has been daemonized by - start() or restart().""" + start() or restart(). + """