class WifiWatch
Constants
- Default_count
- Default_host
- Program
- Version
Public Instance Methods
beautiful_times()
click to toggle source
Make history lines pretty. We want the history minutes to be modulo the accounting minutes. Where possible we want to let the ping counts to be the same. Ping can drift backward by one second and forward by a few seconds each run. Overall the drift is usually forward so when we need to make an adjustment we aim for 10 seconds into the next aligned minute.
# File bin/wifi-watch, line 358 def beautiful_times() count = @options.count # Is the target count minute aligned? if count % 60 != 0 return count # Not minute aligned. end # Is the target count hour aligned? minutes = count / 60 if 60 % minutes != 0 return count # Not hour aligned. end # See if we are already suitably aligned right now. now = Time.now mm = now.min ss = now.sec if mm % minutes == 0 && 1 <= ss && ss <= 50 return count # Now is minute aligned and within minute drift limits. end # Target 10 seconds into the next aligned minute. short_mm = minutes - (mm % minutes) count = (short_mm * 60) + 10 - ss return count end
commit_line()
click to toggle source
If there is a current display line, commit it to history.
# File bin/wifi-watch, line 407 def commit_line() if @display_line if @ping_from # Debug mode show final line value as history. puts @display_line else # Normal mode advance to the next line leaving this line as history. puts end @display_line = nil end end
display_line(line)
click to toggle source
Rewrite the current display line or save it for final debugging result.
# File bin/wifi-watch, line 397 def display_line(line) @display_line = line unless @ping_from # Display this current line. STDOUT.write "\r" # Move to the start of this line. STDOUT.write line # Write over it. end end
get_options()
click to toggle source
# File bin/wifi-watch, line 85 def get_options options = OpenStruct.new options.host = Default_host options.count = Default_count options.periods = 0 opt_parser = OptionParser.new do |opt| opt.banner = "Usage: #{Program} [OPTIONS]" opt.separator "" opt.on("-h", "--host host", "Host to ping [#{options.host}].") do |host| options.host = host end opt.on("-c", "--count seconds", Integer, "Accounting period length [#{options.count} seconds].") do |count| raise "--count must be positive" unless count > 0 options.count = count end opt.on("-p", "--periods periods", Integer, "Accounting periods to run [infinite].") do |periods| options.periods = periods end opt.on("--ping-to file", "Write ping output to a file for debugging.") do |ping_to| options.ping_to = ping_to end opt.on("--ping-from file", "Read ping input from a file ('-' for stdin).") do |ping_from| options.ping_from = ping_from end opt.on("-v", "--version", "Version.") do puts Version exit end opt.on("--help", "This message.") do puts puts opt_parser puts exit end opt.separator "" opt.separator "Documentation at https://github.com/jgorman/wifi-watch" end begin opt_parser.parse! rescue => e puts puts e puts puts opt_parser puts exit end return options end
hhmm_line(time, line)
click to toggle source
# File bin/wifi-watch, line 386 def hhmm_line(time, line) line = "#{time.strftime('%H:%M')} #{line}" display_line(line) end
hhmmss_line(time, line)
click to toggle source
# File bin/wifi-watch, line 391 def hhmmss_line(time, line) line = "#{time.strftime('%H:%M:%S')} #{line}" display_line(line) end
ping_period(stdin)
click to toggle source
Run ping for one accounting period.
# File bin/wifi-watch, line 152 def ping_period(stdin) current_mode = nil # Are we currently "good" or "fail"? mode_count = 0 # For how long in a row? total_count = 0 # Pings so far. totally_good = 0 # Successful pings. ping_ms = 0.0 # Latest successful ping time in milliseconds. packets_line = '' # Ping packets received summary line. round_trip_line = '' # Ping round-trip milliseconds summary line. filter_mode = false # Wifi login filter is active. line_time = nil # Latest line timestamp. first_byte = nil # DNS lookup complete read test byte. # Long DNS lookups can effectively freeze ping for a while. # To reproduce: turn VPN off and then on, then turn WiFi off. # This logic is impractical to simulate in a meaningful way # so we just pass the messages on for later debugging display. waiting_start = Time.now last_notified = waiting_start until first_byte begin first_byte = stdin.read_nonblock(1) rescue IO::EAGAINWaitReadable now = Time.now if now - last_notified >= 1 hhmmss_line(now, "Waiting for dns lookup of #{@options.host} ...") if @ping_to @ping_to.puts "#{now.to_i} DNS_LOOKUP #{@display_line}" @ping_to.flush end last_notified = now end sleep 0.2 # Be nice to the cpu. rescue break # Some systems throw an error at eof. end end if last_notified > waiting_start commit_line() end # Process each line of ping output. while true begin line = stdin.gets rescue break # Some systems throw an error at eof. end break unless line line.chomp! if first_byte line = first_byte + line first_byte = nil end if @ping_from # Read from the debugging source file up to the end of this ping run. if line == "END_OF_PING" break # End this ping accounting period run. end # Simulate the past by loading the effective time for this line. time, line = line.split(" ", 2) line_time = Time.at(time.to_i) else line_time = Time.now end if @ping_to # Capture this ping output line with the timestamp for later debugging. @ping_to.puts "#{line_time.to_i} #{line}" @ping_to.flush end # DNS failure modes. if line =~ /cannot resolve/ # ping: cannot resolve ns.google.com: Unknown host hhmmss_line(line_time, line) sleep 1 unless @ping_from # Speed up testing. @dns_failing = true elsif line =~ /DNS_LOOKUP (.*)/ # Waiting for dns lookup of ns.google.com ... display_line($1) @dns_failing = true elsif @dns_failing # If DNS was previously failing, pop to a fresh line. commit_line() @dns_failing = false end # This indicates a ping success or failure line. Nil means neither. new_mode = nil # Parse the line. if line =~ /PING/ # # The first ping output line shows the destination host and ip. # # PING ns.google.com (216.239.32.10): 56 data bytes # unless @shown_header puts line # Show the destination name and ip. puts puts "Time Ping Run Mode Failed Round Trip" puts "----- ---- ---- ---- ------ ----------" @shown_header = true # Only show these once per wifi-watch. end elsif line =~ /bytes from/ if line =~ /time=(\S+)/ # # Success! Record the round trip time. # # 64 bytes from 104.27.146.148: icmp_seq=48 ttl=61 time=24.780 ms # ping_ms = $1.to_f new_mode = 'good' if filter_mode commit_line() # Keep the filter message line. filter_mode = false end else # # Here the wifi login has timed out and is blocking packets. # Multi line ping output shows both the timeout and filter message # lines plus some additional diagnostic lines that we ignore. # # Request timeout for icmp_seq 5 # 92 bytes from 10.128.128.128: Communication prohibited by filter # Vr HL TOS Len ID Flg off TTL Pro cks Src Dst # 4 5 00 5400 6dea 0 0000 40 01 21dd 10.57.228.249 104.27.147.148 # unless filter_mode commit_line() # Keep the last unfiltered status line. filter_mode = true end hhmmss_line(line_time, line) end elsif line =~ /timeout/ # Request timeout for icmp_seq 3 new_mode = 'fail' elsif line =~ /transmitted/ # 5 packets transmitted, 5 packets received, 0.0% packet loss packets_line = line elsif line =~ /round-trip/ # round-trip min/avg/max/stddev = 0.063/0.082/0.122/0.022 ms round_trip_line = line end if new_mode # Update the counts with success or failure. if new_mode != current_mode current_mode = new_mode mode_count = 0 end mode_count += 1 totally_good += 1 if current_mode == 'good' total_count += 1 unless filter_mode show_stats(line_time, total_count, totally_good, current_mode, mode_count, ping_ms) end end end if @ping_to # Insert a ping run separator line into the debugging file. @ping_to.puts "END_OF_PING" @ping_to.flush end # Final summary line. if packets_line =~ /(\d+) packets transmitted, (\d+) packets received/ total_count = $1.to_i totally_good = $2.to_i if round_trip_line =~ / (\S+) ms/ avg_ms = $1.split('/')[1].to_f # Extract the average time. end show_stats(line_time, total_count, totally_good, 'good', totally_good, avg_ms) end unless @dns_failing commit_line() # Leave this summary line as history. end end
run()
click to toggle source
# File bin/wifi-watch, line 14 def run @shown_header = false # Have we shown the column headers yet? @dns_failing = false # Are DNS lookups currently failing? @display_line = nil # The current display line. @ping_to = nil # Save timestamped ping output here. @ping_from = nil # Read timestamped ping input here. @options = get_options() if @options.ping_to begin @ping_to = File.open(@options.ping_to, "w") rescue => e puts puts "ping_to=#{@options.ping_to}: #{e}" puts exit end end if @options.ping_from # Debug mode simulates ping output from a captured --ping-to file. begin if @options.ping_from == "-" @ping_from = STDIN else @ping_from = File.open(@options.ping_from, "r") end rescue => e puts puts "ping_from=#{@options.ping_from}: #{e}" puts exit end # Call ping_period() for each ping run section. until @ping_from.eof? ping_period(@ping_from) # Reads up to next "END_OF_PING" line. end if @dns_failing commit_line() # Show that we ended up in dns failure mode. end else # Run ping multiple times. periods = 0 while true count = beautiful_times() cmd = "ping -c #{count} #{@options.host}" stdin, stdout, pid = PTY.spawn( cmd ) ping_period(stdin) # Run one accounting period. Process.waitall() # Soak up any zombie pings. if @options.periods > 0 periods += 1 if periods >= @options.periods break end end end end rescue Interrupt commit_line() ensure if @ping_to @ping_to.close end end
show_stats(line_time, total_count, totally_good, current_mode, mode_count, ms)
click to toggle source
Show the statistics.
# File bin/wifi-watch, line 344 def show_stats(line_time, total_count, totally_good, current_mode, mode_count, ms) loss = ((total_count - totally_good) * 100.0) / total_count line = sprintf("%4d %4d %s %5.1f%% %7.2f ms", total_count, mode_count, current_mode, loss, ms) hhmm_line(line_time, line) end