对我来说这可能是一个愚蠢的练习,但它提出了一些有趣的问题。我有一个来自聊天客户端的日志文件目录,我希望每当其中一个更改时使用notify-osd通知我。
我编写的脚本基本上使用os.popen在每个文件上运行linux tail命令以获取最后一行,然后根据字典查看每行上次运行的行。如果该行更改,则使用pynotify向我发送通知。
这个脚本实际上运行得很好,除了它使用了大量的cpu(可能因为它在每次循环运行时运行尾部大约16次,对于通过sshfs挂载的文件。)
像this这样的东西似乎是一个很好的解决方案,但我不知道如何为多个文件实现它。
Here是我写的脚本。请原谅我缺乏评论和不良风格。
编辑:为了澄清,这是桌面上的所有Linux。
答案 0 :(得分:9)
甚至没有查看您的源代码,有两种方法可以更轻松地执行此操作并处理多个文件。
除非必须,否则不要打扰跑尾。只需os.stat
所有文件并记录上次修改时间。如果上次修改时间不同,则发出通知。
使用pyinotify呼叫Linux的inotify设施;这将让内核为您执行选项1,并在目录中的任何文件发生更改时回调您。然后将回调转换为您的osd通知。
现在,可能会有一些棘手的问题,具体取决于当有多条消息时您想要多少通知,以及您是否关心错过消息通知。
保留使用tail
的方法是改为使用tail -f
。使用tail -f
打开所有文件,然后使用select
模块让操作系统在tail -f
打开的其中一个文件描述符上有其他输入时告诉您。您的主循环将调用select,然后迭代每个可读描述符以生成通知。 (你可以在不使用tail
的情况下执行此操作,并在可读时调用readline()
。)
脚本的其他改进方面:
os.listdir
和本机Python过滤(例如,使用列表推导)而不是popen
和一堆grep
过滤器。subprocess.popen
代替os.popen
。答案 1 :(得分:4)
如果你已经在使用pyinotify模块,那么在纯Python中很容易做到这一点(即不需要产生一个单独的进程来拖尾每个文件)。
这是一个由inotify事件驱动的示例,应该使用非常少的cpu。当给定路径发生IN_MODIFY时,我们从文件句柄中读取所有可用数据,并输出找到的所有完整行,缓冲不完整的行,直到有更多数据可用:
import os
import select
import sys
import pynotify
import pyinotify
class Watcher(pyinotify.ProcessEvent):
def __init__(self, paths):
self._manager = pyinotify.WatchManager()
self._notify = pyinotify.Notifier(self._manager, self)
self._paths = {}
for path in paths:
self._manager.add_watch(path, pyinotify.IN_MODIFY)
fh = open(path, 'rb')
fh.seek(0, os.SEEK_END)
self._paths[os.path.realpath(path)] = [fh, '']
def run(self):
while True:
self._notify.process_events()
if self._notify.check_events():
self._notify.read_events()
def process_default(self, evt):
path = evt.pathname
fh, buf = self._paths[path]
data = fh.read()
lines = data.split('\n')
# output previous incomplete line.
if buf:
lines[0] = buf + lines[0]
# only output the last line if it was complete.
if lines[-1]:
buf = lines[-1]
lines.pop()
# display a notification
notice = pynotify.Notification('%s changed' % path, '\n'.join(lines))
notice.show()
# and output to stdout
for line in lines:
sys.stdout.write(path + ': ' + line + '\n')
sys.stdout.flush()
self._paths[path][1] = buf
pynotify.init('watcher')
paths = sys.argv[1:]
Watcher(paths).run()
用法:
% python watcher.py [path1 path2 ... pathN]
答案 2 :(得分:0)
简单的纯python解决方案(不是最好的,但不会分叉,在空闲时间后吐出4个空行,并且每次都会标记块的来源,如果更改):
#!/usr/bin/env python
from __future__ import with_statement
'''
Implement multi-file tail
'''
import os
import sys
import time
def print_file_from(filename, pos):
with open(filename, 'rb') as fh:
fh.seek(pos)
while True:
chunk = fh.read(8192)
if not chunk:
break
sys.stdout.write(chunk)
def _fstat(filename):
st_results = os.stat(filename)
return (st_results[6], st_results[8])
def _print_if_needed(filename, last_stats, no_fn, last_fn):
changed = False
#Find the size of the file and move to the end
tup = _fstat(filename)
# print tup
if last_stats[filename] != tup:
changed = True
if not no_fn and last_fn != filename:
print '\n<%s>' % filename
print_file_from(filename, last_stats[filename][0])
last_stats[filename] = tup
return changed
def multi_tail(filenames, stdout=sys.stdout, interval=1, idle=10, no_fn=False):
S = lambda (st_size, st_mtime): (max(0, st_size - 124), st_mtime)
last_stats = dict((fn, S(_fstat(fn))) for fn in filenames)
last_fn = None
last_print = 0
while 1:
# print last_stats
changed = False
for filename in filenames:
if _print_if_needed(filename, last_stats, no_fn, last_fn):
changed = True
last_fn = filename
if changed:
if idle > 0:
last_print = time.time()
else:
if idle > 0 and last_print is not None:
if time.time() - last_print >= idle:
last_print = None
print '\n' * 4
time.sleep(interval)
if '__main__' == __name__:
from optparse import OptionParser
op = OptionParser()
op.add_option('-F', '--no-fn', help="don't print filename when changes",
default=False, action='store_true')
op.add_option('-i', '--idle', help='idle time, in seconds (0 turns off)',
type='int', default=10)
op.add_option('--interval', help='check interval, in seconds', type='int',
default=1)
opts, args = op.parse_args()
try:
multi_tail(args, interval=opts.interval, idle=opts.idle,
no_fn=opts.no_fn)
except KeyboardInterrupt:
pass