| 1 |
# swank.rb --- swank server for Ruby.
|
| 2 |
#
|
| 3 |
# This is my first Ruby program and looks probably rather strange. Some
|
| 4 |
# people write Scheme interpreters when learning new languages, I
|
| 5 |
# write swank backends.
|
| 6 |
#
|
| 7 |
# Only a few things work.
|
| 8 |
# 1. Start the server with something like: ruby -r swank -e swank
|
| 9 |
# 2. Use M-x slime-connect to establish a connection
|
| 10 |
|
| 11 |
require "socket"
|
| 12 |
|
| 13 |
def swank(port=4005)
|
| 14 |
accept_connections port, false
|
| 15 |
end
|
| 16 |
|
| 17 |
def start_swank(port_file)
|
| 18 |
accept_connections false, port_file
|
| 19 |
end
|
| 20 |
|
| 21 |
def accept_connections(port, port_file)
|
| 22 |
server = TCPServer.new("localhost", port || 0)
|
| 23 |
puts "Listening on #{server.addr.inspect}\n"
|
| 24 |
if port_file
|
| 25 |
write_port_file server.addr[1], port_file
|
| 26 |
end
|
| 27 |
socket = begin server.accept ensure server.close end
|
| 28 |
begin
|
| 29 |
serve socket.to_io
|
| 30 |
ensure
|
| 31 |
socket.close
|
| 32 |
end
|
| 33 |
end
|
| 34 |
|
| 35 |
def write_port_file(port, filename)
|
| 36 |
File.open(filename, File::CREAT|File::EXCL|File::WRONLY) do |f|
|
| 37 |
f.puts port
|
| 38 |
end
|
| 39 |
end
|
| 40 |
|
| 41 |
def serve(io)
|
| 42 |
main_loop(io)
|
| 43 |
end
|
| 44 |
|
| 45 |
def main_loop(io)
|
| 46 |
c = Connection.new(io)
|
| 47 |
while true
|
| 48 |
catch :swank_top_level do
|
| 49 |
c.dispatch(read_packet(io))
|
| 50 |
end
|
| 51 |
end
|
| 52 |
end
|
| 53 |
|
| 54 |
class Connection
|
| 55 |
|
| 56 |
def initialize(io)
|
| 57 |
@io = io
|
| 58 |
end
|
| 59 |
|
| 60 |
def dispatch(event)
|
| 61 |
puts "dispatch: %s\n" % event.inspect
|
| 62 |
case event[0]
|
| 63 |
when :":emacs-rex"
|
| 64 |
emacs_rex *event[1..4]
|
| 65 |
else raise "Unhandled event: #{event.inspect}"
|
| 66 |
end
|
| 67 |
end
|
| 68 |
|
| 69 |
def send_to_emacs(obj)
|
| 70 |
payload = write_sexp_to_string(obj)
|
| 71 |
@io.write("%06x" % payload.length)
|
| 72 |
@io.write payload
|
| 73 |
@io.flush
|
| 74 |
end
|
| 75 |
|
| 76 |
def emacs_rex(form, pkg, thread, id)
|
| 77 |
proc = $rpc_entries[form[0]]
|
| 78 |
args = form[1..-1];
|
| 79 |
begin
|
| 80 |
raise "Undefined function: #{form[0]}" unless proc
|
| 81 |
value = proc[*args]
|
| 82 |
rescue Exception => exc
|
| 83 |
begin
|
| 84 |
pseudo_debug exc
|
| 85 |
ensure
|
| 86 |
send_to_emacs [:":return", [:":abort"], id]
|
| 87 |
end
|
| 88 |
else
|
| 89 |
send_to_emacs [:":return", [:":ok", value], id]
|
| 90 |
end
|
| 91 |
end
|
| 92 |
|
| 93 |
def pseudo_debug(exc)
|
| 94 |
level = 1
|
| 95 |
send_to_emacs [:":debug", 0, level] + sldb_info(exc, 0, 20)
|
| 96 |
begin
|
| 97 |
sldb_loop exc
|
| 98 |
ensure
|
| 99 |
send_to_emacs [:":debug-return", 0, level, :nil]
|
| 100 |
end
|
| 101 |
end
|
| 102 |
|
| 103 |
def sldb_loop(exc)
|
| 104 |
$sldb_context = [self,exc]
|
| 105 |
while true
|
| 106 |
dispatch(read_packet(@io))
|
| 107 |
end
|
| 108 |
end
|
| 109 |
|
| 110 |
def sldb_info(exc, start, _end)
|
| 111 |
[[exc.to_s,
|
| 112 |
" [%s]" % exc.class.name,
|
| 113 |
:nil],
|
| 114 |
sldb_restarts(exc),
|
| 115 |
sldb_backtrace(exc, start, _end),
|
| 116 |
[]]
|
| 117 |
end
|
| 118 |
|
| 119 |
def sldb_restarts(exc)
|
| 120 |
[["Quit", "SLIME top-level."]]
|
| 121 |
end
|
| 122 |
|
| 123 |
def sldb_backtrace(exc, start, _end)
|
| 124 |
bt = []
|
| 125 |
exc.backtrace[start.._end].each_with_index do |frame, i|
|
| 126 |
bt << [i, frame]
|
| 127 |
end
|
| 128 |
bt
|
| 129 |
end
|
| 130 |
|
| 131 |
def frame_src_loc(exc, frame)
|
| 132 |
string = exc.backtrace[frame]
|
| 133 |
match = /([^:]+):([0-9]+)/.match(string)
|
| 134 |
if match
|
| 135 |
file,line = match[1..2]
|
| 136 |
[:":location", [:":file", file], [:":line", line.to_i], :nil]
|
| 137 |
else
|
| 138 |
[:":error", "no src-loc for frame: #{string}"]
|
| 139 |
end
|
| 140 |
end
|
| 141 |
|
| 142 |
end
|
| 143 |
|
| 144 |
$rpc_entries = Hash.new
|
| 145 |
|
| 146 |
$rpc_entries[:"swank:connection-info"] = lambda do ||
|
| 147 |
[:":pid", $$,
|
| 148 |
:":package", [:":name", "ruby", :":prompt", "ruby> "],
|
| 149 |
:":lisp-implementation", [:":type", "Ruby",
|
| 150 |
:":name", "ruby",
|
| 151 |
:":version", RUBY_VERSION]]
|
| 152 |
end
|
| 153 |
|
| 154 |
def swank_interactive_eval(string)
|
| 155 |
eval(string,TOPLEVEL_BINDING).inspect
|
| 156 |
end
|
| 157 |
|
| 158 |
$rpc_entries[:"swank:interactive-eval"] = \
|
| 159 |
$rpc_entries[:"swank:interactive-eval-region"] = \
|
| 160 |
$rpc_entries[:"swank:pprint-eval"] = lambda { |string|
|
| 161 |
swank_interactive_eval string
|
| 162 |
}
|
| 163 |
|
| 164 |
$rpc_entries[:"swank:throw-to-toplevel"] = lambda {
|
| 165 |
throw :swank_top_level
|
| 166 |
}
|
| 167 |
|
| 168 |
$rpc_entries[:"swank:backtrace"] = lambda do |from, to|
|
| 169 |
conn, exc = $sldb_context
|
| 170 |
conn.sldb_backtrace(exc, from, to)
|
| 171 |
end
|
| 172 |
|
| 173 |
$rpc_entries[:"swank:frame-source-location"] = lambda do |frame|
|
| 174 |
conn, exc = $sldb_context
|
| 175 |
conn.frame_src_loc(exc, frame)
|
| 176 |
end
|
| 177 |
|
| 178 |
#ignored
|
| 179 |
$rpc_entries[:"swank:buffer-first-change"] = \
|
| 180 |
$rpc_entries[:"swank:operator-arglist"] = lambda do
|
| 181 |
:nil
|
| 182 |
end
|
| 183 |
|
| 184 |
$rpc_entries[:"swank:simple-completions"] = lambda do |prefix, pkg|
|
| 185 |
swank_simple_completions prefix, pkg
|
| 186 |
end
|
| 187 |
|
| 188 |
# def swank_simple_completions(prefix, pkg)
|
| 189 |
|
| 190 |
def read_packet(io)
|
| 191 |
header = read_chunk(io, 6)
|
| 192 |
len = header.hex
|
| 193 |
payload = read_chunk(io, len)
|
| 194 |
#$deferr.puts payload.inspect
|
| 195 |
read_sexp_from_string(payload)
|
| 196 |
end
|
| 197 |
|
| 198 |
def read_chunk(io, len)
|
| 199 |
buffer = io.read(len)
|
| 200 |
raise "short read" if buffer.length != len
|
| 201 |
buffer
|
| 202 |
end
|
| 203 |
|
| 204 |
def write_sexp_to_string(obj)
|
| 205 |
string = ""
|
| 206 |
write_sexp_to_string_loop obj, string
|
| 207 |
string
|
| 208 |
end
|
| 209 |
|
| 210 |
def write_sexp_to_string_loop(obj, string)
|
| 211 |
if obj.is_a? String
|
| 212 |
string << "\""
|
| 213 |
string << obj.gsub(/(["\\])/,'\\\\\1')
|
| 214 |
string << "\""
|
| 215 |
elsif obj.is_a? Array
|
| 216 |
string << "("
|
| 217 |
max = obj.length-1
|
| 218 |
obj.each_with_index do |e,i|
|
| 219 |
write_sexp_to_string_loop e, string
|
| 220 |
string << " " unless i == max
|
| 221 |
end
|
| 222 |
string << ")"
|
| 223 |
elsif obj.is_a? Symbol or obj.is_a? Numeric
|
| 224 |
string << obj.to_s
|
| 225 |
elsif obj == false
|
| 226 |
string << "nil"
|
| 227 |
elsif obj == true
|
| 228 |
string << "t"
|
| 229 |
else raise "Can't write: #{obj.inspect}"
|
| 230 |
end
|
| 231 |
end
|
| 232 |
|
| 233 |
def read_sexp_from_string(string)
|
| 234 |
stream = StringInputStream.new(string)
|
| 235 |
reader = LispReader.new(stream)
|
| 236 |
reader.read
|
| 237 |
end
|
| 238 |
|
| 239 |
class LispReader
|
| 240 |
def initialize(io)
|
| 241 |
@io = io
|
| 242 |
end
|
| 243 |
|
| 244 |
def read(allow_consing_dot=false)
|
| 245 |
skip_whitespace
|
| 246 |
c = @io.getc
|
| 247 |
case c
|
| 248 |
when ?( then read_list(true)
|
| 249 |
when ?" then read_string
|
| 250 |
when ?' then read_quote
|
| 251 |
when nil then raise EOFError.new("EOF during read")
|
| 252 |
else
|
| 253 |
@io.ungetc(c)
|
| 254 |
obj = read_number_or_symbol
|
| 255 |
if obj == :"." and not allow_consing_dot
|
| 256 |
raise "Consing-dot in invalid context"
|
| 257 |
end
|
| 258 |
obj
|
| 259 |
end
|
| 260 |
end
|
| 261 |
|
| 262 |
def read_list(head)
|
| 263 |
list = []
|
| 264 |
loop do
|
| 265 |
skip_whitespace
|
| 266 |
c = @io.readchar
|
| 267 |
if c == ?)
|
| 268 |
break
|
| 269 |
else
|
| 270 |
@io.ungetc(c)
|
| 271 |
obj = read(!head)
|
| 272 |
if obj == :"."
|
| 273 |
error "Consing-dot not implemented" # would need real conses
|
| 274 |
end
|
| 275 |
head = false
|
| 276 |
list << obj
|
| 277 |
end
|
| 278 |
end
|
| 279 |
list
|
| 280 |
end
|
| 281 |
|
| 282 |
def read_string
|
| 283 |
string = ""
|
| 284 |
loop do
|
| 285 |
c = @io.getc
|
| 286 |
case c
|
| 287 |
when ?"
|
| 288 |
break
|
| 289 |
when ?\\
|
| 290 |
c = @io.getc
|
| 291 |
case c
|
| 292 |
when ?\\, ?" then string << c
|
| 293 |
else raise "Invalid escape char: \\%c" % c
|
| 294 |
end
|
| 295 |
else
|
| 296 |
string << c
|
| 297 |
end
|
| 298 |
end
|
| 299 |
string
|
| 300 |
end
|
| 301 |
|
| 302 |
def read_quote
|
| 303 |
[:quote, read]
|
| 304 |
end
|
| 305 |
|
| 306 |
def read_number_or_symbol
|
| 307 |
token = read_token
|
| 308 |
if token.empty?
|
| 309 |
raise EOFError.new
|
| 310 |
elsif /^[0-9]+$/.match(token)
|
| 311 |
token.to_i
|
| 312 |
elsif /^[0-9]+\.[0-9]+$/.match(token)
|
| 313 |
token.to_f
|
| 314 |
else
|
| 315 |
token.intern
|
| 316 |
end
|
| 317 |
end
|
| 318 |
|
| 319 |
def read_token
|
| 320 |
token = ""
|
| 321 |
loop do
|
| 322 |
c = @io.getc
|
| 323 |
if c.nil?
|
| 324 |
break
|
| 325 |
elsif terminating?(c)
|
| 326 |
@io.ungetc(c)
|
| 327 |
break
|
| 328 |
else
|
| 329 |
token << c
|
| 330 |
end
|
| 331 |
end
|
| 332 |
token
|
| 333 |
end
|
| 334 |
|
| 335 |
def skip_whitespace
|
| 336 |
loop do
|
| 337 |
c = @io.getc
|
| 338 |
case c
|
| 339 |
when ?\s, ?\n, ?\t then next
|
| 340 |
when nil then break
|
| 341 |
else @io.ungetc(c); break
|
| 342 |
end
|
| 343 |
end
|
| 344 |
end
|
| 345 |
|
| 346 |
def terminating?(char)
|
| 347 |
" \n\t()\"'".include?(char)
|
| 348 |
end
|
| 349 |
|
| 350 |
end
|
| 351 |
|
| 352 |
|
| 353 |
class StringInputStream
|
| 354 |
def initialize(string)
|
| 355 |
@string = string
|
| 356 |
@pos = 0
|
| 357 |
@max = string.length
|
| 358 |
end
|
| 359 |
|
| 360 |
def pos() @pos end
|
| 361 |
|
| 362 |
def getc
|
| 363 |
if @pos == @max
|
| 364 |
nil
|
| 365 |
else
|
| 366 |
c = @string[@pos]
|
| 367 |
@pos += 1
|
| 368 |
c
|
| 369 |
end
|
| 370 |
end
|
| 371 |
|
| 372 |
def readchar
|
| 373 |
getc or raise EOFError.new
|
| 374 |
end
|
| 375 |
|
| 376 |
def ungetc(c)
|
| 377 |
if @pos > 0 && @string[@pos-1] == c
|
| 378 |
@pos -= 1
|
| 379 |
else
|
| 380 |
raise "Invalid argument: %c [at %d]" % [c, @pos]
|
| 381 |
end
|
| 382 |
end
|
| 383 |
|
| 384 |
end
|
| 385 |
|