Detecting changes to loaded modules
I'm reworking the web server for my irrigation project. I've already got "kr", which restarts a program whenever it detects the source files have been updated, but first I need the existing web service to close down when there is a change to the source code. For this I implemented the code below, which defines a class. Calling an instance of the class will check the loaded
modules (with custom filtering available) for changes to their source code. If a change is found, a callback is made. You
can shut down your web server with the callback. The only tricky part is getting the event loop to periodically call the check function. It worked fine for my architecture that doesn't block indefinitely. Later I'll probably add an option for having a background thread do the monitoring.
The file is available on github here.
#!python
#Copyright 2015 Mark Santesson
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Test with: kr.py *.py -c watch_modules.py
import os
import re
import sys
class ImportedModulesTimestampChecker(object):
'''
A class to help detect when one of the source files for a
running process has changed.
The returned object should be called periodically to let it check
the timestamps of all files. There is, as of yet, no multi-threaded
version.
Typical use of this module would be to have it track the files
being actively developed and to call a function resulting in program
exit (perhaps sys.exit) when a change is detected. This works well
when combined with "kr" to relaunch the program.
'''
def __init__(self, on_difference_fn, filename_filter_fn=None):
'''
Takes two parameters:
on_difference_fn: a function taking the name of the file which
changed, which is called when the change is
detected.
filename_filter_fn: a function taking a module's filename and
which should return True if the file's timestamp
should be observed. If the filename filter is a
string, then any filenames containing that string
will be observed. If not present, then all modules
will be tracked.
'''
self._onDifferenceFn = on_difference_fn
if isinstance(filename_filter_fn, basestring):
filename_filter_fn = lambda x: filename_filter_fn in x
self._filenameFilterFn = filename_filter_fn or (lambda x:True)
self._timestamps = {}
def __call__(self):
'''
Call this periodically to do a check of the timestamps.
'''
# Get the timestamps on all matching files in this module's
# directory. If any have changed, quit.
all_files = [ x.__file__ for x in sys.modules.values()
if isinstance(x,type(sys))
and hasattr(x, '__file__')
and self._filenameFilterFn(x.__file__)
]
for module_name in sorted(all_files):
module_name = re.sub(r'\.pyc$', r'.py', module_name)
try:
ts = os.stat(module_name).st_mtime
except WindowsError: # TODO: What is the error on Linux?
logging.exception(module_name)
pass
else:
if ts > self._timestamps.setdefault(module_name, ts):
self._onDifferenceFn(module_name)
self._timestamps[module_name] = ts
def main():
import os.path
import SocketServer
import logging
# From an example in the documentation for SocketServer.
class TinyHandler(SocketServer.StreamRequestHandler):
def handle(self):
# Get one line.
self.data = self.rfile.readline().strip()
logging.info('Received: %r', self.data)
out = self.data.lower()
self.wfile.write(out)
logging.info('Sending : %r', out)
class TinySocketServer(SocketServer.TCPServer):
def __init__(self):
self._address = ("localhost", 9999)
SocketServer.TCPServer.__init__( self
, self._address
, TinyHandler )
self.timeout = 0.5
self.quit = False
self._timestampChecker = ImportedModulesTimestampChecker\
( self.on_module_modification
, self.module_name_filter
)
@staticmethod
def module_name_filter(module_name):
this_mod_name = os.path.basename(__file__).split('.')[0]
return this_mod_name in module_name
def on_module_modification(self, module_name):
logging.info('Module %s was modified, exiting.', module_name)
self.quit = True
def run(self):
logging.info('Listening at: %s', self._address)
while not self.quit:
self._timestampChecker()
self.handle_request()
logging.info('Quitting.')
logging.getLogger().setLevel(20)
tws = TinySocketServer()
tws.run()
if __name__ == "__main__":
main()