From eff0a9ee9693181b1f844566274029a172f8e57b Mon Sep 17 00:00:00 2001 From: Mattias Andrée Date: Thu, 2 Apr 2015 15:51:16 +0200 Subject: add locks plugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mattias Andrée --- DEPENDENCIES | 2 + Makefile | 5 +- TODO | 1 - examples/plugins/locks | 83 +++++++++++++++++++++ src/plugins/locks.py | 193 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 281 insertions(+), 3 deletions(-) create mode 100644 examples/plugins/locks create mode 100644 src/plugins/locks.py diff --git a/DEPENDENCIES b/DEPENDENCIES index 737bf3d..08a58d3 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -16,6 +16,8 @@ OPTIONAL RUNTIME DEPENDENCIES: python-sysv-ipc: for ropty example iputils: for ping support inotify-tools: for inotify support + alarm: for limiting the time of a file search in locks + findutils: for file search in locks BUILD DEPENDENCIES: diff --git a/Makefile b/Makefile index 317093b..a221ab6 100644 --- a/Makefile +++ b/Makefile @@ -32,13 +32,14 @@ PLUGINS = chase clock cpuinfo cpuonline cpu df discstats ipaddress \ kmsg leapsec linereader loadavg lunar mem moc network \ pacman snmp snmp6 softirqs solar uname uptime users \ vmstat weather xdisplay xkb alsa dentrystate inodestate \ - files hdparm tzclock ropty ping inotify random swaps + files hdparm tzclock ropty ping inotify random swaps \ + locks PLUGIN_EXAMPLES = chase clock cpu cpuinfo cpuonline df discstats \ ipaddress kmsg loadavg lunar mem moc network \ pacman uname uptime users xdisplay xkb alsa \ dentrystate inodestate files tzclock ropty ping \ - inotify random swaps + inotify random swaps locks TRICK_EXAMPLES = localutcclock anytzclock diff --git a/TODO b/TODO index 6a293c1..67f950b 100644 --- a/TODO +++ b/TODO @@ -43,7 +43,6 @@ List of plugins to implement: /proc/net/udp /proc/net/unix /proc/sysvipc - /proc/locks Demo plugins: diff --git a/examples/plugins/locks b/examples/plugins/locks new file mode 100644 index 0000000..b24f870 --- /dev/null +++ b/examples/plugins/locks @@ -0,0 +1,83 @@ +# -*- python -*- + +# A xpybar configuration example testing the features of plugins.locks + +import time +import threading + +from plugins.locks import Locks + + +OUTPUT, HEIGHT, YPOS, TOP = 0, 12, 24, True + + +locks = Locks().locks +headers = ('Id', 'Syscall', 'Level', 'Shared', 'Pid', 'Device', 'Inode', 'Range', 'Mountpoint', 'Pathname') +def convert(lock): + rc = [] + rc.append(str(lock.lock_id)) + rc.append({'FLOCK' : 'flock', 'POSIX' : 'lockf'}[lock.lock_type]) + rc.append('mandatory' if lock.mandatory else 'advisory') + rc.append('yes' if lock.shared else 'no') + rc.append(str(lock.pid)) + rc.append('%i:%i' % (lock.major, lock.minor)) + rc.append(str(lock.inode)) + rc.append('%i..%s' % (lock.start, 'eof' if lock.end is None else str(lock.end))) + rc += ['...'] * 2 + return rc +text_locks = [convert(lock) for lock in locks] + +HEIGHT *= len(text_locks) + + +def locate_files(): + dev_cache = {} + for i, lock in enumerate(locks): + if (lock.major, lock.minor) in dev_cache: + mountpoint, device = dev_cache[(lock.major, lock.minor)] + text_locks[i][-2] = mountpoint + text_locks[i][5] = '%s(%s)' % (text_locks[i][5], device) + else: + device = lock.find_device() + mountpoint = device[1] if device is not None else '(not found)' + device = device[2] if device is not None else None + dev_cache[(lock.major, lock.minor)] = (mountpoint, device) + text_locks[i][-2] = mountpoint + if device is not None: + text_locks[i][5] = '%s(%s)' % (text_locks[i][5], device) + if not mountpoint.startswith('/'): + text_locks[i][-1] = '(not found)' + try: + with open('/proc/%i/comm' % lock.pid, 'rb') as file: + cmd = file.read() + cmd = cmd.decode('utf-8', 'replace')[:-1] + text_locks[i][4] = '%s(%s)' % (text_locks[i][4], cmd) + except: + pass + bar.invalidate() + for i, lock in enumerate(locks): + mountpoint = text_locks[i][-2] + if not mountpoint.startswith('/'): + continue + pathnames = lock.find_pathname(mountpoint) + if len(pathnames) == 0: + text_locks[i][-1] = '(not found)' + else: + text_locks[i][-1] = pathnames[0] + bar.invalidate() + + +start_ = start +def start(): + start_() + async(locate_files) + + +semaphore = threading.Semaphore() +def redraw(): + semaphore.acquire(blocking = True) + text = '\n'.join(' │ '.join('%s: %s' % (h, v) for (h, v) in zip(headers, lock)) for lock in text_locks) + bar.clear() + bar.draw_coloured_text(0, 10, 0, 2, text) + semaphore.release() + diff --git a/src/plugins/locks.py b/src/plugins/locks.py new file mode 100644 index 0000000..c22474c --- /dev/null +++ b/src/plugins/locks.py @@ -0,0 +1,193 @@ +# -*- python -*- +''' +xpybar – xmobar replacement written in python +Copyright © 2014, 2015 Mattias Andrée (maandree@member.fsf.org) + +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 3 of the License, 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, see . +''' + + +class Locks: + ''' + File lock statistics + + @variable locks:list List of file-locks + ''' + + def __init__(self): + ''' + Constructor + ''' + def convert(fields): + fields[0] = int(fields[0]) + fields[4] = int(fields[4]) + fields[5] = int(fields[5], 16) + fields[6] = int(fields[6], 16) + fields[7] = int(fields[7]) + fields[8] = int(fields[8]) + fields[9] = int(fields[9]) if not fields[9] == 'EOF' else None + return fields + with open('/proc/locks', 'rb') as file: + data = file.read() + data = data.decode('utf-8', 'replace').rstrip('\n').replace(':', ' ').replace(' ', ' ') + while ' ' in data: + data = data.replace(' ', ' ') + self.locks = [Locks.Lock(*convert(line.split(' '))) for line in data.split('\n')] + + + @staticmethod + def find_device(major, minor): + ''' + Find a device from its major and minor number + + This method will not work if a mount's mountpoint or a + device's pathname includes spaces or line feeds. + + @param major:int The major number of the device, in decimal form + @param minor:int The minor number of the device, in decimal form + @return (root:str, mountpoint:str, device:str)? The root of the mount within the filesystem, + the mountpoint relative to the process's root, + and the mounted device (pathname or RAM filesystem), + `None` if not found + ''' + with open('/proc/self/mountinfo', 'rb') as file: + data = file.read() + data = data.decode('utf-8', 'strict').rstrip('\n').split('\n') + devnum = '%i:%i' % (major, minor) + for line in data: + line = line.split(' ') + if not line[2] == devnum: + continue + return (line[3], line[4], line[-2]) + return None + + + @staticmethod + def find_pathname(mountpoint, inode, alarm = 1): + ''' + Find the pathname of a file based on its mountpoint and inode number + + @param mountpoint:str The mountpoint of the file + @param inode:int The inode number of the file + @param alarm:int|str? The time limit of the search, `None` to search forever + @return :list All found pathnames of the file + ''' + from subprocess import Popen, PIPE + command = ['find', mountpoint, '-inum', str(inode), '-mount', '-print0'] + if alarm is not None: + command = ['alarm', str(inode)] + command + found = Popen(command, stdin = PIPE, stdout = PIPE, stderr = PIPE).communicate()[0] + found = found.decode('utf-8', 'replace').rstrip('\n') + found = [pathname for pathname in found.split('\0') if not pathname == ''] + return found + + + @staticmethod + def find(major, minor, inode, alarm = 1): + ''' + Find the pathname of a file based on its device's major and minor number and its inode number + + @param major:int The major number of the device, in decimal form + @param minor:int The minor number of the device, in decimal form + @param inode:int The inode number of the file + @param alarm:int|str? The time limit of the search, `None` to search forever + @return :list All found pathnames of the file + ''' + device = Locks.find_device() + if device is None: + return [] + return Locks.find_pathname(device[1], inode, alarm) + + + class Lock: + ''' + Information about a file-lock + + @variable lock_id:int The unique ID of the lock + @variable lock_type:str Either 'FLOCK' or 'POSIX', indicating what lock system has been used, + 'FLOCK' indicates the 'flock' system call, and 'POSIX' indicates the + 'lockf' system call. + @variable mandatory:bool Whether the lock is mandatory rather than advisory + @variable shared:bool Whether the process has locked the file for reading rather than writing + @variable pid:int The ID of the process that has locked the file + @variable major:int The major number of the device on which the locked file is stored + @variable minor:int The minor number of the device on which the locked file is stored + @variable inode:int The inode number of the locked file + @variable start:int The index of the first bytes of the locked area + @variable end:int? The index of the last bytes of the locked area, `None` for end-of-file + ''' + def __init__(self, lock_id, lock_type, lock_level, sharedness, pid, major, minor, inode, start, end): + ''' + Constructor + + @param lock_id:int The unique ID of the lock + @param lock_type:str Either 'FLOCK' or 'POSIX', indicating what lock system has been used, + 'FLOCK' indicates the 'flock' system call, and 'POSIX' indicates the + 'lockf' system call. + @param lock_level:str Either 'MANDATORY' or 'ADVISORY' indicating the level of the lock + @param sharedness:str Either 'READ' or 'WRITE' indicating the sharedness of the lock + @param pid:int The ID of the process that has locked the file + @param major:int The major number of the device on which the locked file is stored + @param minor:int The minor number of the device on which the locked file is stored + @param inode:int The inode number of the locked file + @param start:int The index of the first bytes of the locked area + @param end:int? The index of the last bytes of the locked area, `None` for end-of-file + ''' + self.lock_id = lock_id + self.lock_type = lock_type + self.mandatory = lock_level == 'MANDATORY' + self.shared = sharedness == 'READ' + self.pid = pid + self.major = major + self.minor = minor + self.inode = inode + self.start = start + self.end = end + + + def find_device(self): + ''' + Find the device that the locked file is located on + + This method will not work if a mount's mountpoint or a + device's pathname includes spaces or line feeds. + + @return (root:str, mountpoint:str, device:str)? The root of the mount within the filesystem, + the mountpoint relative to the process's root, + and the mounted device (pathname or RAM + filesystem), `None` if not found + ''' + return Locks.find_device(self.major, self.minor) + + + def find_pathname(self, mountpoint, alarm = 1): + ''' + Find the pathname of the locked file based on its mountpoint + + @param mountpoint:str The mountpoint of the file + @param alarm:int|str? The time limit of the search, `None` to search forever + @return :list All found pathnames of the file + ''' + return Locks.find_pathname(mountpoint, self.inode, alarm) + + + def find(self, alarm = 1): + ''' + Find the pathname of the locked file + + @param alarm:int|str? The time limit of the search, `None` to search forever + @return :list All found pathnames of the file + ''' + return Locks.find(self.major, self.minor, self.inode, alarm) + -- cgit v1.2.3-70-g09d2