| #!/usr/bin/ruby |
| # encoding: utf-8 |
| |
| require 'socket' |
| |
| module ANTLR3 |
| module Debug |
| |
| |
| =begin rdoc ANTLR3::Debug::EventSocketProxy |
| |
| A proxy debug event listener that forwards events over a socket to |
| a debugger (or any other listener) using a simple text-based protocol; |
| one event per line. ANTLRWorks listens on server socket with a |
| RemoteDebugEventSocketListener instance. These two objects must therefore |
| be kept in sync. New events must be handled on both sides of socket. |
| |
| =end |
| class EventSocketProxy |
| include EventListener |
| |
| SOCKET_ADDR_PACK = 'snCCCCa8'.freeze |
| |
| def initialize( recognizer, options = {} ) |
| super() |
| @grammar_file_name = recognizer.grammar_file_name |
| @adaptor = options[ :adaptor ] |
| @port = options[ :port ] || DEFAULT_PORT |
| @log = options[ :log ] |
| @socket = nil |
| @connection = nil |
| end |
| |
| def log!( message, *interpolation_arguments ) |
| @log and @log.printf( message, *interpolation_arguments ) |
| end |
| |
| def handshake |
| unless @socket |
| begin |
| @socket = Socket.new( Socket::AF_INET, Socket::SOCK_STREAM, 0 ) |
| @socket.setsockopt( Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1 ) |
| @socket.bind( Socket.pack_sockaddr_in( @port, '' ) ) |
| @socket.listen( 1 ) |
| log!( "waiting for incoming connection on port %i\n", @port ) |
| |
| @connection, addr = @socket.accept |
| port, host = Socket.unpack_sockaddr_in( addr ) |
| log!( "Accepted connection from %s:%s\n", host, port ) |
| |
| @connection.setsockopt( Socket::SOL_TCP, Socket::TCP_NODELAY, 1 ) |
| |
| write( 'ANTLR %s', PROTOCOL_VERSION ) |
| write( 'grammar %p', @grammar_file_name ) |
| ack |
| rescue IOError => error |
| log!( "handshake failed due to an IOError:\n" ) |
| log!( " %s: %s", error.class, error.message ) |
| log!( " Backtrace: " ) |
| log!( " - %s", error.backtrace.join( "\n - " ) ) |
| @connection and @connection.close |
| @socket and @socket.close |
| @socket = nil |
| raise |
| end |
| end |
| return self |
| end |
| |
| def write( message, *interpolation_arguments ) |
| message << ?\n |
| log!( "---> #{ message }", *interpolation_arguments ) |
| @connection.printf( message, *interpolation_arguments ) |
| @connection.flush |
| end |
| |
| def ack |
| line = @connection.readline |
| log!( "<--- %s", line ) |
| line |
| end |
| |
| def transmit( event, *interpolation_arguments ) |
| write( event, *interpolation_arguments ) |
| ack() |
| rescue IOError |
| @connection.close |
| raise |
| end |
| |
| def commence |
| # don't bother sending event; listener will trigger upon connection |
| end |
| |
| def terminate |
| transmit 'terminate' |
| @connection.close |
| @socket.close |
| end |
| |
| def enter_rule( grammar_file_name, rule_name ) |
| transmit "%s\t%s\t%s", :enter_rule, grammar_file_name, rule_name |
| end |
| |
| def enter_alternative( alt ) |
| transmit "%s\t%s", :enter_alternative, alt |
| end |
| |
| def exit_rule( grammar_file_name, rule_name ) |
| transmit "%s\t%s\t%s", :exit_rule, grammar_file_name, rule_name |
| end |
| |
| def enter_subrule( decision_number ) |
| transmit "%s\t%i", :enter_subrule, decision_number |
| end |
| |
| def exit_subrule( decision_number ) |
| transmit "%s\t%i", :exit_subrule, decision_number |
| end |
| |
| def enter_decision( decision_number ) |
| transmit "%s\t%i", :enter_decision, decision_number |
| end |
| |
| def exit_decision( decision_number ) |
| transmit "%s\t%i", :exit_decision, decision_number |
| end |
| |
| def consume_token( token ) |
| transmit "%s\t%s", :consume_token, serialize_token( token ) |
| end |
| |
| def consume_hidden_token( token ) |
| transmit "%s\t%s", :consume_hidden_token, serialize_token( token ) |
| end |
| |
| def look( i, item ) |
| case item |
| when AST::Tree |
| look_tree( i, item ) |
| when nil |
| else |
| transmit "%s\t%i\t%s", :look, i, serialize_token( item ) |
| end |
| end |
| |
| def mark( i ) |
| transmit "%s\t%i", :mark, i |
| end |
| |
| def rewind( i = nil ) |
| i ? transmit( "%s\t%i", :rewind, i ) : transmit( '%s', :rewind ) |
| end |
| |
| def begin_backtrack( level ) |
| transmit "%s\t%i", :begin_backtrack, level |
| end |
| def end_backtrack( level, successful ) |
| transmit "%s\t%i\t%p", :end_backtrack, level, ( successful ? true : false ) |
| end |
| |
| def location( line, position ) |
| transmit "%s\t%i\t%i", :location, line, position |
| end |
| |
| def recognition_exception( exception ) |
| transmit "%s\t%p\t%i\t%i\t%i", :recognition_exception, exception.class, |
| exception.index, exception.line, exception.column |
| end |
| |
| def begin_resync |
| transmit '%s', :begin_resync |
| end |
| |
| def end_resync |
| transmit '%s', :end_resync |
| end |
| |
| def semantic_predicate( result, predicate ) |
| pure_boolean = !( !result ) |
| transmit "%s\t%s\t%s", :semantic_predicate, pure_boolean, escape_newlines( predicate ) |
| end |
| |
| def consume_node( tree ) |
| transmit "%s\t%s", :consume_node, serialize_node( tree ) |
| end |
| |
| def adaptor |
| @adaptor ||= ANTLR3::CommonTreeAdaptor.new |
| end |
| |
| def look_tree( i, tree ) |
| transmit "%s\t%s\t%s", :look_tree, i, serialize_node( tree ) |
| end |
| |
| def flat_node( tree ) |
| transmit "%s\t%i", :flat_node, adaptor.unique_id( tree ) |
| end |
| |
| def error_node( tree ) |
| transmit "%s\t%i\t%i\t%p", :error_node, adaptor.unique_id( tree ), |
| Token::INVALID_TOKEN_TYPE, escape_newlines( tree.to_s ) |
| end |
| |
| def create_node( node, token = nil ) |
| if token |
| transmit "%s\t%i\t%i", :create_node, adaptor.unique_id( node ), |
| token.token_index |
| else |
| transmit "%s\t%i\t%i\t%p", :create_node, adaptor.unique_id( node ), |
| adaptor.type_of( node ), adaptor.text_of( node ) |
| end |
| end |
| |
| def become_root( new_root, old_root ) |
| transmit "%s\t%i\t%i", :become_root, adaptor.unique_id( new_root ), |
| adaptor.unique_id( old_root ) |
| end |
| |
| def add_child( root, child ) |
| transmit "%s\t%i\t%i", :add_child, adaptor.unique_id( root ), |
| adaptor.unique_id( child ) |
| end |
| |
| def set_token_boundaries( t, token_start_index, token_stop_index ) |
| transmit "%s\t%i\t%i\t%i", :set_token_boundaries, adaptor.unique_id( t ), |
| token_start_index, token_stop_index |
| end |
| |
| attr_accessor :adaptor |
| |
| def serialize_token( token ) |
| [ token.token_index, token.type, token.channel, |
| token.line, token.column, |
| escape_newlines( token.text ) ].join( "\t" ) |
| end |
| |
| def serialize_node( node ) |
| adaptor ||= ANTLR3::AST::CommonTreeAdaptor.new |
| id = adaptor.unique_id( node ) |
| type = adaptor.type_of( node ) |
| token = adaptor.token( node ) |
| line = token.line rescue -1 |
| col = token.column rescue -1 |
| index = adaptor.token_start_index( node ) |
| [ id, type, line, col, index ].join( "\t" ) |
| end |
| |
| |
| def escape_newlines( text ) |
| text.inspect.tap do |t| |
| t.gsub!( /%/, '%%' ) |
| end |
| end |
| end |
| |
| =begin rdoc ANTLR3::Debug::RemoteEventSocketListener |
| |
| A debugging event listener which intercepts debug event messages sent by a EventSocketProxy |
| over an IP socket. |
| |
| =end |
| class RemoteEventSocketListener < ::Thread |
| autoload :StringIO, 'stringio' |
| ESCAPE_MAP = Hash.new { |h, k| k } |
| ESCAPE_MAP.update( |
| ?n => ?\n, ?t => ?\t, ?a => ?\a, ?b => ?\b, ?e => ?\e, |
| ?f => ?\f, ?r => ?\r, ?v => ?\v |
| ) |
| |
| attr_reader :host, :port |
| |
| def initialize( options = {} ) |
| @listener = listener |
| @host = options.fetch( :host, 'localhost' ) |
| @port = options.fetch( :port, DEFAULT_PORT ) |
| @buffer = StringIO.new |
| super do |
| connect do |
| handshake |
| loop do |
| yield( read_event ) |
| end |
| end |
| end |
| end |
| |
| private |
| |
| def handshake |
| @version = @socket.readline.split( "\t" )[ -1 ] |
| @grammar_file = @socket.readline.split( "\t" )[ -1 ] |
| ack |
| end |
| |
| def ack |
| @socket.puts( "ack" ) |
| @socket.flush |
| end |
| |
| def unpack_event( event ) |
| event.nil? and raise( StopIteration ) |
| event.chomp! |
| name, *elements = event.split( "\t",-1 ) |
| name = name.to_sym |
| name == :terminate and raise StopIteration |
| elements.map! do |elem| |
| elem.empty? and next( nil ) |
| case elem |
| when /^\d+$/ then Integer( elem ) |
| when /^\d+\.\d+$/ then Float( elem ) |
| when /^true$/ then true |
| when /^false$/ then false |
| when /^"(.*)"$/ then parse_string( $1 ) |
| end |
| end |
| elements.unshift( name ) |
| return( elements ) |
| end |
| |
| def read_event |
| event = @socket.readline or raise( StopIteration ) |
| ack |
| return unpack_event( event ) |
| end |
| |
| def connect |
| TCPSocket.open( @host, @port ) do |socket| |
| @socket = socket |
| yield |
| end |
| end |
| |
| def parse_string( string ) |
| @buffer.string = string |
| @buffer.rewind |
| out = '' |
| until @buffer.eof? |
| case c = @buffer.getc |
| when ?\\ # escape |
| nc = @buffer.getc |
| out << |
| if nc.between?( ?0, ?9 ) # octal integer |
| @buffer.ungetc( nc ) |
| @buffer.read( 3 ).to_i( 8 ).chr |
| elsif nc == ?x |
| @buffer.read( 2 ).to_i( 16 ).chr |
| else |
| ESCAPE_MAP[ nc ] |
| end |
| else |
| out << c |
| end |
| end |
| return out |
| end |
| |
| end # class RemoteEventSocketListener |
| end # module Debug |
| end # module ANTLR3 |