diff --git a/picard/audit.py b/picard/audit.py new file mode 100644 index 00000000000..99a27ea1b4a --- /dev/null +++ b/picard/audit.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# +# Picard, the next-generation MusicBrainz tagger +# +# Copyright (C) 2023 Laurent Monin +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + +from collections import defaultdict + +import sys + + +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""" + 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 diff --git a/picard/tagger.py b/picard/tagger.py index 5c1f1dcf38e..7bb9a79023d 100644 --- a/picard/tagger.py +++ b/picard/tagger.py @@ -81,6 +81,7 @@ NatAlbum, run_album_post_removal_processors, ) +from picard.audit import setup_audit from picard.browser.browser import BrowserIntegration from picard.browser.filelookup import FileLookup from picard.cluster import ( @@ -256,6 +257,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 +1442,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")