From b11669e57d4d3716699007bd70bfcf4cde52003d Mon Sep 17 00:00:00 2001 From: Simon Latapie Date: Fri, 3 May 2024 11:14:04 +0200 Subject: [PATCH 1/7] generator: fix python 3.12 warning Citing from Python 3.12 release: "A backslash-character pair that is not a valid escape sequence now generates a SyntaxWarning (...) In a future Python version, SyntaxError will eventually be raised, instead of SyntaxWarning." https://docs.python.org/dev/whatsnew/3.12.html#other-language-changes --- generator/generate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/generator/generate.py b/generator/generate.py index 89c1c31..89a82bd 100755 --- a/generator/generate.py +++ b/generator/generate.py @@ -501,7 +501,7 @@ def parse_param(cls, param_raw): assert('const' not in param_raw) # normalize spaces - param_raw = re.sub('\s+', ' ', param_raw) + param_raw = re.sub(r'\s+', ' ', param_raw) split_value = param_raw.split(' ') if len(split_value) > 1: From 6d73c64f125d205a2b1da3b47a76822502f90b9b Mon Sep 17 00:00:00 2001 From: Simon Latapie Date: Fri, 3 May 2024 16:37:36 +0200 Subject: [PATCH 2/7] generator: add a find_libc library Add a way to load the local standard C library. This could be used to load some C standard functions, like *printf ones. This should work on Linux, Windows and macOS/Darwin environments. --- generator/templates/header.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/generator/templates/header.py b/generator/templates/header.py index a545409..da567b9 100755 --- a/generator/templates/header.py +++ b/generator/templates/header.py @@ -45,6 +45,7 @@ import os import sys import functools +import platform # Used by EventManager in override.py import inspect as _inspect @@ -216,6 +217,22 @@ def find_lib(): # plugin_path used on win32 and MacOS in override.py dll, plugin_path = find_lib() +def find_libc(): + """Return an instance of the loaded standard C library or raise an error if + the library has not been found. + + This function should be compatible with Linux, Windows and macOS. + """ + system = platform.system() + if system == 'Windows': + # On Windows, msvcrt provides MS standard C + return ctypes.cdll.msvcrt + elif system == 'Linux' or system == 'Darwin': + libc_path = find_library('c') + if libc_path is None: + raise NotImplementedError('Cannot find a proper standard C library') + return ctypes.CDLL(libc_path) + class VLCException(Exception): """Exception raised by libvlc methods. """ From 6896d1fe2333b5bad032d1b0bb13b80ae0176fde Mon Sep 17 00:00:00 2001 From: Simon Latapie Date: Fri, 3 May 2024 16:40:37 +0200 Subject: [PATCH 3/7] Instance: add a set_logger method Adds a way to link libVLC log system to a python logging.Logger object. The logger and log callback is attached to the libVLC Instance object to avoid the python garbage collector to destroy them too soon. NOTE: as libVLC log messages are based on a printf + va_list format, we need to use the standard C `vsnprintf` function with a max fixed size output. --- generator/templates/header.py | 14 ++++++++++ generator/templates/override.py | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/generator/templates/header.py b/generator/templates/header.py index da567b9..ce472ec 100755 --- a/generator/templates/header.py +++ b/generator/templates/header.py @@ -489,6 +489,20 @@ class EventUnion(ctypes.Union): ('new_length', ctypes.c_longlong), ] +def loglevel_to_logging(level): + """Converts VLC log level to python logging Log level + """ + if level == LogLevel.DEBUG: + return logging.DEBUG + elif level == LogLevel.ERROR: + return logging.ERROR + elif level == LogLevel.NOTICE: + return logging.INFO + elif level == LogLevel.WARNING: + return logging.WARNING + else: + return logging.INFO + # Generated structs # # GENERATED_STRUCTS go here # see generate.py # End of generated structs # diff --git a/generator/templates/override.py b/generator/templates/override.py index 1934b6b..f772c25 100644 --- a/generator/templates/override.py +++ b/generator/templates/override.py @@ -7,6 +7,7 @@ class Instance: - a list of strings as first parameters - the parameters given as the constructor parameters (must be strings) """ + def __new__(cls, *args): if len(args) == 1: # Only 1 arg. It is either a C pointer, or an arg string, @@ -142,6 +143,52 @@ def video_filter_list_get(self): """ return module_description_list(libvlc_video_filter_list_get(self)) + def set_logger(self, logger, max_log_message_size=4096): + """Links a logging.Logger object to the libVLC Instance + + Along with the log level and message, each libVLC log will also include + The following extra info: + - vlc_module: the name of the VLC module (str). + - file: the VLC source filename (str). + - line: the VLC source file line number (int). + These variables can be used in the logger formatter. + + @param logger: a logging.Logger object + @param max_log_message_size: defines the maximum size that will be + copied from VLC log messages. If you experience truncated log + messages, raise this number (default 4096). + """ + # libVLC provides the log message through a printf format + va_list. + # Unfortunately, there is no simple way to use a + # printf format + va_list in Python outside of the use of a C format + # function. + # As there is no guarantee to have access to a C `vasprintf`, we use + # `vsnprintf` with a log message max size. + libc = find_libc() + self._vsnprintf = libc.vsnprintf + self._max_log_message_size = max_log_message_size + self._logger = logger + # The log_handler is meant to be the "real" callback for libvlc_log_set(). + @CallbackDecorators.LogCb + def log_handler(instance, log_level, ctx, fmt, va_list): + bufferString = ctypes.create_string_buffer(self._max_log_message_size) + self._vsnprintf(bufferString, self._max_log_message_size, fmt, + ctypes.cast(va_list, ctypes.c_void_p)) + msg = bufferString.value.decode('utf-8') + module, file, line = libvlc_log_get_context(ctx) + module = module.decode('utf-8') + file = file.decode('utf-8') + self._logger.log(loglevel_to_logging(LogLevel(log_level)), + msg, extra={"vlc_module": module, "file": file, "line": line}) + # We need to keep a reference to the log_handler function that persists + # after the end of the set_logger. + # If we do not do that, there is a (high) chance the python garbage + # collector will destroy the callback function and produce segfault in + # libVLC. + # To avoid that, link the log_handler lifetime to the Instance's one. + self._log_handler = log_handler + self.log_set(self._log_handler, None) + class Media: """Create a new Media instance. From 722c546491c4d8e080f4537227e10c62b8b227c2 Mon Sep 17 00:00:00 2001 From: Simon Latapie Date: Fri, 3 May 2024 16:50:17 +0200 Subject: [PATCH 4/7] test: add a logger test Adds a functional test for the Instance.set_logger method. This will also test the log_get_context, as this is part of the logger output. --- tests/test.py | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/tests/test.py b/tests/test.py index c08aa0e..bfd027e 100755 --- a/tests/test.py +++ b/tests/test.py @@ -32,6 +32,8 @@ import ctypes import os import unittest +import io +import re try: import urllib.parse as urllib # python3 @@ -203,33 +205,37 @@ def test_meta_get(self): self.assertEqual(m.get_meta(vlc.Meta.Date), '2013') self.assertEqual(m.get_meta(vlc.Meta.Genre), 'Sample') - def notest_log_get_context(self): - """Semi-working test for log_get_context. - - It crashes with a Segmentation fault after displaying some - messages. This should be fixed + a better test should be - devised so that we do not clutter the terminal. - """ - libc = ctypes.cdll.LoadLibrary("libc.{}".format("so.6" if os.uname()[0] == 'Linux' else "dylib")) - @vlc.CallbackDecorators.LogCb - def log_handler(instance, log_level, ctx, fmt, va_list): - bufferString = ctypes.create_string_buffer(4096) - libc.vsprintf(bufferString, fmt, ctypes.cast(va_list, ctypes.c_void_p)) - msg = bufferString.value.decode('utf-8') - module, _file, _line = vlc.libvlc_log_get_context(ctx) - module = module.decode('utf-8') - try: - logger.warn(u"log_level={log_level}, module={module}, msg={msg}".format(log_level=log_level, module=module, msg=msg)) - except Exception as e: - logger.exception(e) - import pdb; pdb.set_trace() - + def test_set_logger(self): + vlc_logger = logging.Logger("VLC") + vlc_logger.setLevel(logging.DEBUG) + # push logs in memory + in_mem = io.StringIO() + handler = logging.StreamHandler(in_mem) + formatter = logging.Formatter('%(asctime)s;;;%(name)s;;;%(levelname)s;;;%(vlc_module)s;;;%(file)s;;;%(line)d;;;%(message)s') + handler.setFormatter(formatter) + vlc_logger.addHandler(handler) instance = vlc.Instance('--vout dummy --aout dummy') - instance.log_set(log_handler, None) + instance.set_logger(vlc_logger) player = instance.media_player_new() media = instance.media_new(SAMPLE) player.set_media(media) player.play() + player.stop() + handler.flush() + # check logs + # there can be a lot of log message to check + # just try to find a message we are sure to get + # Example: + # 2024-05-03 18:44:31,054;;;VLC;;;DEBUG;;;main;;;;;;;;;creating demux: access='file' demux='any' location='' file='' + pattern = r"(?P.*);;;VLC;;;DEBUG;;;main;;;(?P.*);;;(?P.*);;;creating demux: access='file' demux='any' location='(?P.*)' file='(?P.*)'" + found = False + for line in in_mem.getvalue().splitlines(): + #print(line) + m = re.match(pattern, line) + if m is not None: + found = True + break + self.assertEqual(found, True, "Cannot find a proper log message for demux creation") if generate is not None: From 22a22f08de9c4d9a57a986f270499304d300d9f9 Mon Sep 17 00:00:00 2001 From: Simon Latapie Date: Fri, 3 May 2024 23:53:34 +0200 Subject: [PATCH 5/7] Instance: add the logger getter This can be useful if some logger is set and you want to get access to it afterwards. --- generator/templates/override.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/generator/templates/override.py b/generator/templates/override.py index f772c25..c460ceb 100644 --- a/generator/templates/override.py +++ b/generator/templates/override.py @@ -189,6 +189,11 @@ def log_handler(instance, log_level, ctx, fmt, va_list): self._log_handler = log_handler self.log_set(self._log_handler, None) + def get_logger(self): + """Return the logger attached to the libVLC Instance (None by default) + """ + return getattr(self, '_logger', None) + class Media: """Create a new Media instance. From 11a6b8ab109f21575e67f7258f9659f7cf671060 Mon Sep 17 00:00:00 2001 From: Simon Latapie Date: Sat, 4 May 2024 00:29:08 +0200 Subject: [PATCH 6/7] test: logger: start the VLC instance in quiet mode --- tests/test.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/test.py b/tests/test.py index bfd027e..bf85857 100755 --- a/tests/test.py +++ b/tests/test.py @@ -214,7 +214,9 @@ def test_set_logger(self): formatter = logging.Formatter('%(asctime)s;;;%(name)s;;;%(levelname)s;;;%(vlc_module)s;;;%(file)s;;;%(line)d;;;%(message)s') handler.setFormatter(formatter) vlc_logger.addHandler(handler) - instance = vlc.Instance('--vout dummy --aout dummy') + # start in quiet mode: we cannot control how to logs are produced + # before setting the logger + instance = vlc.Instance('--vout dummy --aout dummy --quiet') instance.set_logger(vlc_logger) player = instance.media_player_new() media = instance.media_new(SAMPLE) From ce729188bebd818045a9f8926e963cbad4b3825e Mon Sep 17 00:00:00 2001 From: Simon Latapie Date: Tue, 21 May 2024 14:54:32 +0200 Subject: [PATCH 7/7] find_libc: raise an error on unsupported platforms --- generator/templates/header.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/generator/templates/header.py b/generator/templates/header.py index ce472ec..f57e3f8 100755 --- a/generator/templates/header.py +++ b/generator/templates/header.py @@ -232,6 +232,8 @@ def find_libc(): if libc_path is None: raise NotImplementedError('Cannot find a proper standard C library') return ctypes.CDLL(libc_path) + else: + raise NotImplementedError('Cannot find a proper standard C library (Unsupported platform)') class VLCException(Exception): """Exception raised by libvlc methods.