Skip to content

Instantly share code, notes, and snippets.

@bradland
Created March 17, 2021 18:35
Show Gist options
  • Save bradland/b22f09552ef1a600030678518af90349 to your computer and use it in GitHub Desktop.
Save bradland/b22f09552ef1a600030678518af90349 to your computer and use it in GitHub Desktop.
Ping a host and announce its status; requires macOS or suitable replacement for the say command line app.
#!/usr/bin/env ruby
require 'logger'
require 'open3'
require 'optparse'
require 'ostruct'
require 'thread'
class ShellScript
::Version = [0,0,1]
attr_accessor :options
def initialize(args)
@options = OpenStruct.new
@options.count = nil
@options.log = '/dev/null' # Send logging data to /dev/null by default.
@options.log_level = Logger::INFO
@options.debug = false
@options.terse = false
opt_parser = OptionParser.new do |opt|
opt.banner = <<HEREDOC
NAME
pingr - ping reporter audibly reports the status of a host
SYNOPSIS
#{File.basename($0)} [OPTION]... [HOST]...
HEREDOC
opt.on("-cCOUNT","--count=COUNT","Limit ping check to COUNT pings.") do |count|
@options.count = count
end
opt.on("-d","--debug","Log debug messages.") do
@options.log_level = Logger::DEBUG
end
opt.on("-IINTERFACE","--iface=INTERFACE","Bind ping to INTERFACE for sending.") do |interface|
@options.interface = interface
end
opt.on("-lOUTFILE","--log=OUTFILE","Log debugging information to a filename, STDOUT, or STDERR.") do |filename|
case filename
when 'STDOUT'
@options.log = $stdout
when 'STDERR'
@options.log = $stederr
else
@options.log = filename
end
end
opt.on("-t","--terse","Terse output; don't bother saying the hostname.") do
@options.terse = true
end
# Default options (these aren't specific to this app).
opt.on_tail("-h","--help","Print usage information.") do
$stderr.puts opt_parser
exit 0
end
opt.on_tail("--version", "Show version and exit.") do
puts ::Version.join('.')
exit 0
end
end
begin
opt_parser.parse!
rescue OptionParser::InvalidOption => e
$stderr.puts "Specified #{e}"
$stderr.puts opt_parser
exit 64 # EX_USAGE
end
if ARGV.size < 1
$stderr.puts "No IP address provided."
$stderr.puts opt_parser
exit 64 # EX_USAGE
end
unless say?
$stderr.puts red("The say command is not available on this system.")
exit 78 #EX_CONFIG
end
@host = ARGV.pop
@host_say = @options.terse ? nil : @host
@host_status = nil
@say_queue = Queue.new
setup_logger
end
def run!
announce
ping
end
# Update host status; called for every line of output (STDERR and STDOUT) from the ping command.
def update_status(line)
previous_status = @host_status
current_status = parse_line(line)
return nil if current_status == :ignore
@host_status = current_status
if @host_status != previous_status
case @host_status
when :up
add_announcement "Host #{@host_say} is up."
when :down
add_announcement "Host #{@host_say} is down."
else
add_announcement "Unknown status encountered."
end
end
end
# Add an announcement to the queue.
def add_announcement(line)
@say_queue.clear
@say_queue << line
end
# Parse the output of ping and return a host status.
def parse_line(line)
status = :ignore
case line
when /^PING/
status = :ignore
when /localhost/ # If you've reached localhost, you've screwed something up, or you're testing.
status = :ignore
when /timeout/i # Host is down if 'timeout' is found in line.
status = :down
when /unreachable/i # Host is down if network is unreachable.
status = :down
when /no route to host/i # Host is down if no route exists.
status = :down
when /time to live exceeded/i # Host is down if TTL is exceeded.
status = :down
when /ttl expired/i # Host is down if TTL is exceeded (Linux version).
status = :down
when /bytes from/i # Host is up if 'bytes from' is found in line.
status = :up
end
@logger.debug "Status: #{status.to_s.rjust(6)} Line: '#{line}'"
return status
end
# Ping the host.
def ping
cmd = []
cmd << "ping"
cmd << "-c #{@options.count}" if @options.count
cmd << "#{flag_for_iface} #{options.interface}" if @options.interface
cmd << @host
cmd = cmd.join(' ')
puts "Executing: #{blue(cmd)}"
# Use Open3#popen2e so we get both stdout and stderr content in one stream.
Open3.popen2e(cmd) do |stdin, stdout_and_stderr, thread|
while line = stdout_and_stderr.gets do
$stderr.puts line
update_status line.chomp
end
end
end
# Spawns an announcer thread that looks for enqueued messages and speaks them aloud.
def announce
Thread.new do
loop do
say @say_queue.pop
end
end
end
# Say a message aloud.
def say(msg)
`say #{msg}`
end
# Check to make sure the say command is available on the system.
def say?
system('which say > /dev/null 2>&1')
end
# Return or setup logger
def setup_logger
@logger ||= Logger.new(@options.log)
@logger.level = @options.log_level
end
def flag_for_iface
case platform
when :linux
"-I"
when :osx
"-b"
else
"-I"
end
end
def platform
case `uname -a`
when /Linux/i
return :linux
when /Darwin/i
return :osx
else
return :unkown
end
end
## Embedded library methods; because, scripting!
def colorize(text, color_code)
"\e[#{color_code}m#{text}\e[0m"
end
def red(text); colorize(text, 31); end
def green(text); colorize(text, 32); end
def blue(text); colorize(text, 34); end
end
begin
if $0 == __FILE__
ShellScript.new(ARGV).run!
end
rescue Interrupt
# Ctrl^C
Thread.list.each do |thread|
thread.exit unless thread == Thread.current
end
exit 130
rescue Errno::EPIPE
# STDOUT was closed
exit 74 # EX_IOERR
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment