#!/usr/bin/python3
#
#    powerwaked - PowerNap Server Daemon
#
#    Copyright (C) 2011 Canonical Ltd.
#
#    Authors: Andres Rodriguez <andreserl@canonical.com>
#
#    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, version 3 of the License.
#
#    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/>.

# Imports
import argparse
import subprocess
import logging, logging.handlers
import os
import re
import signal
import sys
import time
import socket, traceback
import struct
from powerwake import powerwake

# Initialize powerwake. This initialization loads the config file.
try:
    powerwake = powerwake.PowerWake("/etc/powernap/powerwaked.conf")
except Exception as e:
    logging.error("Unable to initialize PowerNap Server")
    logging.exception(e)
    sys.exit(1)

# Define globals
global LOCK, CONFIG, MONITORS
LOCK = "/var/run/%s.pid" % powerwake.PKG

# Generic error function
def error(msg):
    logging.error(msg)
    sys.exit(1)

# Lock function, using a pidfile in /var/run
def establish_lock():
    if os.path.exists(LOCK):
        f = open(LOCK,'r')
        pid = f.read()
        f.close()
        error("Another instance is running [%s]" % pid)
    else:
        try:
            f = open(LOCK,'w')
        except:
            error("Administrative privileges are required to run %s" % powerwake.PKG);
        f.write(str(os.getpid()))
        f.close()
        # Set signal handlers
        signal.signal(signal.SIGINT, stop_signal)
        signal.signal(signal.SIGTERM, stop_signal)
        signal.signal(signal.SIGIO, signal.SIG_IGN)

running = True

# Clean up lock file on termination signals
def stop_signal(signal, frame):
    global running

    logging.info("Stopping %s" % powerwake.PKG)
    running = False

def powerwaked_loop():
    global running

    # Starting the Monitors
    for monitor in MONITORS:
        logging.debug("Starting [%s] Monitoring" % monitor._type)
        for ip, mac in monitor._arp_cache.items():
            logging.debug("  Monitoring %s - %s" % (ip, mac))
        monitor.start()

    while running:
        time.sleep(1)

    for monitor in MONITORS:
        monitor.stop()

# "Forking a Daemon Process on Unix" from The Python Cookbook
def daemonize (stdin="/dev/null", stdout="/dev/null", stderr="/dev/null"):
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError as e:
        error("fork #1 failed: (%d) %sn" % (e.errno, e.strerror))
    os.chdir("/")
    os.setsid()
    try:
        pid = os.fork()
        if pid > 0:
            sys.exit(0)
    except OSError as e:
        error("fork #2 failed: (%d) %sn" % (e.errno, e.strerror))
    f = open(LOCK,'w')
    f.write(str(os.getpid()))
    f.close()
    for f in sys.stdout, sys.stderr: f.flush()
    si = open(stdin, 'r')
    so = open(stdout, 'a+')
    se = open(stderr, 'ba+', 0)
    os.dup2(si.fileno(), sys.stdin.fileno())
    os.dup2(so.fileno(), sys.stdout.fileno())
    os.dup2(se.fileno(), sys.stderr.fileno())


# Main program
if __name__ == '__main__':
    arg_parser = argparse.ArgumentParser()
    arg_parser.add_argument("-d", "--daemon", action = "store_true", help = "Detatch and run as a background (daemon) process")
    arg_parser.add_argument("-D", "--debug", action = "store_true",  help = "Enable debug log messages")

    args = arg_parser.parse_args()

    log_handlers = []

    if powerwake.LOG == "syslog":
        syslog = logging.handlers.SysLogHandler(address = '/dev/log', facility = logging.handlers.SysLogHandler.LOG_DAEMON)
        syslog.ident = "powernapd: "
        log_handlers.append(syslog)
    elif powerwake.LOG != "":
        # Log file specified in configuration
        file_handler = logging.handlers.WatchedFileHandler(powerwake.LOG)
        file_handler.setFormatter(logging.Formatter(
            fmt = '%(asctime)s %(levelname)-8s %(message)s',
            datefmt = '%Y-%m-%d_%H:%M:%S'))

        log_handlers.append(file_handler)

    if not args.daemon:
        # Output log to console when not running as a daemon.
        log_handlers.append(logging.StreamHandler())

    logging.basicConfig(
        handlers = log_handlers,
        level = logging.DEBUG if powerwake.DEBUG or args.debug else logging.INFO)

    logging.captureWarnings(True)

    try:
        # Ensure that only one instance runs
        establish_lock()

        if args.daemon:
            daemonize()

        # Run the main powernapd loop
        MONITORS = powerwake.get_monitors()
        logging.info("Starting %s" % powerwake.PKG)
        powerwaked_loop()
    except Exception:
        logging.exception("Uncaught exception")
    finally:
        # Clean up the lock file
        if os.path.exists(LOCK):
            os.remove(LOCK)
