vim/bundle/slimv/ftplugin/slimv.py @ f9d21c2a44b3

vim: sub/gundo
author Steve Losh <steve@stevelosh.com>
date Tue, 16 Nov 2010 17:00:09 -0500
parents c4d3b4507707
children (none)
#!/usr/bin/env python

###############################################################################
#
# Client/Server code for Slimv
# slimv.py:     Client/Server code for slimv.vim plugin
# Version:      0.6.2
# Last Change:  01 Jun 2010
# Maintainer:   Tamas Kovacs <kovisoft at gmail dot com>
# License:      This file is placed in the public domain.
#               No warranty, express or implied.
#               *** ***   Use At-Your-Own-Risk!   *** ***
# 
###############################################################################

import os
import sys
import getopt
import time
import shlex
import socket
import traceback
from subprocess import Popen, PIPE, STDOUT
from threading import Thread, BoundedSemaphore

autoconnect = 1             # Start and connect server automatically

HOST        = ''            # Symbolic name meaning the local host
PORT        = 5151          # Arbitrary non-privileged port

debug_level = 0             # Debug level for diagnostic messages
terminate   = 0             # Main program termination flag

python_path = 'python'      # Path of the Python interpreter (overridden via command line args)
lisp_path   = 'clisp.exe'   # Path of the Lisp interpreter (overridden via command line args)
slimv_path  = 'slimv.py'    # Path of this script (determined later)
run_cmd     = ''            # Complex server-run command (if given via command line args)

newline     = '\n'

# Check if we're running Windows or Mac OS X, otherwise assume Linux
mswindows = (sys.platform == 'win32')
darwin = (sys.platform == 'darwin')

if not (mswindows or darwin):
    import pty

def log( s, level ):
    """Print diagnostic messages according to the actual debug level.
    """
    if debug_level >= level:
        print s


###############################################################################
#
# Client part
#
###############################################################################

def start_server():
    """Spawn server. Does not check if the server is already running.
    """
    if run_cmd == '':
        # Complex run command not given, build it from the information available
        if mswindows or darwin:
            cmd = []
        else:
            cmd = ['xterm', '-T', 'Slimv', '-e']
        cmd = cmd + [python_path, slimv_path, '-p', str(PORT), '-l', lisp_path, '-s']
    else:
        cmd = shlex.split(run_cmd)

    # Start server
    #TODO: put in try-block
    if mswindows:
        CREATE_NEW_CONSOLE = 16
        server = Popen( cmd, creationflags=CREATE_NEW_CONSOLE )
    elif darwin:
        from ScriptingBridge import SBApplication

        term = SBApplication.applicationWithBundleIdentifier_("com.apple.Terminal")
        term.doScript_in_(" ".join(cmd) + " ; exit", 0) 
    else:
        server = Popen( cmd )

    # Allow subprocess (server) to start
    time.sleep( 2.0 )


def connect_server():
    """Try to connect server, if server not found then spawn it.
       Return socket object on success, None on failure.
    """

    s = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
    try:
        s.connect( ( 'localhost', PORT ) )
    except socket.error, msg:
        if autoconnect:
            # We need to try to start the server automatically
            s.close()
            start_server()

            # Open socket to the server
            s = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
            try:
                s.connect( ( 'localhost', PORT ) )
            except socket.error, msg:
                s.close()
                s =  None
        else:   # not autoconnect
            print "Server not found"
            s = None
    return s


def send_line( server, line ):
    """Send a line to the server:
       first send line length in 4 bytes, then send the line itself.
    """
    l = len(line)
    lstr = chr(l&255) + chr((l>>8)&255) + chr((l>>16)&255) + chr((l>>24)&255)
    server.send( lstr )     # send message length first
    server.send( line )     # then the message itself


def client_file( input_filename ):
    """Main client routine - input file version:
       starts server if needed then send text to server.
       Input is read from input file.
    """
    s = connect_server()
    if s is None:
        return

    try:
        file = open( input_filename, 'rt' )
        try:
            # Send contents of the file to the server
            lines = ''
            for line in file:
                lines = lines + line
            send_line( s, lines )
        finally:
            file.close()
    except:
        return

    s.close()


###############################################################################
#
# Server part
#
###############################################################################

class repl_buffer:
    def __init__ ( self, output_pipe ):

        self.output   = output_pipe
        self.filename = ''
        self.buffer   = ''
        self.sema     = BoundedSemaphore()
                            # Semaphore to synchronize access to the global display queue

    def setfile( self, filename ):
        """Set output filename. Greet user if this is the first time.
        """
        self.sema.acquire()
        oldname = self.filename
        self.filename = filename
        if oldname == '':
            try:
                # Delete old file creted at a previous run
                os.remove( self.filename )
            except:
                # OK, at least we tried
                pass
            self.write_nolock( newline + ';;; Slimv client is connected to REPL on port ' + str(PORT) + '.' + newline, True )
            user = None
            if mswindows:
                user = os.getenv('USERNAME')
            else:
                user = os.getenv('USER')
            if not user:
                self.write_nolock( ';;; This could be the start of a beautiful program.' + newline + newline, True )
            else:
                self.write_nolock( ';;; ' + user + ', this could be the start of a beautiful program.' + newline + newline, True )
        self.sema.release()

    def writebegin( self ):
        """Begin the writing process. The process is protected by a semaphore.
        """
        self.sema.acquire()

    def writeend( self ):
        """Finish the writing process. Release semaphore
        """
        self.sema.release()

    def write_nolock( self, text, fileonly=False ):
        """Write text into the global display queue buffer.
           The writing process is not protected.
        """
        if not fileonly:
            try:
                # Write all lines to the display
                os.write( self.output.fileno(), text )
            except:
                pass

        if self.filename != '':
            tries = 4
            while tries > 0:
                try:
                    file = open( self.filename, 'at' )
                    try:
                        #file.write( text )
                        if self.buffer != '':
                            # There is output pending
                            os.write(file.fileno(), self.buffer )
                            self.buffer = ''
                        os.write(file.fileno(), text )
                    finally:
                        file.close()
                    tries = 0
                except IOError:
                    tries = tries - 1
                    if tries == 0:
                        traceback.print_exc()
                    time.sleep(0.05)
                except:
                    tries = tries - 1
                    time.sleep(0.05)
        elif len( self.buffer ) < 2000:
            # No filename supplied, collect output info a buffer until filename is given
            # We collect only some bytes, then probably no filename will be given at all
            self.buffer = self.buffer + text

    def write( self, text, fileonly=False ):
        """Write text into the global display queue buffer.
           The writing process is protected by a semaphome.
        """
        self.writebegin()
        self.write_nolock( text, fileonly )
        self.writeend()


class socket_listener( Thread ):
    """Server thread to receive text from the client via socket.
    """

    def __init__ ( self, inp, buffer, pid ):
        Thread.__init__( self )
        self.inp = inp
        self.buffer = buffer
        self.pid = pid

    def run( self ):
        global terminate

        # Open server socket
        self.s = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
        self.s.bind( (HOST, PORT) )

        while not terminate:
            # Listen server socket
            self.s.listen( 1 )
            conn, addr = self.s.accept()

            while not terminate:
                l = 0
                lstr = ''
                # Read length first, it comes in 4 bytes
                try:
                    lstr = conn.recv(4)
                    if len( lstr ) <= 0:
                        break
                except:
                    traceback.print_exc()
                    break
                if terminate:
                    break
                l = ord(lstr[0]) + (ord(lstr[1])<<8) + (ord(lstr[2])<<16) + (ord(lstr[3])<<24)
                if l > 0:
                    # Valid length received, now wait for the message
                    try:
                        # Read the message itself
                        received = ''
                        while len( received ) < l:
                            r = conn.recv(l)
                            if len( r ) == 0:
                                break
                            received = received + r
                        if len( received ) < l:
                            break
                    except:
                        traceback.print_exc()
                        break

                    if len(received) >= 7 and received[0:7] == 'SLIMV::':
                        command = received[7:]
                        if len(command) >= 9 and command[0:9] == 'INTERRUPT':
                            try:
                                if mswindows:
                                    import win32api
                                    CTRL_C_EVENT = 0
                                    win32api.GenerateConsoleCtrlEvent( CTRL_C_EVENT, 0 )
                                else:
                                    import signal
                                    os.kill( self.pid, signal.SIGINT )
                            except:
                                # OK, at least we tried
                                # Go on without interruption
                                pass
                        if len(command) >= 8 and command[0:8] == 'OUTPUT::':
                            output_filename = command[8:].rstrip( '\n' )
                            self.buffer.setfile( output_filename )
                    else:
                        # Fork here: write message to the stdin of REPL
                        # and also write it to the display (display queue buffer)
                        self.buffer.writebegin()
                        self.buffer.write_nolock( received )
                        os.write(self.inp.fileno(), received)
                        self.buffer.writeend()

            conn.close()


class output_listener( Thread ):
    """Server thread to receive REPL output.
    """

    def __init__ ( self, out, buffer ):
        Thread.__init__( self )
        self.out = out
        self.buffer = buffer

    def run( self ):
        global terminate

        while not terminate:
            try:
                # Read input from the stdout of REPL
                # and write it to the display (display queue buffer)
                if mswindows:
                    c = self.out.read( 1 )
                    if ord( c ) == 0x0D:
                        # Special handling of 0x0D+0x0A on Windows
                        c2 = self.out.read( 1 )
                        if ord( c2 ) == 0x0A:
                            self.buffer.write( '\n' )
                        else:
                            self.buffer.write( c )
                            self.buffer.write( c2 )
                    else:
                        self.buffer.write( c )
                elif darwin:
                    c = self.out.read( 1 )
                    self.buffer.write( c )
                else:
                    c = self.out.read( 1 )
                    if ord( c ) != 0x0D:
                        self.buffer.write( c )
            except:
                terminate = 1


def server():
    """Main server routine: starts REPL and helper threads for
       sending and receiving data to/from REPL.
    """
    global terminate

    # First check if server already runs
    s = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
    try:
        s.connect( ( 'localhost', PORT ) )
    except socket.error, msg:
        # Server not found, our time has come, we'll start a new server in a moment
        pass
    else:
        # Server found, nothing to do here
        s.close()
        print "Server is already running"
        return

    # Build Lisp-starter command
    lisp_exp = lisp_path.replace( '\\', '\\\\' )
    if not mswindows:
        # Popen does not work with tilde-prefix on Linux
        # so we expand them to the home directory
        user = os.path.expanduser( '~/' )
        lisp_exp = lisp_exp.replace( ' ~/', ' ' + user )
    cmd = shlex.split( lisp_exp )

    # Start Lisp
    if mswindows or darwin:
        repl = Popen( cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT )
        repl_stdin = repl.stdin
        repl_stdout = repl.stdout
        repl_pid = repl.pid
    else:
        repl_pid, repl_fd = pty.fork()
        if repl_pid == 0:
            os.execvp( cmd[0], cmd )
            os._exit(1)
        repl_stdin = repl_stdout = os.fdopen( repl_fd )

    buffer = repl_buffer( sys.stdout )

    # Create and start helper threads
    sl = socket_listener( repl_stdin, buffer, repl_pid )
    sl.start()
    ol = output_listener( repl_stdout, buffer )
    ol.start()

    # Allow Lisp to start, confuse it with some fancy Slimv messages
    sys.stdout.write( ";;; Slimv server is started on port " + str(PORT) + newline )
    sys.stdout.write( ";;; Slimv is spawning REPL..." + newline + newline )
    time.sleep(0.5)             # wait for Lisp to start
    #sys.stdout.write( ";;; Slimv connection established" + newline )

    # Main server loop
    while not terminate:
        try:
            # Read input from the console and write it
            # to the stdin of REPL
            text = raw_input()
            os.write( repl_stdin.fileno(), text + newline )
            buffer.write( text + newline, True )
        except EOFError:
            # EOF (Ctrl+Z on Windows, Ctrl+D on Linux) pressed?
            terminate = 1
        except KeyboardInterrupt:
            # Interrupted from keyboard (Ctrl+C)?
            # We just ignore it here, it will be propagated to the child anyway
            pass

    # The socket is opened here only for waking up the server thread
    # in order to recognize the termination message
    #TODO: exit REPL if this script is about to exit
    cs = socket.socket( socket.AF_INET, socket.SOCK_STREAM )
    try:
        cs.connect( ( 'localhost', PORT ) )
        cs.send( " " )
    finally:
        # We don't care if this above fails, we'll exit anyway
        cs.close()

    # Send exit command to child process and
    # wake output listener up at the same time
    try:
        repl_stdin.close()
    except:
        # We don't care if this above fails, we'll exit anyway
        pass

    # Be nice
    print 'Thank you for using Slimv.'

    # Wait for the child process to exit
    time.sleep(1)


def escape_path( path ):
    """Surround path containing spaces with backslash + double quote,
       so that it can be passed as a command line argument.
    """
    if path.find( ' ' ) < 0:
        return path
    if path[0:2] == '\\\"':
        return path
    elif path[0] == '\"':
        return '\\' + path + '\\'
    else:
        return '\\\"' + path + '\\\"'


def usage():
    """Displays program usage information.
    """
    progname = os.path.basename( sys.argv[0] )
    print 'Usage: ', progname + ' [-d LEVEL] [-s] [-f INFILE]'
    print
    print 'Options:'
    print '  -?, -h, --help                show this help message and exit'
    print '  -l PATH, --lisp=PATH          path of Lisp interpreter'
    print '  -r PATH, --run=PATH           full command to run the server'
    print '  -p PORT, --port=PORT          port number to use by the server/client'
    print '  -d LEVEL, --debug=LEVEL       set debug LEVEL (0..3)'
    print '  -s                            start server'
    print '  -f FILENAME, --file=FILENAME  start client and send contents of file'
    print '                                named FILENAME to server'


###############################################################################
#
# Main program
#
###############################################################################

if __name__ == '__main__':

    EXIT, SERVER, CLIENT = range( 3 )
    mode = EXIT
    slimv_path = sys.argv[0]
    python_path = sys.executable
    input_filename = ''

    # Always this trouble with the path/filenames containing spaces:
    # enclose them in double quotes
    if python_path.find( ' ' ) >= 0:
        python_path = '"' + python_path + '"'

    # Get command line options
    try:
        opts, args = getopt.getopt( sys.argv[1:], '?hcsf:p:l:r:d:', \
                                    ['help', 'client', 'server', 'file=', 'port=', 'lisp=', 'run=', 'debug='] )

        # Process options
        for o, a in opts:
            if o in ('-?', '-h', '--help'):
                usage()
                break
            if o in ('-p', '--port'):
                try:
                    PORT = int(a)
                except:
                    # If given port number is malformed, then keep default value
                    pass
            if o in ('-l', '--lisp'):
                lisp_path = a
            if o in ('-r', '--run'):
                run_cmd = a
            if o in ('-d', '--debug'):
                try:
                    debug_level = int(a)
                except:
                    # If given level is malformed, then keep default value
                    pass
            if o in ('-s', '--server'):
                mode = SERVER
            if o in ('-c', '--client'):
                mode = CLIENT
            if o in ('-f', '--file'):
                mode = CLIENT
                input_filename = a

    except getopt.GetoptError:
        # print help information and exit:
        usage()

    if mode == SERVER:
        # We are started in server mode
        server()

    if mode == CLIENT:
        # We are started in client mode
        if run_cmd != '':
            # It is possible to pass special argument placeholders to run_cmd
            run_cmd = run_cmd.replace( '@p', escape_path( python_path ) )
            run_cmd = run_cmd.replace( '@s', escape_path( slimv_path ) )
            run_cmd = run_cmd.replace( '@l', escape_path( lisp_path ) )
            run_cmd = run_cmd.replace( '@@', '@' )
            log( run_cmd, 1 )
        if input_filename != '':
            client_file( input_filename )
        else:
            start_server()

# --- END OF FILE ---