Created
March 17, 2021 18:35
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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