diff --git a/picard/tagger.py b/picard/tagger.py index 5c1f1dcf38e..a9bd5f6c830 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -212,6 +212,60 @@ def __str__(self): return f"files: {repr(self.files)} mbids: f{repr(self.mbids)} urls: {repr(self.urls)}" +def setup_audit(prefixes_string): + """Setup audit hook according to `audit` command-line option""" + if 'all' in prefixes_string.split(','): + def event_match(event): + return ('all', ) + else: + # prebuild the dict, constant + PREFIXES_DICT = make_events_prefixes(prefixes_string) + + def event_match(event): + return event_match_prefixes(event, PREFIXES_DICT) + + def audit(event, args): + # we can't use log here, as it generates events + matched = event_match(event) + if matched: + matched = '.'.join(matched) + print(f'audit:{matched}: {event} args={args}') + + sys.addaudithook(audit) + + +def make_events_prefixes(prefixes_string): + """Build a dict with keys = length of prefix""" + from collections import defaultdict + d = defaultdict(list) + for p in set(tuple(e.split('.')) for e in prefixes_string.split(',')): + d[len(p)].append(p) + return d + + +def prefixes_candidates_for_length(length, prefixes_dict): + """Generate prefixes that may match this length""" + for plen, v in prefixes_dict.items(): + if length >= plen: + yield from v + + +def event_match_prefixes(event, prefixes_dict): + """Matches event against prefixes + Typical case: we want to match `os.mkdir` if prefix is `os` or `os.mkdir` + but not the reverse: if prefix is `os.mkdir` we don't want to match an event named `os` + It returns False, or the matched prefix + """ + ev = tuple(event.split('.')) + ev_len = len(ev) + # only use candidates that may have a chance to match + for p in prefixes_candidates_for_length(ev_len, prefixes_dict): + # check that all elements of ev are in p + if all(v == ev[i] for i, v in enumerate(p)): + return p + return False + + class Tagger(QtWidgets.QApplication): tagger_stats_changed = QtCore.pyqtSignal() @@ -256,6 +310,10 @@ def __init__(self, picard_args, localedir, autoupdate, pipe_handler=None): if picard_args.debug or "PICARD_DEBUG" in os.environ: self.set_log_level(logging.DEBUG) + if sys.version_info[:3] > (3, 8): + if picard_args.audit: + setup_audit(picard_args.audit) + # Main thread pool used for most background tasks self.thread_pool = QtCore.QThreadPool(self) # Two threads are needed for the pipe handler and command processing. @@ -1437,6 +1495,11 @@ def process_picard_args(): parser.add_argument("-display", nargs=1, help=argparse.SUPPRESS) # Picard specific arguments + if sys.version_info[:3] > (3, 8): + parser.add_argument("-a", "--audit", action='store', + default=None, + help="audit events passed as a comma-separated list, prefixes supported, " + "use all to match any (see https://docs.python.org/3/library/audit_events.html#audit-events)") parser.add_argument("-c", "--config-file", action='store', default=None, help="location of the configuration file")