# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""XML-RPC server interface for the scheduler."""

# Python imports
import os
import random
import socket
import logging
from io import StringIO
import socketserver as SocketServer
import xmlrpc.server as SimpleXMLRPCServer

# Sigma imports
from sigma.util import config
from sigma.sched import tasks, webserver

# Global objects
logger = logging.getLogger("sched.server")

class SchedulerServer(SocketServer.ThreadingMixIn,
                      SimpleXMLRPCServer.SimpleXMLRPCServer):
    # This class exists so we can override allow_reuse_address
    allow_reuse_address = True

    def __init__(self, address, sched, options=None, **kwds):
        SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(
            self, address, webserver.MyRequestHandler, **kwds)
        self.set_cloexec()
        self.handler = XMLRPCHandler(self, sched, options)
        self.register_function(self.handler.ping)
        self.register_function(self.handler.shutdown)
        self.register_function(self.handler.restart)
        self.register_function(self.handler.add)
        self.register_function(self.handler.listing)

    def set_cloexec(self):
        try:
            from fcntl import fcntl, F_SETFD, FD_CLOEXEC
        except (ImportError, AttributeError):
            pass # Not on Unix
        else:
            fcntl(self.socket.fileno(), F_SETFD, FD_CLOEXEC)

    def serve_until_stopped(self):
        while not self.handler.exit:
            self.handle_request()

    def force_exit(self):
        try:
            s = socket.socket()
            s.connect((self.server_address))
            s.close()
        except socket.error as err:
            pass

    def server_close(self):
        SimpleXMLRPCServer.SimpleXMLRPCServer.server_close(self)
        self.handler.exit = True # For the PingPong thread

class XMLRPCHandler(object):

    def __init__(self, server, schedule, options=None):
        self.server = server
        self.schedule = schedule
        if options is None:
            options = config.Options()
        self.options = options
        self.exit = False
        self.set_password()

    def ping(self, password):
        """RPC Method for responding to ping messages.

        Returns uptime information about the scheduler as a status
        message in the form of a tuple as returned by os.times().

        """
        self.checkpassword(password)
        # os.times() returns a plain tuple on Py2 but a posix.times_result
        # structseq on Py3, which xmlrpc.client cannot marshal ("cannot marshal
        # <class 'posix.times_result'> objects").  Coerce back to a plain tuple
        # so the localhost self-ping keeps working under Py3.  (This is the
        # internal scheduler RPC, not the ESP wire protocol.)
        return tuple(os.times())

    def shutdown(self, password):
        "RPC Method for shutting down the server."
        self.checkpassword(password)
        logger.info("Shutting down scheduler")
        n = self.schedule.shutdown()
        self.exit = True
        self.server.force_exit()
        return n

    def restart(self, password):
        "RPC Method for restarting the server."
        self.checkpassword(password)
        logger.info("Restarting scheduler")
        self.exit = True
        self.server.force_exit()
        return self.schedule.restart()

    def add(self, password, task_xml):
        "Adds, replaces or removes tasks, and persists it to disk"
        self.checkpassword(password)
        task_list = tasks.parse_tasks(task_xml)
        count = self.schedule.add_list(task_list)
        self.schedule.save()
        return count

    def listing(self, password):
        "Returns a listing of the schedule (in XML)."
        self.checkpassword(password)
        f = StringIO()
        self.schedule.write(f, details=True)
        return f.getvalue()

    def checkpassword(self, password):
        if password != self.password:
            raise RuntimeError("invalid password")

    def set_password(self):
        self.password = str(random.random())
        pwfile = self.options.datapath(".sched.passwd")
        if os.path.exists(pwfile):
            try:
                os.remove(pwfile)
            except os.error as err:
                logger.error("Can't remove %s: %s", pwfile, err)
        f = open(pwfile, "w")
        f.close()
        os.chmod(pwfile, 0o600)
        f = open(pwfile, "w")
        f.write(self.password + "\n")
        f.close()
        logger.info("Scheduler password created")
