#!/usr/bin/env python3
# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Poliwall control.

Usage: %prog -w           # make the poliwall process write its buffers
       %prog -k           # kill the poliwall process

This should be run by root only (on Windows, by users with
Administrator privileges).

"""

import os
import sys
import errno
import signal
import logging
import optparse

is_win = os.name == "nt"

from sigma.util import avalog, config
from sigma.util.errors import usage
from sigma.sched import process

logger = logging.getLogger("poliwall")
options = config.Options()
pidfile = options.datapath("pwpid.txt")
pwflush = options.datapath("pwflush")
pwkill = options.datapath("pwkill")

parser = optparse.OptionParser(usage=__doc__.strip())
parser.add_option("-w", "--write", action="store_true", dest="write",
                  help="make the poliwall process write its buffers")
parser.add_option("-k", "--kill", action="store_true", dest="kill",
                  help="kill the poliwall process")

def main(args=None):
    # Parse arguments
    opts, args = parser.parse_args(args)
    if args:
        return usage("This program takes no arguments")
    if bool(opts.write) == bool(opts.kill):
        return usage("Please specify exactly one of -w and -k")

    # From here on all errors are logged
    avalog.setupLogging()

    # Check for pid file, read and parse its first line
    if not os.path.exists(pidfile):
        logger.error("The poliwall process doesn't appear to be running")
        return 1
    try:
        f = open(pidfile)
        try:
            pidtext = f.readline()
        finally:
            f.close()
    except IOError as err:
        logger.error("Can't read %s: %s", pidfile, err)
        return 1
    try:
        pid = int(pidtext)
    except ValueError as err:
        logger.error("Can't decode %s: %s", pidfile, err)
        return 1

    # Create a Process object and check that it's alive
    try:
        proc = process.Process(pid)
    except Exception as err:
        logger.error("Can't access process %s: %s", pid, err)
        remove_pidfile()
        return 1
    if not proc.isalive():
        logger.error("The poliwall process isn't running any more")
        remove_pidfile()
        return 1

    # Check for -w option
    if opts.write:
        # XXX This, plus some code in pwgather.py, really ought to be
        # placed in a utility library module rather than being kept in
        # various main programs.
        if is_win:
            try:
                oldmtime = os.path.getmtime(pwflush)
            except os.error:
                oldmtime = None
            try:
                f = open(pwflush, "w")
            except IOError as err:
                logger.error("Can't write to %s: %s", pwflush, err)
                return 1
            try:
                f.write("#\n") # Write something to touch it
            finally:
                f.close()
            if oldmtime is not None:
                newmtime = os.path.getmtime(pwflush)
                if oldmtime >= newmtime:
                    # The precision of mtime is seconds.  If we try to
                    # wake up the poliwall more than once in the same
                    # second, this won't work.  So fake a time that's
                    # one second later in that case.  Also in case the
                    # old mtime is already later than the new mtime
                    # (if this happens multiple times in the same
                    # second).  Note that we needn't handle multiple
                    # attempts in parallel, because of the locking
                    # used by pwgather.py (which is the only invoker
                    # of this code).
                    os.utime(pwflush, (oldmtime+1, oldmtime+1))
            logger.info("Touched %s", pwflush)
            return 0
        else:
            return send_sig(proc, "SIGUSR1")
    else:
        # Must be -k option
        assert opts.kill
        if is_win:
            try:
                f = open(pwkill, "w")
            except IOError as err:
                logger.error("Can't write to %s: %s", pwkill, err)
                return 1
            try:
                f.write("#\n") # Write something to touch it
            finally:
                f.close()
            logger.info("Touched %s", pwkill)
            remove_pidfile()
            return 0
        else:
            # This isn't invoked by the scheduler.
            return send_sig(proc, "SIGTERM")

def send_sig(proc, signame):
    # Unix-only helper to send a signal; used for SIGUSR1 and SIGTERM
    signum = getattr(signal, signame)
    pid = proc.getpid()
    try:
        proc.kill(signum)
    except os.error as err:
        if err.errno == errno.ESRCH:
            logger.error("The poliwall process isn't running any more")
            remove_pidfile()
            return 1
        logger.error("Can't send signal %s to process %s: %s",
                     signame, pid, err)
        return 1
    logger.info("Sent signal %s to process %s", signame, pid)
    return 0

def remove_pidfile():
    if not os.path.exists(pidfile):
        return
    try:
        os.remove(pidfile)
    except os.error as err:
        logger.debug("Can't remove pidfile %s: %s", pidfile, err)
    else:
        logger.info("Removed %s", pidfile)

if __name__ == "__main__":
    sys.exit(main())
