From 75d0adf27de754c418f6e97a0a24866f6bcb88c0 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Wed, 5 May 2021 10:01:21 -0400 Subject: [PATCH] Add concurrency fix Related: #1957130 --- ...ule-handle-two-threads-loading-type-.patch | 290 ++++++++++++++++++ pygobject3.spec | 8 +- 2 files changed, 297 insertions(+), 1 deletion(-) create mode 100644 0001-IntrospectionModule-handle-two-threads-loading-type-.patch diff --git a/0001-IntrospectionModule-handle-two-threads-loading-type-.patch b/0001-IntrospectionModule-handle-two-threads-loading-type-.patch new file mode 100644 index 0000000..a38c813 --- /dev/null +++ b/0001-IntrospectionModule-handle-two-threads-loading-type-.patch @@ -0,0 +1,290 @@ +From 1212d1cd3d4b47294770408a7abd18bc4c578a64 Mon Sep 17 00:00:00 2001 +From: Ray Strode +Date: Wed, 10 Jun 2020 18:04:07 -0400 +Subject: [PATCH] IntrospectionModule: handle two threads loading type at same + time + +If two threads are trying to load a type at exactly the same time, +it's possible for two wrappers to get generated for the type. +One thread will end up with the wrapper that's not blessed as the +"real" one and future calls will fail. The blessed wrapper will +be incomplete, and so future calls from it will fail as well. + +This commit adds a lock to ensure the two threads don't stomp +on each others toes. +--- + gi/module.py | 110 +++++++++++++++++++++++++++------------------------ + 1 file changed, 58 insertions(+), 52 deletions(-) + +diff --git a/gi/module.py b/gi/module.py +index f9e26bc2..93b8e6a3 100644 +--- a/gi/module.py ++++ b/gi/module.py +@@ -1,53 +1,54 @@ + # -*- Mode: Python; py-indent-offset: 4 -*- + # vim: tabstop=4 shiftwidth=4 expandtab + # + # Copyright (C) 2007-2009 Johan Dahlin + # + # module.py: dynamic module for introspected libraries. + # + # This library is free software; you can redistribute it and/or + # modify it under the terms of the GNU Lesser General Public + # License as published by the Free Software Foundation; either + # version 2.1 of the License, or (at your option) any later version. + # + # This library 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 + # Lesser General Public License for more details. + # + # You should have received a copy of the GNU Lesser General Public + # License along with this library; if not, write to the Free Software + # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 + # USA + + import importlib ++from threading import Lock + + import gi + + from ._gi import \ + Repository, \ + FunctionInfo, \ + RegisteredTypeInfo, \ + EnumInfo, \ + ObjectInfo, \ + InterfaceInfo, \ + ConstantInfo, \ + StructInfo, \ + UnionInfo, \ + CallbackInfo, \ + Struct, \ + Boxed, \ + CCallback, \ + enum_add, \ + enum_register_new_gtype_and_add, \ + flags_add, \ + flags_register_new_gtype_and_add, \ + GInterface + from .types import \ + GObjectMeta, \ + StructMeta + + from ._constants import \ + TYPE_NONE, \ + TYPE_BOXED, \ + TYPE_POINTER, \ +@@ -90,152 +91,157 @@ def get_interfaces_for_object(object_info): + namespace = interface_info.get_namespace() + name = interface_info.get_name() + + module = importlib.import_module('gi.repository.' + namespace) + interfaces.append(getattr(module, name)) + return interfaces + + + class IntrospectionModule(object): + """An object which wraps an introspection typelib. + + This wrapping creates a python module like representation of the typelib + using gi repository as a foundation. Accessing attributes of the module + will dynamically pull them in and create wrappers for the members. + These members are then cached on this introspection module. + """ + def __init__(self, namespace, version=None): + """Might raise gi._gi.RepositoryError""" + + repository.require(namespace, version) + self._namespace = namespace + self._version = version + self.__name__ = 'gi.repository.' + namespace + + path = repository.get_typelib_path(self._namespace) + self.__path__ = [path] + + if self._version is None: + self._version = repository.get_version(self._namespace) + ++ self._lock = Lock() ++ + def __getattr__(self, name): + info = repository.find_by_name(self._namespace, name) + if not info: + raise AttributeError("%r object has no attribute %r" % ( + self.__name__, name)) + + if isinstance(info, EnumInfo): + g_type = info.get_g_type() +- wrapper = g_type.pytype + +- if wrapper is None: +- if info.is_flags(): +- if g_type.is_a(TYPE_FLAGS): +- wrapper = flags_add(g_type) +- else: +- assert g_type == TYPE_NONE +- wrapper = flags_register_new_gtype_and_add(info) +- else: +- if g_type.is_a(TYPE_ENUM): +- wrapper = enum_add(g_type) ++ with self._lock: ++ wrapper = g_type.pytype ++ ++ if wrapper is None: ++ if info.is_flags(): ++ if g_type.is_a(TYPE_FLAGS): ++ wrapper = flags_add(g_type) ++ else: ++ assert g_type == TYPE_NONE ++ wrapper = flags_register_new_gtype_and_add(info) + else: +- assert g_type == TYPE_NONE +- wrapper = enum_register_new_gtype_and_add(info) +- +- wrapper.__info__ = info +- wrapper.__module__ = 'gi.repository.' + info.get_namespace() +- +- # Don't use upper() here to avoid locale specific +- # identifier conversion (e. g. in Turkish 'i'.upper() == 'i') +- # see https://bugzilla.gnome.org/show_bug.cgi?id=649165 +- ascii_upper_trans = ''.maketrans( +- 'abcdefgjhijklmnopqrstuvwxyz', +- 'ABCDEFGJHIJKLMNOPQRSTUVWXYZ') +- for value_info in info.get_values(): +- value_name = value_info.get_name_unescaped().translate(ascii_upper_trans) +- setattr(wrapper, value_name, wrapper(value_info.get_value())) +- for method_info in info.get_methods(): +- setattr(wrapper, method_info.__name__, method_info) +- +- if g_type != TYPE_NONE: +- g_type.pytype = wrapper ++ if g_type.is_a(TYPE_ENUM): ++ wrapper = enum_add(g_type) ++ else: ++ assert g_type == TYPE_NONE ++ wrapper = enum_register_new_gtype_and_add(info) ++ ++ wrapper.__info__ = info ++ wrapper.__module__ = 'gi.repository.' + info.get_namespace() ++ ++ # Don't use upper() here to avoid locale specific ++ # identifier conversion (e. g. in Turkish 'i'.upper() == 'i') ++ # see https://bugzilla.gnome.org/show_bug.cgi?id=649165 ++ ascii_upper_trans = ''.maketrans( ++ 'abcdefgjhijklmnopqrstuvwxyz', ++ 'ABCDEFGJHIJKLMNOPQRSTUVWXYZ') ++ for value_info in info.get_values(): ++ value_name = value_info.get_name_unescaped().translate(ascii_upper_trans) ++ setattr(wrapper, value_name, wrapper(value_info.get_value())) ++ for method_info in info.get_methods(): ++ setattr(wrapper, method_info.__name__, method_info) ++ ++ if g_type != TYPE_NONE: ++ g_type.pytype = wrapper + + elif isinstance(info, RegisteredTypeInfo): + g_type = info.get_g_type() + + # Create a wrapper. + if isinstance(info, ObjectInfo): + parent = get_parent_for_object(info) + interfaces = tuple(interface for interface in get_interfaces_for_object(info) + if not issubclass(parent, interface)) + bases = (parent,) + interfaces + metaclass = GObjectMeta + elif isinstance(info, CallbackInfo): + bases = (CCallback,) + metaclass = GObjectMeta + elif isinstance(info, InterfaceInfo): + bases = (GInterface,) + metaclass = GObjectMeta + elif isinstance(info, (StructInfo, UnionInfo)): + if g_type.is_a(TYPE_BOXED): + bases = (Boxed,) + elif (g_type.is_a(TYPE_POINTER) or + g_type == TYPE_NONE or + g_type.fundamental == g_type): + bases = (Struct,) + else: + raise TypeError("unable to create a wrapper for %s.%s" % (info.get_namespace(), info.get_name())) + metaclass = StructMeta + else: + raise NotImplementedError(info) + +- # Check if there is already a Python wrapper that is not a parent class +- # of the wrapper being created. If it is a parent, it is ok to clobber +- # g_type.pytype with a new child class wrapper of the existing parent. +- # Note that the return here never occurs under normal circumstances due +- # to caching on the __dict__ itself. +- if g_type != TYPE_NONE: +- type_ = g_type.pytype +- if type_ is not None and type_ not in bases: +- self.__dict__[name] = type_ +- return type_ +- +- dict_ = { +- '__info__': info, +- '__module__': 'gi.repository.' + self._namespace, +- '__gtype__': g_type +- } +- wrapper = metaclass(name, bases, dict_) +- +- # Register the new Python wrapper. +- if g_type != TYPE_NONE: +- g_type.pytype = wrapper ++ with self._lock: ++ # Check if there is already a Python wrapper that is not a parent class ++ # of the wrapper being created. If it is a parent, it is ok to clobber ++ # g_type.pytype with a new child class wrapper of the existing parent. ++ # Note that the return here never occurs under normal circumstances due ++ # to caching on the __dict__ itself. ++ if g_type != TYPE_NONE: ++ type_ = g_type.pytype ++ if type_ is not None and type_ not in bases: ++ self.__dict__[name] = type_ ++ return type_ ++ ++ dict_ = { ++ '__info__': info, ++ '__module__': 'gi.repository.' + self._namespace, ++ '__gtype__': g_type ++ } ++ wrapper = metaclass(name, bases, dict_) ++ ++ # Register the new Python wrapper. ++ if g_type != TYPE_NONE: ++ g_type.pytype = wrapper + + elif isinstance(info, FunctionInfo): + wrapper = info + elif isinstance(info, ConstantInfo): + wrapper = info.get_value() + else: + raise NotImplementedError(info) + + # Cache the newly created wrapper which will then be + # available directly on this introspection module instead of being + # lazily constructed through the __getattr__ we are currently in. + self.__dict__[name] = wrapper + return wrapper + + def __repr__(self): + path = repository.get_typelib_path(self._namespace) + return "" % (self._namespace, path) + + def __dir__(self): + # Python's default dir() is just dir(self.__class__) + self.__dict__.keys() + result = set(dir(self.__class__)) + result.update(self.__dict__.keys()) + + # update *set* because some repository attributes have already been + # wrapped by __getattr__() and included in self.__dict__; but skip + # Callback types, as these are not real objects which we can actually + # get + namespace_infos = repository.get_infos(self._namespace) + result.update(info.get_name() for info in namespace_infos if + not isinstance(info, CallbackInfo)) +-- +2.31.1 + diff --git a/pygobject3.spec b/pygobject3.spec index a3d79dd..46da265 100644 --- a/pygobject3.spec +++ b/pygobject3.spec @@ -6,7 +6,7 @@ Name: pygobject3 Version: 3.40.1 -Release: 1%{?dist} +Release: 2%{?dist} Summary: Python bindings for GObject Introspection License: LGPLv2+ and MIT @@ -21,6 +21,8 @@ BuildRequires: python3-devel >= %{python3_version} BuildRequires: python3-cairo-devel >= %{pycairo_version} BuildRequires: python3-setuptools +Patch10001: 0001-IntrospectionModule-handle-two-threads-loading-type-.patch + %description The %{name} package provides a convenient wrapper for the GObject library for use in Python programs. @@ -83,6 +85,10 @@ This package contains files required to embed PyGObject %{_libdir}/pkgconfig/pygobject-3.0.pc %changelog +* Wed May 05 2021 Ray Strode - 3.40.1-2 +- Add concurrency fix + Related: #1957130 + * Tue Mar 30 2021 Kalev Lember - 3.40.1-1 - Update to 3.40.1