# swank.rb --- swank server for Ruby.
|
|
#
|
|
# This is my first Ruby program and looks probably rather strange. Some
|
|
# people write Scheme interpreters when learning new languages, I
|
|
# write swank backends.
|
|
#
|
|
# Only a few things work.
|
|
# 1. Start the server with something like: ruby -r swank -e swank
|
|
# 2. Use M-x slime-connect to establish a connection
|
|
|
|
require "socket"
|
|
|
|
def swank(port=4005)
|
|
accept_connections port, false
|
|
end
|
|
|
|
def start_swank(port_file)
|
|
accept_connections false, port_file
|
|
end
|
|
|
|
def accept_connections(port, port_file)
|
|
server = TCPServer.new("localhost", port || 0)
|
|
puts "Listening on #{server.addr.inspect}\n"
|
|
if port_file
|
|
write_port_file server.addr[1], port_file
|
|
end
|
|
socket = begin server.accept ensure server.close end
|
|
begin
|
|
serve socket.to_io
|
|
ensure
|
|
socket.close
|
|
end
|
|
end
|
|
|
|
def write_port_file(port, filename)
|
|
File.open(filename, File::CREAT|File::EXCL|File::WRONLY) do |f|
|
|
f.puts port
|
|
end
|
|
end
|
|
|
|
def serve(io)
|
|
main_loop(io)
|
|
end
|
|
|
|
def main_loop(io)
|
|
c = Connection.new(io)
|
|
while true
|
|
catch :swank_top_level do
|
|
c.dispatch(read_packet(io))
|
|
end
|
|
end
|
|
end
|
|
|
|
class Connection
|
|
|
|
def initialize(io)
|
|
@io = io
|
|
end
|
|
|
|
def dispatch(event)
|
|
puts "dispatch: %s\n" % event.inspect
|
|
case event[0]
|
|
when :":emacs-rex"
|
|
emacs_rex *event[1..4]
|
|
else raise "Unhandled event: #{event.inspect}"
|
|
end
|
|
end
|
|
|
|
def send_to_emacs(obj)
|
|
payload = write_sexp_to_string(obj)
|
|
@io.write("%06x" % payload.length)
|
|
@io.write payload
|
|
@io.flush
|
|
end
|
|
|
|
def emacs_rex(form, pkg, thread, id)
|
|
proc = $rpc_entries[form[0]]
|
|
args = form[1..-1];
|
|
begin
|
|
raise "Undefined function: #{form[0]}" unless proc
|
|
value = proc[*args]
|
|
rescue Exception => exc
|
|
begin
|
|
pseudo_debug exc
|
|
ensure
|
|
send_to_emacs [:":return", [:":abort"], id]
|
|
end
|
|
else
|
|
send_to_emacs [:":return", [:":ok", value], id]
|
|
end
|
|
end
|
|
|
|
def pseudo_debug(exc)
|
|
level = 1
|
|
send_to_emacs [:":debug", 0, level] + sldb_info(exc, 0, 20)
|
|
begin
|
|
sldb_loop exc
|
|
ensure
|
|
send_to_emacs [:":debug-return", 0, level, :nil]
|
|
end
|
|
end
|
|
|
|
def sldb_loop(exc)
|
|
$sldb_context = [self,exc]
|
|
while true
|
|
dispatch(read_packet(@io))
|
|
end
|
|
end
|
|
|
|
def sldb_info(exc, start, _end)
|
|
[[exc.to_s,
|
|
" [%s]" % exc.class.name,
|
|
:nil],
|
|
sldb_restarts(exc),
|
|
sldb_backtrace(exc, start, _end),
|
|
[]]
|
|
end
|
|
|
|
def sldb_restarts(exc)
|
|
[["Quit", "SLIME top-level."]]
|
|
end
|
|
|
|
def sldb_backtrace(exc, start, _end)
|
|
bt = []
|
|
exc.backtrace[start.._end].each_with_index do |frame, i|
|
|
bt << [i, frame]
|
|
end
|
|
bt
|
|
end
|
|
|
|
def frame_src_loc(exc, frame)
|
|
string = exc.backtrace[frame]
|
|
match = /([^:]+):([0-9]+)/.match(string)
|
|
if match
|
|
file,line = match[1..2]
|
|
[:":location", [:":file", file], [:":line", line.to_i], :nil]
|
|
else
|
|
[:":error", "no src-loc for frame: #{string}"]
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
$rpc_entries = Hash.new
|
|
|
|
$rpc_entries[:"swank:connection-info"] = lambda do ||
|
|
[:":pid", $$,
|
|
:":package", [:":name", "ruby", :":prompt", "ruby> "],
|
|
:":lisp-implementation", [:":type", "Ruby",
|
|
:":name", "ruby",
|
|
:":version", RUBY_VERSION]]
|
|
end
|
|
|
|
def swank_interactive_eval(string)
|
|
eval(string,TOPLEVEL_BINDING).inspect
|
|
end
|
|
|
|
$rpc_entries[:"swank:interactive-eval"] = \
|
|
$rpc_entries[:"swank:interactive-eval-region"] = \
|
|
$rpc_entries[:"swank:pprint-eval"] = lambda { |string|
|
|
swank_interactive_eval string
|
|
}
|
|
|
|
$rpc_entries[:"swank:throw-to-toplevel"] = lambda {
|
|
throw :swank_top_level
|
|
}
|
|
|
|
$rpc_entries[:"swank:backtrace"] = lambda do |from, to|
|
|
conn, exc = $sldb_context
|
|
conn.sldb_backtrace(exc, from, to)
|
|
end
|
|
|
|
$rpc_entries[:"swank:frame-source-location"] = lambda do |frame|
|
|
conn, exc = $sldb_context
|
|
conn.frame_src_loc(exc, frame)
|
|
end
|
|
|
|
#ignored
|
|
$rpc_entries[:"swank:buffer-first-change"] = \
|
|
$rpc_entries[:"swank:operator-arglist"] = lambda do
|
|
:nil
|
|
end
|
|
|
|
$rpc_entries[:"swank:simple-completions"] = lambda do |prefix, pkg|
|
|
swank_simple_completions prefix, pkg
|
|
end
|
|
|
|
# def swank_simple_completions(prefix, pkg)
|
|
|
|
def read_packet(io)
|
|
header = read_chunk(io, 6)
|
|
len = header.hex
|
|
payload = read_chunk(io, len)
|
|
#$deferr.puts payload.inspect
|
|
read_sexp_from_string(payload)
|
|
end
|
|
|
|
def read_chunk(io, len)
|
|
buffer = io.read(len)
|
|
raise "short read" if buffer.length != len
|
|
buffer
|
|
end
|
|
|
|
def write_sexp_to_string(obj)
|
|
string = ""
|
|
write_sexp_to_string_loop obj, string
|
|
string
|
|
end
|
|
|
|
def write_sexp_to_string_loop(obj, string)
|
|
if obj.is_a? String
|
|
string << "\""
|
|
string << obj.gsub(/(["\\])/,'\\\\\1')
|
|
string << "\""
|
|
elsif obj.is_a? Array
|
|
string << "("
|
|
max = obj.length-1
|
|
obj.each_with_index do |e,i|
|
|
write_sexp_to_string_loop e, string
|
|
string << " " unless i == max
|
|
end
|
|
string << ")"
|
|
elsif obj.is_a? Symbol or obj.is_a? Numeric
|
|
string << obj.to_s
|
|
elsif obj == false
|
|
string << "nil"
|
|
elsif obj == true
|
|
string << "t"
|
|
else raise "Can't write: #{obj.inspect}"
|
|
end
|
|
end
|
|
|
|
def read_sexp_from_string(string)
|
|
stream = StringInputStream.new(string)
|
|
reader = LispReader.new(stream)
|
|
reader.read
|
|
end
|
|
|
|
class LispReader
|
|
def initialize(io)
|
|
@io = io
|
|
end
|
|
|
|
def read(allow_consing_dot=false)
|
|
skip_whitespace
|
|
c = @io.getc
|
|
case c
|
|
when ?( then read_list(true)
|
|
when ?" then read_string
|
|
when ?' then read_quote
|
|
when nil then raise EOFError.new("EOF during read")
|
|
else
|
|
@io.ungetc(c)
|
|
obj = read_number_or_symbol
|
|
if obj == :"." and not allow_consing_dot
|
|
raise "Consing-dot in invalid context"
|
|
end
|
|
obj
|
|
end
|
|
end
|
|
|
|
def read_list(head)
|
|
list = []
|
|
loop do
|
|
skip_whitespace
|
|
c = @io.readchar
|
|
if c == ?)
|
|
break
|
|
else
|
|
@io.ungetc(c)
|
|
obj = read(!head)
|
|
if obj == :"."
|
|
error "Consing-dot not implemented" # would need real conses
|
|
end
|
|
head = false
|
|
list << obj
|
|
end
|
|
end
|
|
list
|
|
end
|
|
|
|
def read_string
|
|
string = ""
|
|
loop do
|
|
c = @io.getc
|
|
case c
|
|
when ?"
|
|
break
|
|
when ?\\
|
|
c = @io.getc
|
|
case c
|
|
when ?\\, ?" then string << c
|
|
else raise "Invalid escape char: \\%c" % c
|
|
end
|
|
else
|
|
string << c
|
|
end
|
|
end
|
|
string
|
|
end
|
|
|
|
def read_quote
|
|
[:quote, read]
|
|
end
|
|
|
|
def read_number_or_symbol
|
|
token = read_token
|
|
if token.empty?
|
|
raise EOFError.new
|
|
elsif /^[0-9]+$/.match(token)
|
|
token.to_i
|
|
elsif /^[0-9]+\.[0-9]+$/.match(token)
|
|
token.to_f
|
|
else
|
|
token.intern
|
|
end
|
|
end
|
|
|
|
def read_token
|
|
token = ""
|
|
loop do
|
|
c = @io.getc
|
|
if c.nil?
|
|
break
|
|
elsif terminating?(c)
|
|
@io.ungetc(c)
|
|
break
|
|
else
|
|
token << c
|
|
end
|
|
end
|
|
token
|
|
end
|
|
|
|
def skip_whitespace
|
|
loop do
|
|
c = @io.getc
|
|
case c
|
|
when ?\s, ?\n, ?\t then next
|
|
when nil then break
|
|
else @io.ungetc(c); break
|
|
end
|
|
end
|
|
end
|
|
|
|
def terminating?(char)
|
|
" \n\t()\"'".include?(char)
|
|
end
|
|
|
|
end
|
|
|
|
|
|
class StringInputStream
|
|
def initialize(string)
|
|
@string = string
|
|
@pos = 0
|
|
@max = string.length
|
|
end
|
|
|
|
def pos() @pos end
|
|
|
|
def getc
|
|
if @pos == @max
|
|
nil
|
|
else
|
|
c = @string[@pos]
|
|
@pos += 1
|
|
c
|
|
end
|
|
end
|
|
|
|
def readchar
|
|
getc or raise EOFError.new
|
|
end
|
|
|
|
def ungetc(c)
|
|
if @pos > 0 && @string[@pos-1] == c
|
|
@pos -= 1
|
|
else
|
|
raise "Invalid argument: %c [at %d]" % [c, @pos]
|
|
end
|
|
end
|
|
|
|
end
|
|
|