#!/usr/bin/python3
#
#    powernapd - monitor a system process table; if IDLE amount of time
#               goes by with no MONITORED_PROCESSES running, run ACTION
#
#    Copyright (C) 2009-2011 Canonical Ltd.
#
#    Authors: Dustin Kirkland <kirkland@canonical.com>
#             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 logging, logging.handlers
import os
import re
import signal
import sys
import time

from powernap import powernap
from powernap.ActionManager import ActionManager

# Initialize Powernap. This initialization loads the config file.
try:
    powernap = powernap.PowerNap()
    #os.putenv("ACTION_METHOD", str(powernap.ACTION_METHOD))
except:
    print("Unable to initialize PowerNap")
    sys.exit(1)

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

SLEEPING = False

# Generic (fatal) 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" % powernap.PKG);
        f.write(str(os.getpid()))
        f.close()
        # Set signal handlers
        signal.signal(signal.SIGHUP, signal_handler)
        signal.signal(signal.SIGINT, signal_handler)
        signal.signal(signal.SIGQUIT, signal_handler)
        signal.signal(signal.SIGTERM, signal_handler)
        signal.signal(signal.SIGIO, signal.SIG_IGN)
        #signal.signal(signal.SIGIO, input_singal_handler)

        signal.signal(signal.SIGUSR1, sleep_handler)
        signal.signal(signal.SIGUSR2, wake_handler)

# Clean up lock file on termination signals
def signal_handler(signal, frame):
    if os.path.exists(LOCK):
        os.remove(LOCK)
    logging.info("Stopping %s" % powernap.PKG)
    sys.exit(1)

def sleep_handler(signal, frame):
    global SLEEPING

    logging.info("Received SIGUSR1 - system is going to sleep")
    SLEEPING = True

def wake_handler(signal, frame):
    global SLEEPING

    logging.info("Received SIGUSR1 - system has resumed")
    SLEEPING = False

def powernapd_loop():
    global SLEEPING

    # Starting the Monitors
    for monitor in MONITORS:
        # logging.debug("Starting [%s:%s]" % (monitor._type, monitor._name))
        monitor.start()

    action_manager = ActionManager(powernap.config["actions"], powernap.ACTIONS_PATH)
    action_manager.update(True, time.monotonic())

    was_sleeping = False

    while 1:
        #if powernap.WATCH_CONFIG == True:
            #if watch_config_timestamp != os.stat(powernap.CONFIG).st_mtime:
                ## TODO: This only reloads general settings. Does not restart monitors.
                ## In the future, this should also stop/start monitors when reloading.
                #logging.warning("Reloading configuration file")
                #powernap.load_config_file()
                #watch_config_timestamp = os.stat(powernap.CONFIG).st_mtime
        logging.debug("Sleeping [%d] seconds" % powernap.INTERVAL_SECONDS)
        time.sleep(powernap.INTERVAL_SECONDS)

        if SLEEPING:
            was_sleeping = True
        else:
            activity_detected = False

            for monitor in MONITORS:
                #logging.debug("  Looking for [%s] %s" % (monitor._name, monitor._type))
                if monitor.active():
                    activity_detected = True

            # If the system just woke up from suspend, ensure ActionManager
            # sees some activity so we don't immediately go back to sleep.
            if was_sleeping:
                activity_detected = True
                was_sleeping = False

            action_manager.update(activity_detected, time.monotonic())


# "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 powernap.LOG == "syslog":
        syslog = logging.handlers.SysLogHandler(address = '/dev/log', facility = logging.handlers.SysLogHandler.LOG_DAEMON)
        syslog.ident = "powernapd: "
        log_handlers.append(syslog)
    elif powernap.LOG != None:
        # Log file specified in configuration
        file_handler = logging.handlers.WatchedFileHandler(powernap.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 powernap.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 = powernap.get_monitors()
        logging.info("Starting %s" % powernap.PKG)
        powernapd_loop()
    except Exception:
        logging.exception("Uncaught exception")
    finally:
        # Clean up the lock file
        if os.path.exists(LOCK):
            os.remove(LOCK)
