#! /usr/bin/ruby # Copyright 2002 Neil Spring # GPL # report bugs to wmbiff-devel@lists.sourceforge.net # or (preferred) use the debian BTS via 'reportbug' # Based on security-update-check.py by Rob Bradford require 'net/http' #require 'profile' # re-fetch interval - only bug the server once every hour. # allows wmbiff to ask us often how many packages have been # updated so that the number goes back to cyan (old) from # yellow (new) quickly on upgrade. # this still doesn't mean we grab the whole file. we get # if-modified-since. it just means we don't connect to the # server more often than this. # 6 hours * 60 min/hour * 60 sec/min Refetch_Interval_Sec = 6 * 60 * 60 # as an ordinary user, we store Packages in the home directory. Cachedir = ENV['HOME'] + '/.wmbiff-sdr' # look for updates from this server. This script is designed around # (and simplified greatly by) using just a single server. Server = 'security.debian.org' # extend the Array class with a max method. class Array def inject(n) each { |value| n = yield(n, value) } n end def max inject(0) { |n, value| ((n > value) ? n : value) } end end def debugmsg(str) $stderr.puts str if($VERBOSE) end # to be reimplemented without execing touch. def touch(filename) debugmsg "touching #{filename}" Kernel.system('/usr/bin/touch ' + filename) end # to be reimplemented without execing dpkg, though running # dpkg excessively doesn't seem to be a bottleneck. def version_a_gt_b(a, b) cmd = "/usr/bin/dpkg --compare-versions %s le %s" % [ a, b ] # $stderr.puts cmd return (!Kernel.system(cmd)) end # figure out which lists to check # there can be many implementations of # this behavior, this seemed simplest. # we're going to make an array of arrays, for each package # file, the url, the system's cache of the file, and a # per-user cache of the file. packagelists = Dir.glob("/var/lib/apt/lists/#{Server}*Packages").map { |pkgfile| [ pkgfile.gsub(/.*#{Server}/, '').tr('_','/'), # the url path pkgfile, # the system cache of the packages file. probably up-to-date. # and finally, a user's cache of the page, if needed. "%s/%s" % [ Cachedir, pkgfile.gsub(/.*#{Server}_/,'') ] ] } # we'll open a persistent session, but only if we need it. session = nil # update the user's cache if necessary. packagelists.each { |urlpath, sc, uc| sctime = File.stat(sc).mtime cached_time = if(test(?e, uc)) then uctime = File.stat(uc).mtime if ( uctime < sctime ) then # we have a user cache, but it is older than the system cache File.unlink(uc) # delete the obsolete user cache. sctime else uctime end else # the user cache doesn't exist, but we might have # talked to the server recently. if(test(?e, uc + '.stamp')) then File.stat(uc + '.stamp').mtime else sctime end end if(Time.now > cached_time + Refetch_Interval_Sec) then debugmsg "fetching #{urlpath} %s > %s + %d" % [Time.now, cached_time, Refetch_Interval_Sec] begin if(session == nil) then session = Net::HTTP.new(Server) # session.set_pipe($stderr); end begin # the warning with ruby1.8 on the following line # has to do with the resp, data bit, which should # eventually be replaced with (copied from the # docs with the 1.8 net/http.rb) # response = http.get('/index.html') # puts response.body resp, data = session.get(urlpath, { 'If-Modified-Since' => cached_time.strftime( "%a, %d %b %Y %H:%M:%S GMT" ) }) rescue SocketError => e # if the net is down, we'll get this error; avoid printing a stack trace. puts "XX old" puts e exit 1; rescue Timeout::Error => e # if the net is down, we might get this error instead. # but there is no good reason to print the specific exception. (execution expired) puts "XX old" exit 1; end test(?e, Cachedir) or Dir.mkdir(Cachedir) File.open(uc, 'w') { |o| o.puts data } test(?e, uc + '.stamp') and File.unlink(uc + '.stamp') # we have a copy, don't need the stamp. debugmsg "urlpath updated" rescue Net::ProtoRetriableError => detail head = detail.data if head.code != "304" raise "unexpected error occurred: " + detail end test(?e, Cachedir) or Dir.mkdir(Cachedir) if(test(?e, uc)) then touch(uc) else # we didn't get an update, but we don't have a cached # copy in the user directory. touch(uc + '.stamp') end end else debugmsg "skipping #{urlpath}" end } available = Hash.new package = nil packagelists.each { |url, sc, uc| File.open( (test(?e, uc)) ? uc : sc, 'r').each { |ln| if(m = /^Package: (.*)/.match(ln)) then package = m[1] elsif(m = /^Version: (.*)/.match(ln)) then available[package] = m[1] end } } installed = Hash.new package = nil isinstalled = false File.open('/var/lib/dpkg/status').each { |ln| if(m = /^Package: (.*)$/.match(ln)) then package = m[1] isinstalled = false # reset elsif(m = /^Status: install ok installed/.match(ln)) then isinstalled = true elsif(m = /^Version: (.*)$/.match(ln)) then isinstalled && installed[package] = m[1] end } debugmsg "%d installed, %d available" % [ installed.length, available.length ] updatedcount = 0 updated = Array.new ( installed.keys & available.keys ).each { |pkg| if(version_a_gt_b(available[pkg], installed[pkg])) then updatedcount += 1 updated.push(pkg + ": #{available[pkg]} > #{installed[pkg]}") end } # we're done. output a count in the format expected by wmbiff. if(updatedcount > 0) then puts "%d new" % [ updatedcount ] else puts "%d old" % [ installed.length ] end puts updated.join("\n")