How To Display Characters Received Via A Socket? - ruby

I have a very simple Ruby program that acts as an "echo server". When you connect to it via telnet any text you type is echoed back. That part is working. If I add a 'putc' statement to also print each received character on the console running the program only the very first character displayed is printed. After that it continues to echo things back to the telnet client but there is nothing printed on the console.
The following is a small, stripped down program that exhibits the problem.
I am very new to Ruby and have probably made a typical rookie mistake. What did I do wrong?
require 'socket'
puts "Simple Echo Server V1.0"
server = TCPServer.new('127.0.0.1', '2150')
cbuf = ""
while socket = server.accept
cbuf = socket.readchar
socket.putc cbuf
putc cbuf
end

The problem is that your code is only running the while loop once for every time somebody connects (TCPServer#accept accepts a connection). Try something more like:
require 'socket'
puts "Simple Echo Server V1.0"
server = TCPServer.new('127.0.0.1', '2150')
socket = server.accept
while line = socket.readline
socket.puts line
puts line
end

Related

Ruby and Websockets

I'm trying to create an asynchronous connection to a device that broadcasts packets when something changes, like a door is opened. I want to process the feedback in realtime to the control system. I also need to try to do this with the standard library due to the limitations on some controllers.
Right now I've been using cURL inside ruby to keep a connection open and reconnect if it disconnects. That has worked fine, but on macOS Big Sur after a few days terminal stops working due to the requests. I have not been able to figure out why.
I've rewritten most of my script to use net/http instead of cURL, but I can't figure out keeping a connection open and then real-time sending data to another function.
cURL Ruby Code:
def httpBcast(cmd, url, header,valuesListID,valuesListFID)
fullCommand = "curl -s -k -X #{cmd} '#{url}' #{header}"
loop do
begin
PTY.spawn( fullCommand ) do |stdout, stdin, pid|
begin
# Send Output to Savant
stdout.each { |line| parseBcast(line,valuesListID,valuesListFID)}
rescue Errno::EIO
puts "Errno:EIO error, but this probably just means " +
"that the process has finished giving output"
end
end
rescue PTY::ChildExited
puts "The child process exited!"
end
end
end
def parseBcast(msg='',valuesListID,valuesListFID)
if msg.start_with?("data:")
msg.gsub!(/\n+/, '')
msg.gsub!(/\s+/, "")
msg.gsub!("data:","")
msg.to_json
msgP = JSON.parse(msg)
if valuesListFID.include?(msgP['result']['deviceId'])
id = valuesListFID.index(msgP['result']['deviceId'])
id +=1
else
id = "Device Doesn't Exist"
end
msgP['result']['deviceId'] = id
send_savant msgP.to_json
end
end
Any guidance anyone can offer would be most appreciated.
Turns out the vendor needed to make a firmware update. All Good Now.

Basic Ruby http server not displaying .jpg to localhost on win7?

This http script works just fine from my gnome-terminal on Ubuntu (and per Aleksey, on Mac), but on win7, a small square gets loaded in the chrome browser. What do I need to do to get the JPEG sent through local host so it displays in the win7 browser? Per Holger's comment, I need to address the content encoding, but everything I've tried so far makes no difference on win7 (and still loads fine in Ubuntu without any explicit content encoding). ?.
PS C:\Users\user_name\Ruby\http_test> ls
basic_http.rb
lolcat.jpg
PS C:\Users\user_name\Ruby\http_test> ruby basic_http.rb
# very basic http server
require 'socket'
def send_200(socket, content)
socket.puts "HTTP/1.1 200 OK\r\n\r\n#{content}" # <-- Correct? (Per Holger)
socket.close
end
server = TCPServer.new 2016
loop do
Thread.start(server.accept) do |client|
request = client.gets
if request.start_with?("GET")
url = request.split(" ")[1]
if url.start_with?("/images/")
file = url.sub("/images/", "")
picture = File.read(file) # <-- initially Aleksey pointed out
send_200(client, picture) # <-- a variable name mismatch here
else # pictures/picture... heh.
send_200(client, "hello!")
end
end
end
end
FWIW: Ruby 2.2, win7 & coding along with this demo.
You just have a typo in variable name.
You read file to pictures
pictures = File.read(file)
but you send it as picture
send_200(client, picture)
So you just need to edit variable name.
Maybe it would be a good idea to wrap request procession into begin block.
Thread.start(server.accept) do |client|
begin
...
rescue => ex
puts ex
end
end
This way you can see if something goes wrong.
To get the jpeg loaded into the browser on a win7 system, the File.read command needs to explicitly address the binary content encoding, e.g. File.binread("foo.bar") per:
https://ruby-doc.org/core-2.1.0/IO.html#method-c-binread
if url.start_with?("/images/")
file = url.sub("/images/", "")
picture = File.binread(file) # <-- Thank you Holger & Aleksey!!!
send_200(client, picture)
Thanks to Aleksey and Holger!

Is it okay to connect raw ruby sockets to event machine UNIX server?

I have a UNIX server started and the code goes like:
module UNIX_Server
def receive_data(data)
send_data "testing"
end
def unbind
puts "[server] client disconnected."
end
end
EM::run {
EM::start_unix_domain_server('/tmp/file.sock', UNIX_Server)
}
This works fine, and I am trying to connect to this using a Ruby 1.8.7
UNIX Socket:
s = UNIXSocket.new
s.puts "test"
s.gets
The problem here is that my gets method seems to hang and the client
only gets data when I do a Ctrl-C and terminate the server. What am I
missing here?
IO#gets reads a whole line at a time. Your client is waiting for the newline char which your server never sends.
Using
send_data "testing\n" # note the newline character
in your server should work, or you could use IO#getc in a loop.

Reading / Writing from a Unix Socket in Ruby

I'm trying to connect, read and write from a UNIX socket in Ruby. It is a stats socket used by haproxy.
My code is the following:
require 'socket'
socket = UNIXSocket.new("/tmp/haproxy.stats.socket")
# First attempt: works
socket.puts("show stat")
while(line = socket.gets) do
puts line
end
# Second attemp: fails
socket.puts("show stat")
while(line = socket.gets) do
puts line
end
It succeeds the first time, but on the second attempt fails. I'm not sure why.
# pxname,svname,qcur,qmax,scur,smax,slim,stot,bin,bout,dreq,dresp,ereq,econ,eresp,wretr,wredis,status,weight,act,bck,chkfail,chkdown,lastchg,downtime,qlimit,pid,iid,sid,throttle,lbtot,tracked,type,rate,rate_lim,rate_max,check_status,check_code,check_duration,hrsp_1xx,hrsp_2xx,hrsp_3xx,hrsp_4xx,hrsp_5xx,hrsp_other,hanafail,req_rate,req_rate_max,req_tot,cli_abrt,srv_abrt,
stats,FRONTEND,,,0,0,2000,0,0,0,0,0,0,,,,,OPEN,,,,,,,,,1,1,0,,,,0,0,0,0,,,,0,0,0,0,0,0,,0,0,0,,,
stats,BACKEND,0,0,0,0,2000,0,0,0,0,0,,0,0,0,0,UP,0,0,0,,0,22,0,,1,1,0,,0,,1,0,,0,,,,0,0,0,0,0,0,,,,,0,0,
legacy_socket,FRONTEND,,,0,0,1000,0,0,0,0,0,0,,,,,OPEN,,,,,,,,,1,2,0,,,,0,0,0,0,,,,0,0,0,0,0,0,,0,0,0,,,
all,FRONTEND,,,0,0,10000,0,0,0,0,0,0,,,,,OPEN,,,,,,,,,1,3,0,,,,0,0,0,0,,,,0,0,0,0,0,0,,0,0,0,,,
socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,1,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0,
socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,2,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0,
socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,3,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0,
socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,4,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0,
socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,5,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0,
socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,6,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0,
socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,22,22,,1,4,7,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0,
socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,21,21,,1,4,8,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0,
socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,21,21,,1,4,9,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0,
socket_backend,socket,0,0,0,0,200,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,21,21,,1,4,10,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0,
socket_backend,BACKEND,0,0,0,0,0,0,0,0,0,0,,0,0,0,0,DOWN,0,0,0,,1,21,21,,1,4,0,,0,,1,0,,0,,,,0,0,0,0,0,0,,,,,0,0,
api_backend,api,0,0,0,0,200,0,0,0,,0,,0,0,0,0,UP,1,1,0,0,0,22,0,,1,5,1,,0,,2,0,,0,L4OK,,0,0,0,0,0,0,0,0,,,,0,0,
api_backend,api,0,0,0,0,1,0,0,0,,0,,0,0,0,0,UP,1,1,0,0,0,22,0,,1,5,2,,0,,2,0,,0,L4OK,,0,0,0,0,0,0,0,0,,,,0,0,
api_backend,api,0,0,0,0,1,0,0,0,,0,,0,0,0,0,DOWN,1,1,0,0,1,21,21,,1,5,3,,0,,2,0,,0,L4CON,,0,0,0,0,0,0,0,0,,,,0,0,
api_backend,BACKEND,0,0,0,0,0,0,0,0,0,0,,0,0,0,0,UP,2,2,0,,0,22,0,,1,5,0,,0,,1,0,,0,,,,0,0,0,0,0,0,,,,,0,0,
www_backend,ruby-www,0,0,0,0,10000,0,0,0,,0,,0,0,0,0,UP,1,1,0,0,0,22,0,,1,6,1,,0,,2,0,,0,L4OK,,0,0,0,0,0,0,0,0,,,,0,0,
www_backend,BACKEND,0,0,0,0,0,0,0,0,0,0,,0,0,0,0,UP,1,1,0,,0,22,0,,1,6,0,,0,,1,0,,0,,,,0,0,0,0,0,0,,,,,0,0,
/Users/Olly/Desktop/haproxy_stats.rb:14:in `write': Broken pipe (Errno::EPIPE)
from /Users/Olly/Desktop/haproxy_stats.rb:14:in `puts'
from /Users/Olly/Desktop/haproxy_stats.rb:14
What is the problem? Is there a good reference to using UNIX sockets and Ruby?
Olly,
HAproxy closes connection after the first request unless you use the "prompt" command (see http://haproxy.1wt.eu/download/1.4/doc/configuration.txt section 9.2) :
#!/usr/bin/env ruby
require 'socket'
socket = UNIXSocket.new("/tmp/haproxy.stats.socket")
# Goes interactive mode
socket.puts("prompt")
# Ask statistics every second
while true
socket.puts("show stat")
socket.each_char do |c|
# We had the prompt, break out
break if c == '>'
print c
end
sleep 1
end
Looks like the connection has been closed after the first request. I don't think you are doing anything wrong. The HAProxy stats socket is probably designed so that it responds to a single command and then closes the connection.
I think you need to reconnect for each request.
If you look at this blog post which is about using HAProxy stats socket with socat then this makes sense because you pipe the show stat command into socat and socat reads from the socket until it closes.
I also encountered the same problem when use socket.puts, you can use socket.write instead of socket.puts to fix it.
#!/usr/bin/evn ruby
# -*- coding: UTF-8 -*-
require 'rubygems'
require 'uri'
require 'socket'
require 'yaml'
SOCKET = URI.parse("/var/run/haproxy/haproxy.sock")
def get_info
UNIXSocket.open(SOCKET.path) do |socket|
socket.write("show info;")
info = YAML::load socket
#info.each {|key, value| puts "#{key} ➤ #{value}"}
end
end
puts get_info["Uptime_sec"]
Look this gem for more information, source code is here.
You can use man socket. You can use the socket-class just like you would if it was a C-function.
I found man-pages to be very useful.

Exposing console apps to the web with Ruby

I'm looking to expose an interactive command line program via JSON or another RPC style service using Ruby. I've found a couple tricks to do this, but im missing something when redirecting the output and input.
One method at least on linux is to redirect the stdin and stdout to a file then read and write to that file asynchronously with file reads and writes. Another method ive been trying after googling around was to use open4. Here is the code I wrote so far, but its getting stuck after reading a few lines from standard output.
require "open4"
include Open4
status = popen4("./srcds_run -console -game tf +map ctf_2fort -maxplayers 6") do |pid, stdin, stdout, stderr|
puts "PID #{pid}"
lines=""
while (line=stdout.gets)
lines+=line
puts line
end
while (line=stderr.gets)
lines+=line
puts line
end
end
Any help on this or some insight would be appreciated!
What I would recommend is using Xinetd (or similar) to run the command on some socket and then using the ruby network code. One of the problems you've already run into in your code here is that your two while loops are sequential, which can cause problems.
Another trick you might try is to re-direct stderr to stdout in your command, so that your program only has to read the stdout. Something like this:
popen4("./srcds_run -console -game tf +map ctf_2fort -maxplayers 6 2>&1")
The other benefit of this is that you get all the messages/errors in the order they happen during the program run.
EDIT
Your should consider integrating with AnyTerm. You can then either expose AnyTerm directly e.g. via Apache mod_proxy, or have your Rails controller act as the reverse proxy (handling authentication/session validation, then playing back controller.request minus any cookies to localhost:<AnyTerm-daemon-port>, and sending back as a response whatever AnyTerm replies with.)
class ConsoleController < ApplicationController
# AnyTerm speaks via HTTP POST only
def update
# validate session
...
# forward request to AnyTerm
response = Net::HTTP.post_form(URI.parse('http://localhost:#{AnyTermPort}/', request.params))
headers['Content-Type'] = response['Content-Type']
render_text response.body, response.status
end
Otherwise, you'd need to use IO::Select or IO::read_noblock to know when data is available to be read (from either network or subprocess) so you don't deadlock. See this too. Also check that either your Rails is used in a multi-threaded environment or that your Ruby version is not affected by this IO::Select bug.
You can start with something along these lines:
status = POpen4::popen4("ping localhost") do |stdout, stderr, stdin, pid|
puts "PID #{pid}"
# our buffers
stdout_lines=""
stderr_lines=""
begin
loop do
# check whether stdout, stderr or both are
# ready to be read from without blocking
IO.select([stdout,stderr]).flatten.compact.each { |io|
# stdout, if ready, goes to stdout_lines
stdout_lines += io.readpartial(1024) if io.fileno == stdout.fileno
# stderr, if ready, goes to stdout_lines
stderr_lines += io.readpartial(1024) if io.fileno == stderr.fileno
}
break if stdout.closed? && stderr.closed?
# if we acumulated any complete lines (\n-terminated)
# in either stdout/err_lines, output them now
stdout_lines.sub!(/.*\n/m) { puts $& ; '' }
stderr_lines.sub!(/.*\n/m) { puts $& ; '' }
end
rescue EOFError
puts "Done"
end
end
To also handle stdin, change to:
IO.select([stdout,stderr],[stdin]).flatten.compact.each { |io|
# program ready to get stdin? do we have anything for it?
if io.fileno == stdin.fileno && <got data from client?>
<write a small chunk from client to stdin>
end
# stdout, if ready, goes to stdout_lines

Resources