/usr/bin/ds-logpipe is in 389-ds-base 1.3.4.9-1.
This file is owned by root:root, with mode 0o755.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 | #!/usr/bin/env python
from __future__ import print_function
import sys
import os, os.path
import errno
import signal
import pprint
import types
import time
import fcntl
import pwd
maxlines = 1000 # set on command line
S_IFIFO = 0o010000
buffer = [] # default circular buffer used by default plugin
totallines = 0
logfname = "" # name of log pipe
debug = False
# default plugin just keeps a circular buffer
def defaultplugin(line):
global totallines
buffer.append(line)
totallines = totallines + 1
if len(buffer) > maxlines:
del buffer[0]
return True
def printbuffer():
sys.stdout.writelines(buffer)
print("Read %d total lines" % totallines)
print(logfname, "=" * 60)
sys.stdout.flush()
def defaultpost(): printbuffer()
plgfuncs = [] # list of plugin functions
plgpostfuncs = [] # list of post plugin funcs
def finish():
for postfunc in plgpostfuncs: postfunc()
if options.scriptpidfile: os.unlink(options.scriptpidfile)
sys.exit(0)
def sighandler(signum, frame):
if signum != signal.SIGHUP:
signal.signal(signal.SIGHUP, signal.SIG_DFL)
signal.signal(signal.SIGINT, signal.SIG_DFL)
#signal.signal(signal.SIGPIPE, signal.SIG_DFL)
signal.signal(signal.SIGTERM, signal.SIG_DFL)
signal.signal(signal.SIGALRM, signal.SIG_DFL)
if signum == signal.SIGALRM and debug:
print("script timed out waiting to open pipe")
finish()
else: printbuffer()
def isvalidpluginfile(plg):
return os.path.isfile(plg)
def my_import(plgfile):
'''import plgfile as a python module and return
an error string if error - also return the prefunc if any'''
if not isvalidpluginfile(plgfile):
return ("%s is not a valid plugin filename" % plgfile, None, None)
# __import__ searches for the file in sys.path - we cannot
# __import__ a file by the full path
# __import__('basename') looks for basename.py in sys.path
(dir, fname) = os.path.split(plgfile)
base = os.path.splitext(fname)[0]
if not dir: dir = "."
sys.path.insert(0, dir) # put our path first so it will find our file
mod = __import__(base) # will throw exception if problem with python file
sys.path.pop(0) # remove our path
# check for the plugin functions
plgfunc = getattr(mod, 'plugin', None)
if not plgfunc:
return ('%s does not specify a plugin function' % plgfile, None, base)
if not isinstance(plgfunc, types.FunctionType):
return ('the symbol "plugin" in %s is not a function' % plgfile, None, base)
plgfuncs.append(plgfunc) # add to list in cmd line order
# check for 'post' func
plgpostfunc = getattr(mod, 'post', None)
if plgpostfunc:
if not isinstance(plgpostfunc, types.FunctionType):
return ('the symbol "post" in %s is not a function' % plgfile, None, base)
else:
plgpostfuncs.append(plgpostfunc) # add to list in cmd line order
prefunc = getattr(mod, 'pre', None)
# check for 'pre' func
if prefunc and not isinstance(prefunc, types.FunctionType):
return ('the symbol "pre" in %s is not a function' % plgfile, None, base)
return ('', prefunc, base)
def parse_plugins(parser, options, args):
'''Each plugin in the plugins list may have additional
arguments, specified on the command line like this:
--plugin=foo.py foo.bar=1 foo.baz=2 ...
that is, each argument to plugin X will be specified as X.arg=value'''
if not options.plugins: return args
for plgfile in options.plugins:
(errstr, prefunc, base) = my_import(plgfile)
if errstr:
parser.error(errstr)
return args
# parse the arguments to the plugin given on the command line
bvals = {} # holds plugin args and values, if any
newargs = []
for arg in args:
if arg.startswith(base + '.'):
argval = arg.replace(base + '.', '')
(plgarg, plgval) = argval.split('=', 1) # split at first =
if not plgarg in bvals:
bvals[plgarg] = plgval
elif isinstance(bvals[plgarg],list):
bvals[plgarg].append(plgval)
else: # convert to list
bvals[plgarg] = [bvals[plgarg], plgval]
else:
newargs.append(arg)
if prefunc:
if debug:
print('Calling "pre" function in', plgfile)
if not prefunc(bvals):
parser.error('the "pre" function in %s returned an error' % plgfile)
args = newargs
return args
def open_pipe(logfname):
opencompleted = False
logf = None
while not opencompleted:
try:
logf = open(logfname, 'r') # blocks until there is some input
opencompleted = True
except IOError as e:
if e.errno == errno.EINTR:
continue # open was interrupted, try again
else: # hard error
raise Exception("%s [%d]" % (e.strerror, e.errno))
return logf
def is_proc_alive(procpid):
retval = False
try:
retval = os.path.exists("/proc/%d" % procpid)
except IOError as e:
if e.errno != errno.ENOENT: # may not exist yet - that's ok
# otherwise, probably permissions or other badness
raise Exception("could not open file %s - %s [%d]" % (procfile, e.strerror, e.errno))
# using /proc/pid failed, try kill
if not retval:
try:
os.kill(procpid, 0) # sig 0 is a "ping"
retval = True # if we got here, proc exists
except OSError as e:
pass # no such process, or EPERM/EACCES
return retval
def get_pid_from_file(pidfile):
procpid = 0
if pidfile:
line = None
try:
pfd = open(pidfile, 'r')
line = pfd.readline()
pfd.close()
except IOError as e:
if e.errno != errno.ENOENT: # may not exist yet - that's ok
# otherwise, probably permissions or other badness
raise Exception("Could not read pid from file %s - %s [%d]" % (pidfile, e.strerror, e.errno))
if line:
procpid = int(line)
return procpid
def write_pid_file(pidfile):
try:
pfd = open(pidfile, 'w')
pfd.write("%d\n" % os.getpid())
pfd.close()
except IOError as e:
raise Exception("Could not write pid to file %s - %s [%d]" % (pidfile, e.strerror, e.errno))
def handle_script_pidfile(scriptpidfile):
scriptpid = get_pid_from_file(scriptpidfile)
# 0 if no file or no pid or error
if scriptpid and is_proc_alive(scriptpid):
# already running
if debug:
print("Script is already running: process id %d" % scriptpid)
return False
else:
# either process is not running or no file
# write our pid to the file
write_pid_file(scriptpidfile)
return True
def read_and_process_line(logf, plgfuncs):
line = None
done = False
readcompleted = False
while not readcompleted:
try:
line = logf.readline()
readcompleted = True # read completed
except IOError as e:
if e.errno == errno.EINTR:
continue # read was interrupted, try again
else: # hard error
raise Exception("%s [%d]" % (e.strerror, e.errno))
if line: # read something
for plgfunc in plgfuncs:
if not plgfunc(line):
print("Aborting processing due to function %s.%s" % (plgfunc.__module__, plgfunc.__name__))
finish() # this will exit the process
done = True
break
else: # EOF
done = True
return done
def parse_options():
from optparse import OptionParser
usage = "%prog <name of pipe> [options]"
parser = OptionParser(usage)
parser.add_option("-m", "--maxlines", dest="maxlines", type='int',
help="maximum number of lines to keep in the buffer", default=1000)
parser.add_option("-d", "--debug", dest="debug", action="store_true",
default=False, help="gather extra debugging information")
parser.add_option("-p", "--plugin", type='string', dest='plugins', action='append',
help='filename of a plugin to use with this log')
parser.add_option("-s", "--serverpidfile", type='string', dest='serverpidfile',
help='name of file containing the pid of the server to monitor')
parser.add_option("-t", "--servertimeout", dest="servertimeout", type='int',
help="timeout in seconds to wait for the serverpid to be alive. only applies when using -s or --serverpid", default=60)
parser.add_option("--serverpid", dest="serverpid", type='int',
help="process id of server to monitor", default=0)
parser.add_option("-u", "--user", type='string', dest='user',
help='name of user to set effective uid to')
parser.add_option("-i", "--scriptpidfile", type='string', dest='scriptpidfile',
help='name of file containing the pid of this script')
options, args = parser.parse_args()
args = parse_plugins(parser, options, args)
if len(args) < 1:
parser.error("You must specify the name of the pipe to use")
if len(args) > 1:
parser.error("error - unhandled command line arguments: %s" % args.join(' '))
return options, args[0]
options, logfname = parse_options()
if options.debug: debug = True
if len(plgfuncs) == 0:
plgfuncs.append(defaultplugin)
if len(plgpostfuncs) == 0:
plgpostfuncs.append(defaultpost)
if options.user:
try: userid = int(options.user)
except ValueError: # not a numeric userid - look it up
userid = pwd.getpwnam(options.user)[2]
os.seteuid(userid)
if options.scriptpidfile:
if not handle_script_pidfile(options.scriptpidfile):
options.scriptpidfile = None
sys.exit(1)
serverpid = options.serverpid
if serverpid:
if not is_proc_alive(serverpid):
print("Server pid [%d] is not alive - exiting" % serverpid)
sys.exit(1)
try:
if os.stat(logfname).st_mode & S_IFIFO:
if debug:
print("Using existing log pipe", logfname)
else:
print("Error:", logfname, "exists and is not a log pipe")
print("use a filename other than", logfname)
sys.exit(1)
except OSError as e:
if e.errno == errno.ENOENT:
if debug:
print("Creating log pipe", logfname)
os.mkfifo(logfname)
os.chmod(logfname, 0o600)
else:
raise Exception("%s [%d]" % (e.strerror, e.errno))
if debug:
print("Listening to log pipe", logfname, "number of lines", maxlines)
# set up our signal handlers
signal.signal(signal.SIGHUP, sighandler)
signal.signal(signal.SIGINT, sighandler)
#signal.signal(signal.SIGPIPE, sighandler)
signal.signal(signal.SIGTERM, sighandler)
signal.signal(signal.SIGALRM, sighandler)
timerisset = False
neverdone = False
if options.serverpidfile:
# start the timer to wait for the pid file to be available
signal.setitimer(signal.ITIMER_REAL, options.servertimeout)
timerisset = True
# if we are tracking a server, we will be done
# when the server exits
# if not tracking a server, we will only be done
# when we are killed
if not serverpid and not options.serverpidfile:
neverdone = True
done = False
while not done:
# open the pipe - will hang until
# 1. something opens the other end
# 2. alarm goes off - will just exit
logf = open_pipe(logfname)
# if we get here, logf is not None
if debug:
print("opened pipe", logf)
if timerisset:
# cancel the timer - the open succeeded
timerisset = False
signal.setitimer(signal.ITIMER_REAL, 0)
if debug:
print("cancelled startup timer")
lines = 0
# read and process the next line in the pipe
# if server exits while we are reading, we will get
# EOF and the func will return True - will also
# return True if a plugin returns failure
while not read_and_process_line(logf, plgfuncs):
lines += 1
# the other end of the pipe closed - we close our end too
if debug:
print("read", lines, "lines")
logf.close()
logf = None
if debug:
print("closed log pipe", logfname)
if not serverpid and options.serverpidfile:
# see if the server has written its server pid file yet
# it may take a "long time" for the server to actually
# write its pid file
serverpid = get_pid_from_file(options.serverpidfile)
# if the server is no longer running, just finish
if serverpid and not is_proc_alive(serverpid):
done = True
if debug:
print("server pid", serverpid, "exited - script exiting")
if neverdone:
done = False
elif not done:
if not lines:
# at startup the server will close the log and reopen it
# when it does this lines will be 0 - this means we need
# immediately attempt to reopen the log pipe and read it
# however, at shutdown, the server will close the log before
# the process has exited - so is_proc_alive will return
# true for a short time - if we then attempt to open the
# pipe, the open will hang forever - to avoid this situation
# we set the alarm again to wake up the open - use a short
# timeout so we don't wait a long time if the server
# really is exiting
signal.setitimer(signal.ITIMER_REAL, 0.25)
timerisset = True
if debug:
print("set startup timer - see if server is really shut down")
else: # we read something
# pipe closed - usually when server shuts down
done = True
if not done and debug:
print("log pipe", logfname, "closed - reopening - read", totallines, "total lines")
finish()
|