Ticket #14079: 14079_pselecter.patch

File 14079_pselecter.patch, 15.4 KB (added by jdemeyer, 9 years ago)
  • doc/en/reference/libs/index.rst

    # HG changeset patch
    # User Jeroen Demeyer <jdemeyer@cage.ugent.be>
    # Date 1361578610 -3600
    # Node ID 5984cf199489dee6408dc21c09f3127c1c5d62b1
    # Parent  a104458275e71f8c45dc3f8729e22659981598ec
    Cython interface to pselect() system call
    
    diff --git a/doc/en/reference/libs/index.rst b/doc/en/reference/libs/index.rst
    a b  
    2828   sage/libs/flint/fmpz_poly
    2929   sage/libs/libecm
    3030   sage/libs/lrcalc/lrcalc
    31    sage/libs/pari/gen
    3231   sage/libs/ntl/all
    3332   sage/libs/mwrank/all
    3433   sage/libs/mwrank/mwrank
    3534   sage/libs/mwrank/interface
     35   sage/libs/pari/gen
     36   sage/libs/ppl
     37   sage/ext/pselect
    3638   sage/libs/singular/function
    3739   sage/libs/singular/option
    38    sage/libs/ppl
    3940
    4041.. include:: ../footer.txt
  • module_list.py

    diff --git a/module_list.py b/module_list.py
    a b  
    266266              sources = ['sage/crypto/boolean_function.pyx']),
    267267
    268268    ################################
    269     ## 
     269    ##
    270270    ## sage.ext
    271271    ##
    272272    ################################
     
    286286    Extension('sage.ext.multi_modular',
    287287              sources = ['sage/ext/multi_modular.pyx'],
    288288              extra_compile_args = ['-std=c99'],
    289               libraries=['gmp']),
     289              libraries=['gmp']),
     290
     291    Extension('sage.ext.pselect',
     292              sources = ['sage/ext/pselect.pyx']),
    290293
    291294    ################################
    292     ## 
     295    ##
    293296    ## sage.finance
    294297    ##
    295298    ################################
  • new file sage/ext/pselect.pyx

    diff --git a/sage/ext/pselect.pyx b/sage/ext/pselect.pyx
    new file mode 100644
    - +  
     1"""
     2Interface to the ``pselect()`` system call
     3
     4This module defines a class :class:`PSelecter` which can be used to
     5call the system call ``pselect()`` and which can also be used in a
     6``with`` statement to block given signals until
     7:meth:`PSelecter.pselect` is called.
     8
     9AUTHORS:
     10
     11- Jeroen Demeyer (2013-02-07): initial version (:trac:`14079`)
     12
     13Waiting for subprocesses
     14------------------------
     15
     16One possible use is to wait with a **timeout** until **any child process**
     17exits, as opposed to ``os.wait()`` which doesn't have a timeout or
     18``multiprocessing.Process.join()`` which waits for one specific process.
     19
     20Since ``SIGCHLD`` is ignored by default, we first need to install a
     21signal handler for ``SIGCHLD``. It doesn't matter what it does, as long
     22as the signal isn't ignored::
     23
     24    sage: import signal
     25    sage: def dummy_handler(sig, frame):
     26    ....:     pass
     27    ....:
     28    sage: _ = signal.signal(signal.SIGCHLD, dummy_handler)
     29
     30We wait for a child created using the ``subprocess`` module::
     31
     32    sage: from sage.ext.pselect import PSelecter
     33    sage: from subprocess import *
     34    sage: with PSelecter([signal.SIGCHLD]) as sel:
     35    ....:     p = Popen(["sleep", "1"])
     36    ....:     _ = sel.sleep()
     37    ....:
     38    sage: p.poll()  # p should be finished
     39    0
     40
     41Now using the ``multiprocessing`` module::
     42
     43    sage: from sage.ext.pselect import PSelecter
     44    sage: from multiprocessing import *
     45    sage: import time
     46    sage: with PSelecter([signal.SIGCHLD]) as sel:
     47    ....:     p = Process(target=time.sleep, args=(1,))
     48    ....:     p.start()
     49    ....:     _ = sel.sleep()
     50    ....:     p.is_alive()  # p should be finished
     51    ....:
     52    False
     53"""
     54#*****************************************************************************
     55#       Copyright (C) 2013 Jeroen Demeyer <jdemeyer@cage.ugent.be>
     56#
     57#  Distributed under the terms of the GNU General Public License (GPL)
     58#  as published by the Free Software Foundation; either version 2 of
     59#  the License, or (at your option) any later version.
     60#                  http://www.gnu.org/licenses/
     61#*****************************************************************************
     62
     63include "cdefs.pxi"
     64include "signals.pxi"
     65cimport libc.errno
     66
     67
     68cpdef int get_fileno(f) except -1:
     69    """
     70    Return the file descriptor of ``f``.
     71
     72    INPUT:
     73
     74    - ``f`` -- an object with a ``.fileno`` method or an integer,
     75      which is a file descriptor.
     76
     77    OUTPUT: A C ``long`` representing the file descriptor.
     78
     79    EXAMPLES::
     80
     81        sage: from sage.ext.pselect import get_fileno
     82        sage: get_fileno(open(os.devnull))  # random
     83        5
     84        sage: get_fileno(42)
     85        42
     86        sage: get_fileno(None)
     87        Traceback (most recent call last):
     88        ...
     89        TypeError: an integer is required
     90        sage: get_fileno(-1)
     91        Traceback (most recent call last):
     92        ...
     93        ValueError: Invalid file descriptor
     94        sage: get_fileno(2^30)
     95        Traceback (most recent call last):
     96        ...
     97        ValueError: Invalid file descriptor
     98    """
     99    cdef int n
     100    try:
     101        n = f.fileno()
     102    except AttributeError:
     103        n = f
     104    if n < 0 or n >= FD_SETSIZE:
     105        raise ValueError("Invalid file descriptor")
     106    return n
     107
     108
     109cdef class PSelecter:
     110    """
     111    This class gives an interface to the ``pselect`` system call.
     112   
     113    It can be used in a ``with`` statement to block given signals
     114    such that they can only occur during the :meth:`pselect()` or
     115    :meth:`sleep()` calls.
     116
     117    As an example, we block the ``SIGHUP`` and ``SIGINT`` signals and
     118    then raise a ``SIGINT`` signal. The interrupt will only be seen
     119    during the :meth:`sleep` call::
     120
     121        sage: from sage.ext.pselect import PSelecter
     122        sage: import signal, time
     123        sage: with PSelecter([signal.SIGHUP, signal.SIGINT]) as sel:
     124        ....:     os.kill(os.getpid(), signal.SIGINT)
     125        ....:     time.sleep(0.5)  # Simply sleep, no interrupt detected
     126        ....:     try:
     127        ....:         _ = sel.sleep(1)  # Interrupt seen here
     128        ....:     except KeyboardInterrupt:
     129        ....:         print("Interrupt OK")
     130        ....:
     131        Interrupt OK
     132
     133    .. WARNING::
     134
     135        If ``SIGCHLD`` is blocked inside the ``with`` block, then you
     136        should not use ``Popen().wait()`` or ``Process().join()``
     137        because those might block, even if the process has actually
     138        exited. Use non-blocking alternatives such as ``Popen.poll()``
     139        or ``multiprocessing.active_children()`` instead.
     140    """
     141    cdef sigset_t oldset
     142    cdef sigset_t blockset
     143
     144    def __cinit__(self):
     145        """
     146        Store old signal mask, needed if this class is used *without*
     147        a ``with`` statement.
     148
     149        EXAMPLES::
     150
     151            sage: from sage.ext.pselect import PSelecter
     152            sage: PSelecter()
     153            <sage.ext.pselect.PSelecter ...>
     154        """
     155        cdef sigset_t emptyset
     156        sigemptyset(&emptyset)
     157        sigprocmask(SIG_BLOCK, &emptyset, &self.oldset)
     158
     159    def __init__(self, block=[]):
     160        """
     161        Store list of signals to block during ``pselect()``.
     162
     163        EXAMPLES::
     164
     165            sage: from sage.ext.pselect import PSelecter
     166            sage: from signal import *
     167            sage: PSelecter([SIGINT, SIGSEGV])
     168            <sage.ext.pselect.PSelecter ...>
     169        """
     170        sigemptyset(&self.blockset)
     171        for sig in block:
     172            sigaddset(&self.blockset, sig)
     173
     174    def __enter__(self):
     175        """
     176        Block signals chosen during :meth:`__init__` in this ``with`` block.
     177
     178        OUTPUT: ``self``
     179
     180        TESTS:
     181
     182        Test nesting, where the inner ``with`` statements should have no
     183        influence, in particular they should not unblock signals which
     184        were already blocked upon entering::
     185
     186            sage: from sage.ext.pselect import PSelecter
     187            sage: import signal
     188            sage: with PSelecter([signal.SIGINT]) as sel:
     189            ....:     os.kill(os.getpid(), signal.SIGINT)
     190            ....:     with PSelecter([signal.SIGFPE]) as sel2:
     191            ....:         _ = sel2.sleep(0.1)
     192            ....:     with PSelecter([signal.SIGINT]) as sel3:
     193            ....:         _ = sel3.sleep(0.1)
     194            ....:     try:
     195            ....:         _ = sel.sleep(0.1)
     196            ....:     except KeyboardInterrupt:
     197            ....:         print("Interrupt OK")
     198            ....:
     199            Interrupt OK
     200        """
     201        sigprocmask(SIG_BLOCK, &self.blockset, &self.oldset)
     202        return self
     203
     204    def __exit__(self, type, value, traceback):
     205        """
     206        Reset signal mask to what it was before :meth:`__enter__`.
     207
     208        EXAMPLES:
     209
     210        Install a ``SIGCHLD`` handler::
     211
     212            sage: import signal
     213            sage: def child_handler(sig, frame):
     214            ....:     global got_child
     215            ....:     got_child = 1
     216            ....:
     217            sage: _ = signal.signal(signal.SIGCHLD, child_handler)
     218            sage: got_child = 0
     219
     220        Start a process which will cause a ``SIGCHLD`` signal::
     221
     222            sage: import time
     223            sage: from multiprocessing import *
     224            sage: from sage.ext.pselect import PSelecter
     225            sage: w = PSelecter([signal.SIGCHLD])
     226            sage: with w:
     227            ....:     p = Process(target=time.sleep, args=(0.25,))
     228            ....:     t0 = walltime()
     229            ....:     p.start()
     230            ....:
     231
     232        This ``sleep`` should be interruptible now::
     233
     234            sage: time.sleep(1)
     235            sage: t = walltime(t0)
     236            sage: (0.2 <= t <= 0.9) or t
     237            True
     238            sage: got_child
     239            1
     240            sage: p.join()
     241        """
     242        sigprocmask(SIG_SETMASK, &self.oldset, NULL)
     243
     244    def pselect(self, rlist=[], wlist=[], xlist=[], timeout=None):
     245        """
     246        Wait until one of the given files is ready, or a signal has
     247        been received, or until ``timeout`` seconds have past.
     248
     249        INPUT:
     250
     251        - ``rlist`` -- (default: ``[]``) a list of files to wait for
     252          reading.
     253
     254        - ``wlist`` -- (default: ``[]``) a list of files to wait for
     255          writing.
     256
     257        - ``xlist`` -- (default: ``[]``) a list of files to wait for
     258          exceptions.
     259
     260        - ``timeout`` -- (default: ``None``) a timeout in seconds,
     261          where ``None`` stands for no timeout.
     262
     263        OUTPUT: A 4-tuple ``(rready, wready, xready, tmout)`` where the
     264        first three are lists of file descriptors which are ready,
     265        that is a subset of ``(rlist, wlist, xlist)``. The fourth is a
     266        boolean which is ``True`` if and only if the command timed out.
     267        If ``pselect`` was interrupted by a signal, the output is
     268        ``([], [], [], False)``.
     269
     270        .. SEEALSO::
     271
     272            Use the :meth:`sleep` method instead if you don't care about
     273            file descriptors.
     274
     275        EXAMPLES:
     276
     277        The file ``/dev/null`` should always be available for reading
     278        and writing::
     279
     280            sage: from sage.ext.pselect import PSelecter
     281            sage: f = open(os.devnull, "r+")
     282            sage: sel = PSelecter()
     283            sage: sel.pselect(rlist=[f])
     284            ([<open file '/dev/null', mode 'r+' at ...>], [], [], False)
     285            sage: sel.pselect(wlist=[f])
     286            ([], [<open file '/dev/null', mode 'r+' at ...>], [], False)
     287
     288        A list of various files, all of them should be ready for
     289        reading. Also create a pipe, which should be ready for
     290        writing, but not reading (since nothing has been written)::
     291
     292            sage: f = open(os.devnull, "r")
     293            sage: g = open(os.path.join(SAGE_LOCAL, 'bin', 'python'), "r")
     294            sage: (pr, pw) = os.pipe()
     295            sage: r, w, x, t = PSelecter().pselect([f,g,pr,pw], [pw], [pr,pw])
     296            sage: len(r), len(w), len(x), t
     297            (2, 1, 0, False)
     298
     299        Checking for exceptions on the pipe should simply time out::
     300
     301            sage: sel.pselect(xlist=[pr,pw], timeout=0.2)
     302            ([], [], [], True)
     303
     304        TESTS:
     305
     306        It is legal (but silly) to list the same file multiple times::
     307
     308            sage: r, w, x, t = PSelecter().pselect([f,g,f,f,g])
     309            sage: len(r)
     310            5
     311
     312        Invalid input::
     313
     314            sage: PSelecter().pselect([None])
     315            Traceback (most recent call last):
     316            ...
     317            TypeError: an integer is required
     318
     319        Open a file and close it, but save the (invalid) file
     320        descriptor::
     321
     322            sage: f = open(os.devnull, "r")
     323            sage: n = f.fileno()
     324            sage: f.close()
     325            sage: PSelecter().pselect([n])
     326            Traceback (most recent call last):
     327            ...
     328            IOError: ...
     329        """
     330        # Convert given lists to fd_set
     331        cdef fd_set rfds, wfds, xfds
     332        FD_ZERO(&rfds)
     333        FD_ZERO(&wfds)
     334        FD_ZERO(&xfds)
     335
     336        cdef int nfds = 0
     337        cdef int n
     338        for f in rlist:
     339            n = get_fileno(f)
     340            if (n >= nfds): nfds = n + 1
     341            FD_SET(n, &rfds)
     342        for f in wlist:
     343            n = get_fileno(f)
     344            if (n >= nfds): nfds = n + 1
     345            FD_SET(n, &wfds)
     346        for f in xlist:
     347            n = get_fileno(f)
     348            if (n >= nfds): nfds = n + 1
     349            FD_SET(n, &xfds)
     350
     351        cdef double tm
     352        cdef timespec tv
     353        cdef timespec *ptv = NULL
     354        cdef int ret
     355        if timeout is not None:
     356            tm = timeout
     357            if tm < 0:
     358                tm = 0
     359            tv.tv_sec = <long>tm
     360            tv.tv_nsec = <long>(1e9 * (tm - <double>tv.tv_sec))
     361            ptv = &tv
     362
     363        ret = pselect(nfds, &rfds, &wfds, &xfds, ptv, &self.oldset)
     364        # No file descriptors ready => timeout
     365        if ret == 0:
     366            return ([], [], [], True)
     367
     368        # Error?
     369        cdef int err
     370        if ret < 0:
     371            # Save this value in case Python statements change it.
     372            err = libc.errno.errno
     373            if err == libc.errno.EINTR:
     374                return ([], [], [], False)
     375            import os
     376            if err == libc.errno.EBADF:
     377                raise IOError(err, os.strerror(err))
     378            else:
     379                raise OSError(err, os.strerror(err))
     380
     381        # Figure out which file descriptors to return
     382        rready = []
     383        wready = []
     384        xready = []
     385        for f in rlist:
     386            n = get_fileno(f)
     387            if FD_ISSET(n, &rfds):
     388                rready.append(f)
     389        for f in wlist:
     390            n = get_fileno(f)
     391            if FD_ISSET(n, &wfds):
     392                wready.append(f)
     393        for f in xlist:
     394            n = get_fileno(f)
     395            if FD_ISSET(n, &xfds):
     396                xready.append(f)
     397
     398        return (rready, wready, xready, False)
     399
     400    def sleep(self, timeout=None):
     401        """
     402        Wait until a signal has been received, or until ``timeout``
     403        seconds have past.
     404
     405        This is implemented as a special case of :meth:`pselect` with
     406        empty lists of file descriptors.
     407
     408        INPUT:
     409
     410        - ``timeout`` -- (default: ``None``) a timeout in seconds,
     411          where ``None`` stands for no timeout.
     412
     413        OUTPUT: A boolean which is ``True`` if the call timed out,
     414        False if it was interrupted.
     415
     416        EXAMPLES:
     417
     418        A simple wait with timeout::
     419
     420            sage: from sage.ext.pselect import PSelecter
     421            sage: sel = PSelecter()
     422            sage: sel.sleep(timeout=0.1)
     423            True
     424
     425        0 or negative time-outs are allowed, ``sleep`` should then
     426        return immediately::
     427
     428            sage: sel.sleep(timeout=0)
     429            True
     430            sage: sel.sleep(timeout=-123.45)
     431            True
     432        """
     433        return self.pselect(timeout=timeout)[3]