blob: 49c87797f25c1ec7e3252ab6608d264565b3e228 [file] [log] [blame]
#!/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