Advertisement
Guest User

Checkrestart ported to Arch Linux (from debian-goodies)

a guest
Apr 4th, 2015
543
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 21.92 KB | None | 0 0
  1. #!/usr/bin/python2
  2.  
  3. # Copyright (C) 2001 Matt Zimmerman <mdz@debian.org>
  4. # Copyright (C) 2007,2011 Javier Fernandez-Sanguino <jfs@debian.org>
  5. # - included patch from Justin Pryzby <justinpryzby_AT_users.sourceforge.net>
  6. #   to work with the latest Lsof - modify to reduce false positives by not
  7. #   complaining about deleted inodes/files under /tmp/, /var/log/,
  8. #   /var/run or named   /SYSV.
  9. # - introduced a verbose option
  10.  
  11. # PENDING:
  12. # - included code from 'psdel' contributed by Sam Morris <sam_AT_robots.org.uk> to
  13. #   make the program work even if lsof is not installed
  14. #   (available at http://robots.org.uk/src/psdel)
  15. # - make it work with a whitelist of directories instead of a blacklist
  16. #   (might make it less false positive prone)
  17. #
  18. #
  19. # This program is free software; you can redistribute it and/or modify
  20. # it under the terms of the GNU General Public License as published by
  21. # the Free Software Foundation; either version 2, or (at your option)
  22. # any later version.
  23.  
  24. # This program is distributed in the hope that it will be useful,
  25. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  26. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  27. # GNU General Public License for more details.
  28.  
  29. # You should have received a copy of the GNU General Public License
  30. # along with this program; if not, write to the Free Software
  31. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
  32. # MA 02110-1301 USA
  33. #
  34. # On Debian systems, a copy of the GNU General Public License may be
  35. # found in /usr/share/common-licenses/GPL.
  36.  
  37. import sys
  38. import os, errno
  39. import re
  40. import pwd
  41. import sys
  42. import string
  43. import subprocess
  44. import getopt
  45. from stat import *
  46.  
  47. if os.getuid() != 0:
  48.     sys.stderr.write('ERROR: This program must be run as root in order to obtain information\n')
  49.     sys.stderr.write('about all open file descriptors in the system.\n')
  50.     sys.exit(1)
  51.  
  52. def find_cmd(cmd):
  53.      dirs = [ '/', '/usr/', '/usr/local/', sys.prefix ]
  54.      for d in dirs:
  55.          for sd in ('bin', 'sbin'):
  56.              location = os.path.join(d, sd, cmd)
  57.              if os.path.exists(location):
  58.                  return location
  59.      return 1
  60.  
  61. def usage():
  62.     sys.stderr.write('usage: checkrestart [-vhpa] [-bblacklist] [-iignore]\n')
  63.  
  64. def main():
  65.     global lc_all_c_env, file_query_check
  66.     process = None
  67.     toRestart = {}
  68.  
  69.     lc_all_c_env = os.environ
  70.     lc_all_c_env['LC_ALL'] = 'C'
  71.     file_query_check = {}
  72.     blacklistFiles = []
  73.     blacklist = []
  74.     ignorelist = [ 'util-linux', 'screen' ]
  75.  
  76. # Process options
  77.     try:
  78.         opts, args = getopt.getopt(sys.argv[1:], "hvpab:i:", ["help", "verbose", "packages", "all", "blacklist", "ignore"])
  79.     except getopt.GetoptError, err:
  80.         # print help information and exit:
  81.         print str(err) # will print something like "option -x not recognized"
  82.         usage()
  83.         sys.exit(2)
  84.  
  85.     # Global variables set through the command line
  86.     global verbose, onlyPackageFiles, allFiles
  87.     verbose = False
  88.     # Only look for deleted files that belong to packages
  89.     onlyPackageFiles = False
  90.     # Look for any deleted file
  91.     allFiles = False
  92.  
  93.     for o, a in opts:
  94.         if o in ("-v", "--verbose"):
  95.             verbose = True
  96.         elif o in ("-h", "--help"):
  97.             usage()
  98.             sys.exit()
  99.         elif o in ("-p", "--packages"):
  100.             onlyPackageFiles = True
  101.         elif o in ("-a", "--all"):
  102.             allFiles = True
  103.             onlyPackageFiles = False
  104.         elif o in ("-b", "--blacklist"):
  105.             blacklistFiles.append(a)
  106.             onlyPackageFiles = False
  107.         elif o in ("-i", "--ignore"):
  108.             ignorelist.append(a)
  109.         else:
  110.             assert False, "unhandled option"
  111.  
  112.     for f in blacklistFiles:
  113.         for line in file(f, "r"):
  114.             if line.startswith("#"):
  115.                 continue
  116.             blacklist.append(re.compile(line.strip()))
  117.  
  118. # Start checking
  119.  
  120.     if find_cmd('lsof') == 1:
  121.         sys.stderr.write('ERROR: This program needs lsof in order to run.\n')
  122.         sys.stderr.write('Please install the lsof package in your system.\n')
  123.         sys.exit(1)
  124. # Check if we have lsof, if not, use psdel
  125. #    if find_cmd('lsof'):
  126. #         toRestart = lsofcheck()
  127. #    else:
  128. # TODO - This does not work yet:
  129. #        toRestart = psdelcheck()
  130.  
  131.     toRestart = lsofcheck(blacklist = blacklist)
  132.  
  133.     print "Found %d processes using old versions of upgraded files" % len(toRestart)
  134.  
  135.     if len(toRestart) == 0:
  136.         sys.exit(0)
  137.  
  138.     programs = {}
  139.     for process in toRestart:
  140.         programs.setdefault(process.program, [])
  141.         programs[process.program].append(process)
  142.  
  143.     if len(programs) == 1:
  144.         print "(%d distinct program)" % len(programs)
  145.     else:
  146.         print "(%d distinct programs)" % len(programs)
  147.  
  148. # Verbose information
  149.     if verbose:
  150.         for process in toRestart:
  151.             print "Process %s (PID: %d) "  % (process.program, process.pid)
  152.             process.listDeleted()
  153.  
  154.     packages = {}
  155.     diverted = None
  156.     packageCount = -1
  157.     dpkgQuery = ["pacman", "-Qoq"] + programs.keys()
  158.     dpkgProc = subprocess.Popen(dpkgQuery, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
  159.                                 env = lc_all_c_env)
  160.     if verbose:
  161.         print "Running:%s" % dpkgQuery
  162.     while True:
  163.             packageCount = packageCount + 1
  164.             line = dpkgProc.stdout.readline()
  165.             if not line:
  166.                 break
  167.             if verbose:
  168.                 print "Reading line: %s" % line
  169.             if line.startswith('local diversion'):
  170.                 continue
  171.             #if not ':' in line:
  172.             #    continue
  173.  
  174.             m = re.match('^diversion by (\S+) (from|to): (.*)$', line)
  175.             if m:
  176.                 if m.group(2) == 'from':
  177.                     diverted = m.group(3)
  178.                     continue
  179.                 if not diverted:
  180.                     raise Exception('Weird error while handling diversion')
  181.                 packagename, program = m.group(1), diverted
  182.             else:
  183.                  packagename = line
  184.                  program = programs.keys()[packageCount]
  185.                  sys.stdout.write ('package: %s program: %s' % (packagename, program))
  186.                  if program == diverted:
  187.                      # dpkg prints a summary line after the diversion, name both
  188.                      # packages of the diversion, so ignore this line
  189.                      # mutt-patched, mutt: /usr/bin/mutt
  190.                     continue
  191.             packages.setdefault(packagename,Package(packagename))
  192.             try:
  193.                  packages[packagename].processes.extend(programs[program])
  194.             except KeyError:
  195.                   sys.stderr.write ('checkrestart (program not found): %s: %s\n' % (packagename, program))
  196.             sys.stdout.flush()
  197.  
  198.     # Close the pipe
  199.     dpkgProc.stdout.close()
  200.  
  201.     print "(%d distinct packages)" % len(packages)
  202.  
  203.     if len(packages) == 0:
  204.         print "No packages seem to need to be restarted."
  205.         print "(please read checkrestart(1))"
  206.         sys.exit(0)
  207.  
  208.     for package in packages.values():
  209.         skip = False
  210.         if ignorelist:
  211.             for i in ignorelist:
  212.                 if i in package.name > 0:
  213.                     skip = True
  214.         if skip:
  215.             continue
  216.         dpkgQuery = ["pacman", "-Qlq", package.name]
  217.         dpkgProc = subprocess.Popen(dpkgQuery, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
  218.                                 env = lc_all_c_env)
  219.         while True:
  220.             line = dpkgProc.stdout.readline()
  221.             if not line:
  222.                 break
  223.             path = line[:-1]
  224.             if path.startswith('/etc/init.d/'):
  225.                 if path.endswith('.sh'):
  226.                     continue
  227.                 package.initscripts.add(path[12:])
  228.             sys.stdout.flush()
  229.         dpkgProc.stdout.close()
  230.  
  231.         # Alternatively, find init.d scripts that match the process name
  232.         if len(package.initscripts) == 0:
  233.             for process in package.processes:
  234.                 proc_name = os.path.basename(process.program)
  235.                 if os.path.exists('/etc/init.d/' + proc_name):
  236.                     package.initscripts.add(proc_name)
  237.     if not verbose:
  238.     sys.exit(0)
  239.     restartable = []
  240.     nonrestartable = []
  241.     restartCommands = []
  242.     for package in packages.values():
  243.         if len(package.initscripts) > 0:
  244.             restartable.append(package)
  245.             restartCommands.extend(map(lambda s: 'service ' + s + ' restart',package.initscripts))
  246.         else:
  247.             nonrestartable.append(package)
  248.            
  249.     if len(restartable) > 0:
  250.         print
  251.         print "Of these, %d seem to contain init scripts which can be used to restart them:" % len(restartable)
  252.         # TODO - consider putting this in a --verbose option
  253.         print "The following packages seem to have init scripts that could be used\nto restart them:"
  254.         for package in restartable:
  255.               print package.name + ':'
  256.               for process in package.processes:
  257.                    print "\t%s\t%s" % (process.pid,process.program)
  258.                    
  259.         print
  260.         print "These are the init scripts:"
  261.         print '\n'.join(restartCommands)
  262.         print
  263.  
  264.     if len(nonrestartable) == 0:
  265.         sys.exit(0)
  266.  
  267.     # TODO - consider putting this in a --verbose option
  268.     print "These processes do not seem to have an associated init script to restart them:"
  269.     for package in nonrestartable:
  270.         skip = False
  271.         if ignorelist:
  272.             for i in ignorelist:
  273.                 if i in package.name > 0:
  274.                     skip = True
  275.         if skip:
  276.             continue
  277.         print package.name + ':'
  278.         for process in package.processes:
  279.             print "\t%s\t%s" % (process.pid,process.program)
  280.  
  281. def lsofcheck(blacklist = None):
  282.     processes = {}
  283.  
  284.     for line in os.popen('lsof +XL -F nf').readlines():
  285.         field, data = line[0], line[1:-1]
  286.  
  287.         if field == 'p':
  288.             process = processes.setdefault(data,Process(int(data)))
  289.         elif field == 'k':
  290.             process.links.append(data)
  291.         elif field == 'n':
  292.             # Remove the previous entry to check if this is something we should use
  293.             if data.startswith('/SYSV'):
  294.                 # If we find SYSV we discard the previous descriptor
  295.                 last = process.descriptors.pop()
  296.             elif data.startswith('/'):
  297.                 last = process.descriptors.pop()
  298.                 # Add it to the list of deleted files if the previous descriptor
  299.                 # was DEL or lsof marks it as deleted
  300.                 if re.compile("DEL").search(last) or re.compile("deleted").search(data) or re.compile("\(path inode=[0-9]+\)$").search(data):
  301.                     process.files.append(data)
  302.             else:
  303.                 # We discard the previous descriptors and drop it
  304.                 last = process.descriptors.pop()
  305.         elif field == 'f':
  306.             # Save the descriptor for later comparison
  307.             process.descriptors.append(data)
  308.  
  309.     toRestart = filter(lambda process: process.needsRestart(blacklist),
  310.                        processes.values())
  311.     return toRestart
  312.  
  313. # Tells if a given file is part of a package
  314. # Returns:
  315. #  - False - file does not exist in the system or cannot be found when querying the package database
  316. #  - True  - file is found in an operating system package
  317. def ispackagedFile (f):
  318.     file_in_package = False
  319.     file_regexp = False
  320.     if verbose:
  321.         print "Checking if file %s belongs to any package" % f
  322.     # First check if the file exists
  323.     if not os.path.exists(f):
  324.         if ( f.startswith('/lib/') or f.startswith('/usr/lib/') ) and re.compile("\.so[\d.]+$"):
  325.         # For libraries that do not exist then try to use a regular expression with the
  326.         # soname
  327.         # In libraries, indent characters that could belong to a regular expression first
  328.             f = re.compile("\+").sub("\+", f)
  329.             f = re.compile(".so[\d.]+$").sub(".so.*", f)
  330.             f = re.compile("\.").sub("\.", f)
  331.             file_regexp = True
  332.         else:
  333.         # Do not call dpkg-query if the file simply does not exist in the file system
  334.         # just assume it does not belong to any package
  335.             return False
  336.  
  337.     # If it exists, run dpkg-query
  338.     dpkgQuery = ["pacman", "-Qoq", f ]
  339.     dpkgProc = subprocess.Popen(dpkgQuery, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
  340.                 env = lc_all_c_env, close_fds=True)
  341.     dpkgProc.wait()
  342.     if verbose:
  343.         print "Running:%s" % dpkgQuery
  344.     for line in dpkgProc.stdout.readlines():
  345.         line = line.strip()
  346.         if line.find('no path found matching pattern ' + f) > 0:
  347.             file_in_package = False
  348.             break
  349.         if line.endswith(f) or ( file_regexp and re.search(f, line)):
  350.             package  = re.compile(":.*$").sub("",line)
  351.             file_in_package = True
  352.             break
  353.  
  354.     if file_in_package and verbose:
  355.         print "YES: File belongs to package %s" % package
  356.     if not file_in_package and verbose:
  357.         print "NO: File does not belongs to any package"
  358.     return file_in_package
  359.  
  360. # Tells if a file has to be considered a deleted file
  361. # Returns:
  362. #  - 0 (NO) for known locations of files which might be deleted
  363. #  - 1 (YES) for valid deleted files we are interested in
  364. def isdeletedFile (f, blacklist = None):
  365.  
  366.     global lc_all_c_env, file_query_check
  367.  
  368.     if allFiles:
  369.         return 1
  370.     if blacklist:
  371.         for p in blacklist:
  372.             if p.search(f):
  373.                 return 0
  374.     # We don't care about log files
  375.     if f.startswith('/var/log/'):
  376.         return 0
  377.     # Or about files under temporary locations
  378.     if f.startswith('/var/run/'):
  379.         return 0
  380.     # Or about files under /tmp
  381.     if f.startswith('/tmp/'):
  382.         return 0
  383.     # Or about files under /dev/shm
  384.     if f.startswith('/dev/shm/'):
  385.         return 0
  386.     # Or about files under /run
  387.     if f.startswith('/run/'):
  388.         return 0
  389.     # Or about files under /drm
  390.     if f.startswith('/drm'):
  391.         return 0
  392.     # Or about files under /var/tmp
  393.     if f.startswith('/var/tmp/'):
  394.         return 0
  395.     # Or /dev/zero
  396.     if f.startswith('/dev/zero'):
  397.         return 0
  398.     # Or /dev/pts (used by gpm)
  399.     if f.startswith('/dev/pts/'):
  400.         return 0
  401.     # Or /usr/lib/locale
  402.     if f.startswith('/usr/lib/locale/'):
  403.         return 0
  404.     # Skip files from the user's home directories
  405.     # many processes hold temporafy files there
  406.     if f.startswith('/home/'):
  407.         return 0
  408.     # Skip automatically generated files
  409.     if f.endswith('icon-theme.cache'):
  410.         return 0
  411.     # Skip font files
  412.     if f.startswith('/var/cache/fontconfig/'):
  413.         return 0
  414.     # Skip Nagios Spool
  415.     if f.startswith('/var/lib/nagios3/spool/'):
  416.         return 0
  417.     # Skip nagios spool files
  418.     if f.startswith('/var/lib/nagios3/spool/checkresults/'):
  419.     return 0
  420.     # Skip, if asked to, files that do not belong to any package
  421.     if onlyPackageFiles:
  422.         # Remove some lsof information from the file to ensure that it is
  423.         # a proper filename
  424.         file_name = re.sub(r'\(.*\)','', f)
  425.         file_name = re.sub(r'\s+$','', file_name)
  426.         # First check: have we checked this file before? If we have not then make the check
  427.         if not file_name in file_query_check:
  428.                 file_query_check[file_name] = ispackagedFile(file_name)
  429.         # Once we have the result then check if the file belongs to a package
  430.         if not file_query_check[file_name]:
  431.                 return 0
  432.  
  433.     # TODO: it should only care about library files (i.e. /lib, /usr/lib and the like)
  434.     # build that check with a regexp to exclude others
  435.     if f.endswith(' (deleted)'):
  436.         return 1
  437.     if re.compile("\(path inode=[0-9]+\)$").search(f):
  438.         return 1
  439.     # Default: it is a deleted file we are interested in
  440.     return 1
  441.  
  442. def psdelcheck():
  443. # TODO - Needs to be fixed to work here
  444. # Useful for seeing which processes need to be restarted after you upgrade
  445. # programs or shared libraries. Written to replace checkrestart(1) from the
  446. # debian-goodies, which often misses out processes due to bugs in lsof; see
  447. # <http://bugs.debian.org/264985> for more information.
  448.  
  449.     numeric = re.compile(r'\d+')
  450.     toRestart = map (delmaps, map (string.atoi, filter (numeric.match, os.listdir('/proc'))))
  451.     return toRestart
  452.  
  453. def delmaps (pid):
  454.     processes = {}
  455.     process = processes.setdefault(pid,Process(int(pid)))
  456.     deleted = re.compile(r'(.*) \(deleted\)$')
  457.     boring = re.compile(r'/(dev/zero|SYSV([\da-f]{8}))|/usr/lib/locale')
  458.  
  459.     mapline = re.compile(r'^[\da-f]{8}-[\da-f]{8} [r-][w-][x-][sp-] '
  460.             r'[\da-f]{8} [\da-f]{2}:[\da-f]{2} (\d+) *(.+)( \(deleted\))?\n$')
  461.     maps = open('/proc/%d/maps' % (pid))
  462.     for line in maps.readlines ():
  463.         m = mapline.match (line)
  464.         if (m):
  465.             inode = string.atoi (m.group (1))
  466.             file = m.group (2)
  467.             if inode == 0:
  468.                 continue
  469.             # remove ' (deleted)' suffix
  470.             if deleted.match (file):
  471.                 file = file [0:-10]
  472.             if boring.match (file):
  473.                 continue
  474.             # list file names whose inode numbers do not match their on-disk
  475.             # values; or files that do not exist at all
  476.             try:
  477.                 if os.stat (file)[stat.ST_INO] != inode:
  478.                     process = processes.setdefault(pid,Process(int(pid)))
  479.             except OSError, (e, strerror):
  480.                 if e == errno.ENOENT:
  481.                     process = processes.setdefault(pid,Process(int(pid)))
  482.                 else:
  483.                     sys.stderr.write ('checkrestart (psdel): %s %s: %s\n' % (SysProcess.get(pid).info (), file, os.strerror (e)))
  484.         else:
  485.             print 'checkrestart (psdel): Error parsing "%s"' % (line [0:-1])
  486.     maps.close ()
  487.  
  488.     return process
  489.  
  490.  
  491. class SysProcess:
  492.     re_name = re.compile('Name:\t(.*)$')
  493.     re_uids = re.compile('Uid:\t(\d+)\t(\d+)\t(\d+)\t(\d+)$')
  494.     processes = {}
  495.     def get (pid):
  496.         try:
  497.             return Process.processes [pid]
  498.         except KeyError:
  499.             Process.processes [pid] = Process (pid)
  500.             return Process.get (pid)
  501.  
  502.     # private
  503.     def __init__ (self, pid):
  504.         self.pid = pid
  505.  
  506.         status = open ('/proc/%d/status' % (self.pid))
  507.         for line in status.readlines ():
  508.             m = self.re_name.match (line)
  509.             if m:
  510.                 self.name = m.group (1)
  511.                 continue
  512.             m = self.re_uids.match (line)
  513.             if m:
  514.                 self.user = pwd.getpwuid (string.atoi (m.group (1)))[0]
  515.                 continue
  516.         status.close ()
  517.    
  518.     def info (self):
  519.         return '%d %s %s' % (self.pid, self.name, self.user)
  520.  
  521. class Process:
  522.     def __init__(self, pid):
  523.         self.pid = pid
  524.         self.files = []
  525.         self.descriptors = []
  526.         self.links = []
  527.         self.program = ''
  528.  
  529.         try:
  530.             self.program = os.readlink('/proc/%d/exe' % self.pid)
  531.             # if the executable command is an interpreter such as perl/python,
  532.             # we want to find the real program
  533.             m = re.match("^/usr/bin/(perl|python)$", self.program)
  534.             if m:
  535.                 with open('/proc/%d/cmdline' % self.pid, 'r') as cmdline:
  536.                     # only match program in /usr (ex.: /usr/sbin/smokeping)
  537.                     # ignore child, etc.
  538.                     #m = re.search(r'^(([/]\w*){1,5})\s.*$', cmdline.read())
  539.                     m = re.search(r'^(/usr/\S+)$', cmdline.read())
  540.                     if m:
  541.                         # store the real full path of script as the program
  542.                         self.program = m.group(1)
  543.         except OSError, e:
  544.             if e.errno != errno.ENOENT:
  545.                 if self.pid == 1:
  546.                     sys.stderr.write("Found unreadable pid 1. Assuming we're under vserver and continuing.\n")
  547.                 else:
  548.                     sys.stderr.write('ERROR: Failed to read %d' % self.pid)
  549.                     raise
  550.         self.program = self.cleanFile(self.program)
  551.  
  552.     def cleanFile(self, f):
  553.         # /proc/pid/exe has all kinds of junk in it sometimes
  554.         null = f.find('\0')
  555.         if null != -1:
  556.             f = f[:null]
  557.         # Support symlinked /usr
  558.         if f.startswith('/usr'):
  559.             statinfo = os.lstat('/usr')[ST_MODE]
  560.             # If /usr is a symlink then find where it points to
  561.             if S_ISLNK(statinfo):
  562.                 newusr = os.readlink('/usr')
  563.                 if not newusr.startswith('/'):
  564.                     # If the symlink is relative, make it absolute
  565.                     newusr = os.path.join(os.path.dirname('/usr'), newusr)
  566.                 f = re.sub('^/usr',newusr, f)
  567.                 # print "Changing usr to " + newusr + " result:" +f; # Debugging
  568.         return re.sub('( \(deleted\)|.dpkg-new).*$','',f)
  569.  
  570.     def listDeleted(self):
  571.         listfiles = []
  572.         listdescriptors = []
  573.         for f in self.files:
  574.             if isdeletedFile(f):
  575.                 listfiles.append(f)
  576.         if  listfiles != []:
  577.             print "List of deleted files in use:"
  578.             for file in listfiles:
  579.                 print "\t" + file
  580.  
  581.     # Check if a process needs to be restarted, previously we would
  582.     # just check if it used libraries named '.dpkg-new' since that's
  583.     # what dpkg would do. Now we need to be more contrieved.
  584.     # Returns:
  585.     #  - 0 if there is no need to restart the process
  586.     #  - 1 if the process needs to be restarted
  587.     def needsRestart(self, blacklist = None):
  588.         for f in self.files:
  589.             if isdeletedFile(f, blacklist):
  590.             return 1
  591.     for f in self.links:
  592.         if f == 0:
  593.             return 1
  594.         return 0
  595.  
  596. class Package:
  597.     def __init__(self, name):
  598.         self.name = name
  599.         # use a set, we don't need duplicates
  600.         self.initscripts = set()
  601.         self.processes = []
  602.  
  603. if __name__ == '__main__':
  604.     main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement