# lyxserver.py            -*- coding: iso-8859-1 -*-
# Copyright (c) 2005 Günter Milde
# Released under the terms of the GNU General Public License (ver. 2 or later)
"""
Functions and classes to connect to LyX via the lyxserver pipes

   filter_options  parse list of command line arguments and filter lyx options

   start_lyx()     start a LyX session in a separate process
                   
   lyxpipe()       open a lyxpipe for nonblocking access
                   
   LyXServer       class for communication with the lyxserver

Needs the lyxserver running 
     Set Edit>Preferences>Paths>Server-pipe to "~/.lyx/lyxpipe"
     (set the variable SERVERPIPE in ~/.lyx/scripts/python/config.py if you
      prefer a different path)
      
Notes
-----
   
   * Server documentation (slightly outdated) is available at
      LYXDIR/doc/Customization.lyx (but not in the German translation)
   
   * Test the lyxserver with lyx -dbg 4096.
   
   * A full list of lyx functions (LFUNS) is available at 
     http://wiki.lyx.org/pmwiki.php/LyX/LyxFunctions 
     and in the file `doc/lfuns.txt` of this package
"""

import os, sys, time, select, logging

from constants import LYXCMD, SERVERPIPE, LOG_LEVEL,\
LYXSERVER_POLL_TIMEOUT, LYXSERVER_SETUP_DELAY, LYXSERVER_SETUP_RETRIES

# Setup
# -----

# set up the logger instance
logger = logging.getLogger("lyxserver")
logging.basicConfig()
#set verbosity to show all messages of severity >= LOG_LEVEL
logger.setLevel(LOG_LEVEL) 

# Auxiliary functions
# -------------------

def start_lyx(cmd=LYXCMD, args=[]):
    """start a LyX session in a separate process
    
    cmd     -- lyx binary command name (searched on PATH)
    args    -- list of command parameters (excluding the command name)
               (e.g. sys.argv[1:])
    """
    # parse cmd for args and move to argv
    argv = cmd.split(' ') + args
    cmd = argv[0]
    logger.info("starting %s"%" ".join(argv))
    return os.spawnvp(os.P_NOWAIT, cmd, argv)


# open a server pipe
def lyxpipe(path=SERVERPIPE, mode='w', lyxcmd=None):
    """open a lyxpipe for nonblocking access, return file object
    
    path    -- path to the lyx-server pipes (without .in and .out)
               must correspond to the setting in Edit>Preferences>Paths
    
    mode    -- 'w'   write access to `path` +'.in'
               'r'   read access from `path` +'.out' 
               'w+'  read-write to `path` +'.in'
               'r+'  read-write to `path` +'.out'
    
    lyxcmd  -- command to start LyX if the lyxpipes are not open,
               e.g. lyxcmd='lyx -dbg 4096'
    
               Special cases: 
    
               lyxcmd=None     do not start a lyx session 
                               (non-open pipes will immediately 
                               return an OSError).
    
               lyxcmd=''       do not start a lyx session
                               (retry connecting to the pipes
                               LYXSERVER_SETUP_RETRIES times)
    
    Note: The built in file() function doesnot allow nonblocking mode
    --> use os.open and os.fdopen to get a nonblocking file object
    """
    #
    extensions = {'w': '.in', 'r': '.out'}
    flags = {'w':  os.O_WRONLY|os.O_NONBLOCK,
             'r':  os.O_RDONLY|os.O_NONBLOCK,
             'w+': os.O_RDWR|os.O_NONBLOCK,
             'r+': os.O_RDWR|os.O_NONBLOCK}
    pipename = path + extensions[mode[0]]
    # open, catch exceptions (no pipe, lefover pipe)
    try:
        pipe_fd = os.open(pipename, flags[mode])
    except OSError, exception:
        # handle nonexisting or non-readable/writable pipes
        if exception.errno == 2:
            exception.strerror += " (wrong path or no running LyX)"
        elif exception.errno == 6:
            exception.strerror += " (leftover from a crashed lyx? -> deleting)"
            os.remove(path + ".in")
            os.remove(path + ".out")
        else:
            raise
        if lyxcmd is None or not os.path.isdir(os.path.dirname(path)):
            raise
        # grep in ps (returns 0 if found running lyx session)
        # no_lyx = os.system('ps -u "$USER" | grep -w %s'%lyxcmd.split()[0])
        # start `lyxcmd`, if grep in ps fails (i.e. no lyx session running)
        # if lyxcmd and no_lyx:
        if lyxcmd:
            start_lyx(lyxcmd)
        # connect to the serverpipe, try until LyX managed to set it up
        for i in xrange(LYXSERVER_SETUP_RETRIES):
            time.sleep(LYXSERVER_SETUP_DELAY)
            logger.debug("%d-th try to connect to lyxpipe"%(i+1))
            try:
                return lyxpipe(path, mode, lyxcmd=None)
            except OSError:
                continue
        raise
    # return as file object (with  bufsize=0)
    return os.fdopen(pipe_fd, mode, 0)


class LyXServer:
    """FileType like class for communication with the lyxserver
    reading from lyxpipe.in and writing to lyxpipe.out
    (Comparable to a bidirectional `popen` object)
    
    The pipes are open in nonblocking mode with polling enabled 
    for the outpipe. If there is no data waiting after polling timeout,
    this is treated like an EOF in a normal file
    
    This class provides all (sensible) attributes and methods as listed in
    the "Python Library Reference" section 2.3.8 "File Objects".
    
    The derived LyXClient class offers a more highlevel interface to 
    the lyxserver
    """
    name = "pyServer" # instances will append an id number to make it unique
    inpipe = None     # class variable, initialized on demand by self.open
    outpipe = None    # class variable, initialized on demand by self.open
    outpipe_poll = None  # polling object, initialized by self.open()
    timeout = LYXSERVER_POLL_TIMEOUT  # timeout for reading the outpipe (in ms)
    closed = None   # True, if closed with LyXServer.close()
    encoding = None # use the system default for converting Unicode strings
    mode = 'r+'     # open for read-write 
    newlines = None # 
    softspace = 0   # Classes trying to simulate a file object should also 
                    # have a writable softspace attribute, initialized to zero.
    #
    def __init__(self, lyxcmd=LYXCMD, path=SERVERPIPE):
        self.name += str(id(self))
        self.lyxcmd = lyxcmd
        self.path = path
    #
    def open(self):
        """Open the server pipes"""
        self.inpipe = lyxpipe(self.path, 'w', self.lyxcmd)
        self.outpipe = lyxpipe(self.path, 'r+')
        # register the outpipe for polling       (see poll-objects.html)
        self.outpipe_poll = select.poll()
        # self.outpipe_poll.register(self.outpipe, select.POLLIN)
        # print select.POLLIN, select.POLLPRI, select.POLLOUT, select.POLLERR, select.POLLHUP, select.POLLNVAL
        self.outpipe_poll.register(self.outpipe, 59)
        self.closed = False
        self.newlines = self.outpipe.newlines
    #
    def close(self):
        """Close the server pipes"""
        if self.closed is not None:
            self.inpipe.close()
            self.outpipe.close()
            self.outpipe_poll = None
        self.closed = True
    #
    def poll(self, timeout=None):
        """Poll the outpipe for new data (see poll-objects.html)
        
        timeout --  wait up to n ms, 
                     * block if negative, 
                     * use `self.timeout` if None (default)
                       (Note: this differs from select.poll.poll(), 
                       where both negative values and None block)
        
        Polling is implicitely done by LyXServer's read* methods
        """
        if timeout is None: # use default value at time of function call
            timeout = self.timeout
        #
        try:
            status = self.outpipe_poll.poll(timeout)
        except AttributeError: # polling object set to None --> pipes not open
            if self.closed:
                raise IOError, "polling closed pipe"
            else:
                self.open()
                status = self.outpipe_poll.poll(timeout)
        logger.debug("poll result: %s"%status)
        if not status:
            logger.debug("poll: no data after %d ms"%(timeout))
        return status
    #
    def read(self, size=-1, timeout=None):
        """read `size` bytes from the outpipe,
        if empty, wait `timeout` ms for new data to arrive
        """
        if self.poll(timeout):
            return self.outpipe.read(size)
        else:
            return ''
    #
    def readline(self, size=-1, timeout=None):
        """read one line (up to `size` bytes) from the outpipe,
        if empty, wait `timeout` ms for new data to arrive
        """
        if self.poll(timeout):
            return self.outpipe.readline(size)
        else:
            return ''
    #
    def readlines(self, sizehint=-1, timeout=None):
        """Read lines from the outpipe and return as list.
        
        if empty, wait `timeout` ms for new data to arrive
        
        Attention: outpipe.readlines() always (also with waiting data) gives
        IOError: [Errno 11] Resource temporarily unavailable
        
        Implementation: read() and split lines with splitlines() string method
        """
        output = self.read(sizehint, timeout)
        return output.splitlines(True)
    #
    def write(self, string):
        """Write a string representation of the argument to lyxpipe.in
        """
        try:
            self.inpipe.write(str(string))
        except (AttributeError, IOError): # pipes not open?, broken pipe?
            if self.closed:
                raise IOError, "writing to closed pipe"
            else:
                self.open()
                self.inpipe.write(str(string))
    #
    def writelines(self, sequence):
        """Write a sequence to the file. 
        
        The sequence can be any iterable object, the elements will be 
        converted to strings and written to lyxpipe.in. 
        There is no return value. 
        
        The name is intended to match readlines(); writelines() does not
        add line separators. (However, a sequence of LyXMessages has them 
        already)
        """
        sequence = map(str, sequence)
        self.write("".join(sequence))
    # Iterator protocoll
    def __iter__(self, timeout=None):
        """Return iterator (generator) yielding `self.readline`
        
        See `LyXServer.readline` for discussion of the `timeout`
        argument.
              
        Example: 
            1. simple call with default timout (self.timeout):
                for line in instance:
                    print line
            2. call with custom timeout:
                for line in instance.__iter__(timeout=20):
                    print line
        
        Note: While `LyXServer.readlines` polls only once,
              `LyXServer.__iter__` polls for new data with every iteration
            
              I.e. if new data arrives during polling, the next iteration
              polls again for `timeout` ms.
        """
        while self.poll(timeout):
            yield self.outpipe.readline()
    #
    # def __del__(self):
        # no special action needed
