#!/usr/bin/env python # # Public Domain 2014-present MongoDB, Inc. # Public Domain 2008-2014 WiredTiger, Inc. # # This is free and unencumbered software released into the public domain. # # Anyone is free to copy, modify, publish, use, compile, sell, or # distribute this software, either in source code form or as a compiled # binary, for any purpose, commercial or non-commercial, and by any # means. # # In jurisdictions that recognize copyright laws, the author or authors # of this software dedicate any and all copyright interest in the # software to the public domain. We make this dedication for the benefit # of the public at large and to the detriment of our heirs and # successors. We intend this dedication to be an overt act of # relinquishment in perpetuity of all present and future rights to this # software under copyright law. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. # IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. # # [TEST_TAGS] # ignored_file # [END_TAGS] # # WiredTigerHookManager # Manage running of hooks # from __future__ import print_function import os, sys from importlib import import_module from abc import ABC, abstractmethod import wiredtiger # Three kinds of hooks available: HOOK_REPLACE = 1 # replace the call with the hook function HOOK_NOTIFY = 2 # call the hook function after the function HOOK_ARGS = 3 # transform the arg list before the call # Print to /dev/tty for debugging, since anything extraneous to stdout/stderr will # cause a test error. def tty(message): from wttest import WiredTigerTestCase WiredTigerTestCase.tty(message) ################ # Hooks Overview # # Here are some useful terms to know, with some commentary for each. # # API functions # potentially any WiredTiger API functions that a hook creator wishes to modify (like # Session.rename). In Python most everything is an object. Of course an instance of # "Session" is an object, but also the "Session" class itself is an object. The Session.rename # function is also an object (of a certain form that can be called). Also in Python, # attributes on an object don't have to be "pre-declared", they can be created at any time. # So it's easy to imagine assigning Session._rename_orig to be (the original value of) # Session.rename, and then assigning Session.rename to be some other function object, that # knows how to do something and then perhaps calls Session._rename_orig . This is the # essence of the hook concept. # # Hook Creator: # A way to attach a set of "behavior modifications" to various API functions. More precisely, # a hook creator derives from WiredTigerHookCreator and sets up a number of "hook functions", # that are actions that are done either just before, after, or instead of, an API function. # A XxxxHookCreator lives in a hook_xxxx.py file. When a HookCreator is loaded, it may be # given an optional argument. This argument comes from the original python command line. # For example, "python run.py --hook abc" loads hook_abc.py (where it expects to find a hook). # "python run.py --hook abc=123" loads hook_abc.py with an argument "123". # # Hook Function: # One function that will be called before, after or instead of, an API function. A hook # function will be bound to an API function. It is the job of the HookCreator to set up that # binding. It is possible to have multiple hook functions bound to the same API function. # A hook function that replaces an API function will have the same args as the function # it replaces (but there is a trick to give it additional context if needed - # see session_create_replace in hook_demo.py). # For every API function altered, there is one of these objects # stashed in the .__hooks attribute. class WiredTigerHookInfo(object): def __init__(self): self.arg_funcs = [] # The set of hook functions for manipulating arguments self.notify_funcs = [] # The set of hook functions for manipulating arguments # At the moment, we can only replace a method once. # If needed, we can think about removing this restriction. self.replace_func = None # hooked_function - # A helper function for the hook manager. def hooked_function(self, orig_func, hook_info_name, *args): hook_info = getattr(self, hook_info_name) notifies = [] replace_func = None # The three kinds of hooks are acted upon at different times. # Before we call the function, we modify the args as indicated # by hooks. Then we call the function, possibly with a replacement. # Finally, we'll call any notify hooks. # # We only walk through the hook list once, and process the config # hooks while we're doing that, and copy any other hooks needed. for hook_func in hook_info.arg_funcs: args = hook_func(self, args) call_func = hook_info.replace_func if call_func == None: call_func = orig_func if self == wiredtiger: ret = call_func(*args) else: ret = call_func(self, *args) for hook_func in hook_info.notify_funcs: hook_func(ret, self, *args) return ret # WiredTigerHookManager - # The hook manager class. There is only one hook manager. It is responsible for finding all the # HookCreators at the beginning of the run, and calling setup_hooks() for each one, to have it bind # hook functions to API functions. The hook manager is initialized with a list of hook names. Each # name is expanded, for example, "demo" causes the hook manager to load hook_demo.py, and to call # the "initialize" global function in that file. We expect "initialize" to return a list of objects # (hooks) derived from WiredTigerHook (class defined below). Generally, "initialize" returns a # single object (setting up some number of "hook functions") but to allow flexibility for different # sorts of packaging, we allow any number of hooks to be returned. # # A hook can set up any number of "hook functions". See hook_demo.py for a sample hook class. class WiredTigerHookManager(object): def __init__(self, hooknames = []): self.hooks = [] names_seen = [] for name in hooknames: # The hooks are indicated as "somename=arg" or simply "somename". # hook_somename.py will be imported, and initialized with the arg. # Names must be unique, as we stash some info into extra fields # on the connection/session/cursor, these are named using the # unique name of the hook. if '=' in name: name,arg = name.split('=', 1) else: arg = None if name in names_seen: raise Exception(name + ': hook name cannot be used multiple times') names_seen.append(name) modname = 'hook_' + name try: imported = import_module(modname) for hook in imported.initialize(arg): hook._initialize(name, self) self.hooks.append(hook) except: print('Cannot import hook: ' + name + ', check file ' + modname + '.py') raise for hook in self.hooks: hook.setup_hooks() def add_hook(self, clazz, method_name, hook_type, hook_func): if not hasattr(clazz, method_name): raise Exception('Cannot find method ' + method_name + ' on class ' + str(clazz)) # We need to set up some extra attributes on the Connection class. # Given that the method name is XXXX, and class is Connection, here's what we're doing: # orig = wiredtiger.Connection.XXXX # wiredtiger.Connection._XXXX_hooks = WiredTigerHookInfo() # wiredtiger.Connection._XXXX_orig = wiredtiger.Connection.XXXX # wiredtiger.Connection.XXXX = lambda self, *args: # hooked_function(self, orig, '_XXXX_hooks', *args) hook_info_name = '_' + method_name + '_hooks' orig_name = '_' + method_name + '_orig' if not hasattr(clazz, hook_info_name): #tty('Setting up hook on ' + str(clazz) + '.' + method_name) orig_func = getattr(clazz, method_name) if orig_func == None: raise Exception('method ' + method_name + ' hook setup: method does not exist') setattr(clazz, hook_info_name, WiredTigerHookInfo()) # If we're using the wiredtiger module and not a class, we need a slightly different # style of hooked_function, since there is no self. What would be the "self" argument # is in fact the class. if clazz == wiredtiger: f = lambda *args: hooked_function(wiredtiger, orig_func, hook_info_name, *args) else: f = lambda self, *args: hooked_function(self, orig_func, hook_info_name, *args) setattr(clazz, method_name, f) setattr(clazz, orig_name, orig_func) # Now add to the list of hook functions # If it's a replace hook, we only allow one of them for a given method name hook_info = getattr(clazz, hook_info_name) if hook_type == HOOK_ARGS: hook_info.arg_funcs.append(hook_func) elif hook_type == HOOK_NOTIFY: hook_info.notify_funcs.append(hook_func) elif hook_type == HOOK_REPLACE: if hook_info.replace_func == None: hook_info.replace_func = hook_func else: raise Exception('method ' + method_name + ' hook setup: trying to replace the same method with two hooks') #tty('Setting up hooks list in ' + str(clazz) + '.' + hook_info_name) def get_function(self, clazz, method_name): orig_name = '_' + method_name + '_orig' if hasattr(clazz, orig_name): orig_func = getattr(clazz, orig_name) else: orig_func = getattr(clazz, method_name) return orig_func def filter_tests(self, tests): for hook in self.hooks: tests = hook.filter_tests(tests) return tests class HookCreatorProxy(object): def __init__(self, hookmgr, clazz): self.hookmgr = hookmgr self.clazz = clazz # Get the original function/method before any hooks applied def __getitem__(self, name): return self.hookmgr.get_function(self.clazz, name) # Get the original function/method before any hooks applied def __setitem__(self, name, value): try: hooktype = int(value[0]) fcn = value[1] except: raise ValueError('value must be (HOOK_xxxx, function)') self.hookmgr.add_hook(self.clazz, name, hooktype, fcn) # Hooks must derive from this class class WiredTigerHookCreator(ABC): # This is called right after creation and should not be overridden. def _initialize(self, name, hookmgr): self.name = name self.hookmgr = hookmgr self.wiredtiger = HookCreatorProxy(self.hookmgr, wiredtiger) self.Connection = HookCreatorProxy(self.hookmgr, wiredtiger.Connection) self.Session = HookCreatorProxy(self.hookmgr, wiredtiger.Session) self.Cursor = HookCreatorProxy(self.hookmgr, wiredtiger.Cursor) # default version of filter_tests, can be overridden def filter_tests(self, tests): return tests @abstractmethod def setup_hooks(self): """Set up all hooks using add_*_hook methods.""" return