#!/usr/bin/python # Copyright (C) 2001 Matt Zimmerman # This version Modified by Steen Eugen Poulsen # - Changed the code to be Gentoo Linux compatible, by using qfile and qlist # from portage-utils. # # I would suggest not contacting Matt or other debian maintainers about errors # in this version, as it's no longer debian compatible. # # # Further updated by Javier Fernandez-Sanguino # - included patch from Justin Pryzby # to work with the latest Lsof - modify to reduce false positives by not # complaining about deleted inodes/files under /tmp/, /var/log/ or named # /SYSV. # PENDING: # - included code from 'psdel' contributed by Sam Morris to # make the program work even if lsof is not installed # (available at http://robots.org.uk/src/psdel) # # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2, or (at your option) # any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, # MA 02110-1301 USA # # On Debian systems, a copy of the GNU General Public License may be # found in /usr/share/common-licenses/GPL. import sys import os, errno import re import stat import pwd import sys import string if os.getuid() != 0: sys.stderr.write('This program must be run as root\n') sys.stderr.write('in order to collect information about all open file descriptors\n') sys.exit(1) def find_cmd(cmd): dirs = [ '/', '/usr/', '/usr/local/', sys.prefix ] for d in dirs: for sd in ('bin', 'sbin'): location = os.path.join(d, sd, cmd) if os.path.exists(location): return location return 0 def main(): process = None toRestart = {} toRestart = lsofcheck() # Check if we have lsof, if not, use psdel # if find_cmd('lsof'): # toRestart = lsofcheck() # else: # TODO - This does not work yet: # toRestart = psdelcheck() print "Found %d processes using old versions of upgraded files" % len(toRestart) if len(toRestart) == 0: sys.exit(0) programs = {} for process in toRestart: programs.setdefault(process.program, []) programs[process.program].append(process) print "(%d distinct programs)" % len(programs) packages = {} #dpkgQuery = 'dpkg-query --search ' + ' '.join(programs.keys()) diverted = None # dpkgQuery = 'dpkg --search ' + ' '.join(programs.keys()) # Miravlix 2007-07-22 Modified to use emerge tools dpkgQuery = 'qfile -C ' + ' '.join(programs.keys()) # print dpkgQuery for line in os.popen(dpkgQuery).readlines(): # if line.startswith('local diversion'): # continue # m = re.match('^diversion by (\S+) (from|to): (.*)$', line) # if m: # if m.group(2) == 'from': # diverted = m.group(3) # continue # if not diverted: # raise 'Weird error while handling diversion' # packagename, program = m.group(1), diverted # else: packagename, m = line[:-1].split(' ') m = re.match('\((.*)\)', m) program = m.group(1) packages.setdefault(packagename,Package(packagename)) packages[packagename].processes.extend(programs[program]) print "(%d distinct packages)" % len(packages) if len(packages) == 0: sys.exit(0) for package in packages.values(): if package.name == 'sys-apps/util-linux': continue #dpkgQuery = 'dpkg-query --listfiles ' + package.name #dpkgQuery = 'dpkg --listfiles ' + package.name dpkgQuery = 'qlist ' + package.name # print dpkgQuery # sys.exit(0) for line in os.popen(dpkgQuery).readlines(): path = line[:-1] if path.startswith('/etc/init.d/'): if path.endswith('.sh'): continue package.initscripts.append(path) restartable = [] nonrestartable = [] restartCommands = [] for package in packages.values(): if len(package.initscripts) > 0: restartable.append(package) restartCommands.extend(map(lambda s: s + ' restart',package.initscripts)) else: nonrestartable.append(package) print print "Of these, %d seem to contain init scripts which can be used to restart them:" % len(restartable) print '\n'.join(restartCommands) print if len(nonrestartable) == 0: sys.exit(0) print "Here are the others:" for package in nonrestartable: print package.name + ':' for process in package.processes: print "\t%s\t%s" % (process.pid,process.program) def lsofcheck(): processes = {} for line in os.popen('lsof +XL -F nf').readlines(): field, data = line[0], line[1:-1] if field == 'p': process = processes.setdefault(data,Process(int(data))) elif field == 'k': process.links.append(data) elif field == 'n': # Remove the previous entry to check if this is something we should do if data.startswith('/SYSV'): last = process.descriptors.pop() if not re.compile("DEL").search(last): process.files.append(data) else: process.files.append(data) elif field == 'f': process.descriptors.append(data) toRestart = filter(lambda process: process.needsRestart(), processes.values()) return toRestart def psdelcheck(): # TODO - Needs to be fixed to work here # Useful for seeing which processes need to be restarted after you upgrade # programs or shared libraries. Written to replace checkrestart(1) from the # debian-goodies, which often misses out processes due to bugs in lsof; see # for more information. numeric = re.compile(r'\d+') toRestart = map (delmaps, map (string.atoi, filter (numeric.match, os.listdir('/proc')))) return toRestart def delmaps (pid): processes = {} process = processes.setdefault(pid,Process(int(pid))) deleted = re.compile(r'(.*) \(deleted\)$') boring = re.compile(r'/(dev/zero|SYSV([\da-f]{8}))') mapline = re.compile(r'^[\da-f]{8}-[\da-f]{8} [r-][w-][x-][sp-] ' r'[\da-f]{8} [\da-f]{2}:[\da-f]{2} (\d+) *(.+)( \(deleted\))?\n$') maps = open('/proc/%d/maps' % (pid)) for line in maps.readlines (): m = mapline.match (line) if (m): inode = string.atoi (m.group (1)) file = m.group (2) if inode == 0: continue # remove ' (deleted)' suffix if deleted.match (file): file = file [0:-10] if boring.match (file): continue # list file names whose inode numbers do not match their on-disk # values; or files that do not exist at all try: if os.stat (file)[stat.ST_INO] != inode: process = processes.setdefault(pid,Process(int(pid))) except OSError, (e, strerror): if e == errno.ENOENT: process = processes.setdefault(pid,Process(int(pid))) else: sys.stderr.write ('checkrestart (psdel): %s %s: %s\n' % (SysProcess.get(pid).info (), file, os.strerror (e))) else: print 'checkrestart (psdel): Error parsing "%s"' % (line [0:-1]) maps.close () return process class SysProcess: re_name = re.compile('Name:\t(.*)$') re_uids = re.compile('Uid:\t(\d+)\t(\d+)\t(\d+)\t(\d+)$') processes = {} def get (pid): try: return Process.processes [pid] except KeyError: Process.processes [pid] = Process (pid) return Process.get (pid) # private def __init__ (self, pid): self.pid = pid status = open ('/proc/%d/status' % (self.pid)) for line in status.readlines (): m = self.re_name.match (line) if m: self.name = m.group (1) continue m = self.re_uids.match (line) if m: self.user = pwd.getpwuid (string.atoi (m.group (1)))[0] continue status.close () def info (self): return '%d %s %s' % (self.pid, self.name, self.user) class Process: def __init__(self, pid): self.pid = pid self.files = [] self.descriptors = [] self.links = [] try: self.program = self.cleanFile(os.readlink('/proc/%d/exe' % self.pid)) except OSError, e: if e.errno != 2: raise def cleanFile(self, f): # /proc/pid/exe has all kinds of junk in it sometimes null = f.find('\0') if null != -1: f = f[:null] return re.sub('( \(deleted\)|.dpkg-new).*$','',f) # Check if a process needs to be restarted, previously we would # just check if it used libraries named '.dpkg-new' since that's # what dpkg would do. Now we need to be more contrieved. def needsRestart(self): for f in self.files: #print "Checking %s" % f # We don't care about log files if f.startswith('/var/log/'): continue # Or about files under /tmp if f.startswith('/tmp/'): continue # TODO: it should only care about library files (i.e. /lib, /usr/lib and the like) # build that check with a regexp to exclude others if f.endswith(' (deleted)'): return 1 if re.compile("\(path inode=[0-9]+\)$").search(f): return 1 for f in self.descriptors: # If it begins with SYSV it is not a deleted file if re.compile("DEL").search(f): return 1 for f in self.links: if f == 0: return 1 return 0 class Package: def __init__(self, name): self.name = name self.initscripts = [] self.processes = [] if __name__ == '__main__': main()