qmsk/net/lib/event2/base.pyx
author Tero Marttila <terom@fixme.fi>
Sat, 26 Sep 2009 23:38:02 +0300
changeset 56 07ed878c847b
parent 51 c6b4abfc21da
permissions -rw-r--r--
doctweaks and some skeleton tests for event-aliveness
from qmsk.net.lib.event2.event cimport build_timeout, event
from qmsk.net.socket.platform cimport AF_UNIX, SOCK_STREAM
cimport qmsk.net.py as py

class EventError (Exception) :
    """
        Base class of errors raised by lib.event2 code
    """

    pass

class EventCallError (EventError) :
    """
        Some libevent function returned an error code.
    """
    
    def __init__ (self, func, msg=None) :
        super(EventCallError, self).__init__(func)

        self.func = func
        self.msg = msg

cdef lib.event* build_pysignal_ev (lib.event_base *ev_base, lib._ev_callback_t cb_func, void *cb_arg) except NULL :
    """
        This constructs an returns an event* which will activate the given callback when a Python signal handler runs.

        The event is constructed as a persistent internal event, which means that it *must* be removed before trying to
        release the event_base.
    """

    cdef lib.evutil_socket_t fds[2]

    # construct the pipe/sockpair
    if lib.evutil_socketpair(AF_UNIX, SOCK_STREAM, 0, fds) < 0 :
        raise EventCallError("evutil_socketpair(signals)")

    # prep: nonblocking
    # XXX: close-on-exec?
    lib.evutil_make_socket_nonblocking(fds[0])
    lib.evutil_make_socket_nonblocking(fds[1])

    # build event for one end
    cdef lib.event *ev = lib.event_new(ev_base, fds[1], lib.EV_READ | lib.EV_PERSIST, cb_func, cb_arg)
    
    # set magic flag to not count as active
    ev.ev_flags |= lib.EVLIST_INTERNAL

    if ev == NULL :
        raise EventCallError("event_new(signals)")

    # register wakeup fd
    py.PySignal_SetWakeupFd(fds[0])

    return ev


cdef void on_pysignal_wakeup (int sig, short what, void *arg) :
    """
        Python's signal handler trapped a signal, we need to run the signal handlers.

        This is executed by libevent's event_base_loop as a normal event (using the wakeup fd built using
        build_pysignal_ev), and hence the signal handlers will be run from "inside" of event_base.loop(). Exceptions
        raised by the signal handlers should be propagated "out" of event_base.loop().
    """

    cdef event_base ev_base = <event_base> arg

    try :
        # run the real signal handlers for any pending signals
        py.PyErr_CheckSignals()

    except :
        # TODO: implement propagate-to-event_base.loop()
        # drop out of the event loop
        ev_base.loopbreak()

        # Cython will log it
        raise
        

cdef class event_base :

    def __init__ (self) :
        """
            Constructs a new event_base with default parameters.
        """

        # construct
        self.ev_base = lib.event_base_new()

        if self.ev_base == NULL :
            raise EventCallError("event_base_new")
        
        # internal signal wakeup handler
        # XXX: only the main event_base really needs one...
        self.ev_pysignal = build_pysignal_ev(self.ev_base, on_pysignal_wakeup, <void *> self)
            
        # XXX: this will keep the loop active?
        if lib.event_add(self.ev_pysignal, NULL) < 0 :
            raise EventCallError("event_add(signals)")


    def reinit (self) :
        """
            Re-initialize the event_base following a fork(). This is required for some event mechanisms.

            Raises an error if some events could not be re-added.
        """

        if lib.event_reinit(self.ev_base) < 0 :
            raise EventCallError("event_reinit", "could not re-add all events")


    property method :
        def __get__ (self) :
            """
                Query the underlying method used by this event_base as a string ("kqueue", "epoll", etc.)
            """

            return lib.event_base_get_method(self.ev_base)


    def loop (self, once = False, nonblock = False) :
        """
            Run the event loop.

                once        - only run the event loop once, at most
                nonblock    - do not block waiting for events

            Returns True if the event loop exited, False if there were no more events to process
        """

        cdef int flags = 0

        # build flags
        if once :
            flags |= lib.EVLOOP_ONCE

        if nonblock :
            flags |= lib.EVLOOP_NONBLOCK
        

        # event_base_loop()
        ret = lib.event_base_loop(self.ev_base, flags)

        
        # check return status
        if ret < 0 :
            raise EventCallError("event_base_loop")

        elif ret > 0 :
            return False

        else :
            return True


    def loopexit (self, timeout = None) :
        """
            Exit the event loop normally.

            This will optionally wait until the given timeout expires, and then allow the current event loop iteration
            to complete normally, before causing .loop() to return.
        """

        cdef lib.timeval tv

        if lib.event_base_loopexit(self.ev_base, build_timeout(&tv, timeout)) < 0 :
            raise EventCallError("event_base_loopexit")


    def loopbreak (self) :
        """
            Abort the event immediately.

            The current event loop will end and .loop() return as soon as the currently running event has been
            processed.
        """

        if lib.event_base_loopbreak(self.ev_base) < 0 :
            raise EventCallError("event_base_loopbreak")


    def __dealloc__ (self) :
        """
            Release the event_base's resources.

            XXX: is this entirely safe re our events?
        """
        
        if self.ev_pysignal :
            # remove the internal signal wakeup event
            lib.event_free(self.ev_pysignal)

        lib.event_base_free(self.ev_base)