Search code examples
rubyjsontelnet

Ruby, Telnet, read multiline response without timeout


I need some hints/help, how can I read multiline response into variable. My current command results me multiline response but after that I get timeout.

Here's how my connection is setup:

connection = Net::Telnet.new('Host' => host,'Port' => 4800, 'Telnetmode' => false, 'Timeout' => 1)

Here's my request and how I save it:

puts "Weather request\n"
connection.cmd("{weather}"){ |c| print c }
parsed = JSON.parse(str)
puts "#{parsed}\n\n"

And here's the error:

/usr/lib/ruby/1.9.1/net/telnet.rb:558:in `waitfor': timed out while waiting for more data (Timeout::Error)
    from /usr/lib/ruby/1.9.1/net/telnet.rb:695:in `cmd'
    from ruby_check.rb:37:in `<main>'

My response is multiple JSON lines, like this:

{"City":"Tallinn", "Degrees":"23"}
{"City":"Berlin", "Degrees":"23"}
{"City":"Helsinki", "Degrees":"23"}
{"City":"Stockholm", "Degrees":"23"}

Solution

  • Why the timeout?

    The Net::Telnet documentation says:

    For some protocols, it will be possible to specify the Prompt option once when you create the Telnet object and use cmd() calls; for others, you will have to specify the response sequence to look for as the Match option to every cmd() call, or call puts() and waitfor() directly; for yet others, you will have to use sysread() instead of waitfor() and parse server responses yourself.

    This makes more sense when combined with the Net::Telnet#cmd method's documentation, which says that the method:

    sends a string to the host, and reads in all received data until is sees the prompt or other matched sequence.

    You're not specifying a custom Prompt or Match option, so #cmd is waiting for something from the server that matches the default Net::Telnet prompt (/[$%#>] \z/n) to indicate the end of the message. If the message doesn't end with that kind of prompt, then it'll be waiting forever.

    Possible solutions

    Match the server's prompt

    If the server does send some kind of prompt to indicate it's finished sending data and you should type the next command, you can pass a regular expression that matches it to the Net::Telnet initialiser. For example, if the server prompted you with command:, you could use:

    connection = Net::Telnet.new(
      "Prompt" => /command: \z/,
      # …
    )
    

    Match the end of the response

    If there's no prompt, but the response you're waiting for ends with a specific character sequence, you could explicitly specify the Match option when you call #cmd. For example, if your response was a single JSON array it would end with ], so you might be able to use this:

    connection.cmd("String" => "{weather}", "Match" => "]") { |c| print c }
    

    Give up on Net::Telnet and use a TCPSocket

    If there's no prompt and no known ending, you could try to use the Net::Telnet object's underlying TCPSocket to read the data without using #cmd:

    connection.puts("{weather}")
    connection.sock.readline
    

    At this point, there might not be much benefit to using Net::Telnet over a plain TCPSocket.