Fix AttributeError in egl.GLContext.__del__ on partial construction - #3497
Open
shoemoney wants to merge 1 commit into
Open
Fix AttributeError in egl.GLContext.__del__ on partial construction#3497shoemoney wants to merge 1 commit into
shoemoney wants to merge 1 commit into
Conversation
If GLContext.__init__ raises before self._context is assigned -- e.g. because the EGL driver lacks PLATFORM_DEVICE support and create_initialized_egl_device_display() returns EGL_NO_DISPLAY -- __del__ later calls free(), which unconditionally reads self._context and raises AttributeError. That masks the real, actionable ImportError raised from __init__. See google-deepmind#991. Pre-initialize self._context to None as the first statement of __init__, mirroring the fix already applied to Renderer in google-deepmind#3225.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
self._context = Noneas the first statement ofGLContext.__init__inpython/mujoco/egl/__init__.py, sofree()(called from__del__) is safe on a partially-constructed instance.free()/__del__do not raiseAttributeErroron aGLContextthat never ran__init__.Why
On a headless host whose EGL driver lacks the
PLATFORM_DEVICEextension,create_initialized_egl_device_display()returnsEGL_NO_DISPLAYandGLContext.__init__raisesImportErrorat line 97-101, beforeself._contextis ever assigned (it's first set at line 114). When the partially-constructed object is later garbage collected,__del__callsfree(), which unconditionally readsself._context:This raises
AttributeError: 'GLContext' object has no attribute '_context'from__del__, surfaced viasys.unraisablehookas a noisyException ignored in: <function GLContext.__del__ ...>traceback -- which masks the real, actionableImportErrorthat told the user EGL is unavailable. Reported in #991.This is the same bug class fixed for
Renderer.__init__/renderer.pyin #3225 (issue #3213): pre-initialize the guarded attribute toNonebefore anything that can raise, so__del__is always safe regardless of how far__init__got.The fix is a single line:
Test
hasattr(mujoco, 'GLContext')-style tests (used byrender_test.py/renderer_test.py) are skipped in this repo's own CI, which runs the Python test job withMUJOCO_GL=disable. Reliably forcingGLContext.__init__itself to fail the way a real headless host withoutPLATFORM_DEVICEsupport would additionally requires an EGL driver that's present but specifically lacks that extension -- not a precondition a test environment can create on demand.So
python/mujoco/egl_gl_context_test.pyinstead patchescreate_initialized_egl_device_display(viaunittest.mock.patch.object) to returnEGL.EGL_NO_DISPLAY, exactly as it would on such a host, and drives a realGLContext(640, 480)call through its real__init__code path. The test asserts__init__still raisesImportError(the real, actionable error), then garbage-collects the partially-constructed object undersys.unraisablehookand asserts nothing was raised from__del__. It skips only when themujoco.eglmodule itself can't be imported (nolibEGL/libOpenGLon the host -- an environment precondition orthogonal to this bug, and the same reason this repo's own CI runs withMUJOCO_GL=disable).Scope
This PR fixes only the missing-attribute-on-
__del__bug. It does not touch the unrelatedEGL_DISPLAY-goes-stale-after-atexit-termination bug that #2510 (open) fixes in the same file'sfree()/make_current()-- that PR addsEGL_DISPLAY andto theif self._context:check and swapsatexit.register(EGL.eglTerminate, ...)for a wrapper that nullsEGL_DISPLAYafter termination; it does not pre-initializeself._context, so it does not fix this bug, and this PR's one-line change does not touch either of the lines #2510 touches.Validation
Built MuJoCo from source (CMake + Ninja, Release, no IPO/LTO) and the Python bindings against it (Python 3.12), then ran
pytest -v --pyargs mujocowithMUJOCO_GL=disable-- matching this repo's own CI invocation (.github/workflows/build.yml, "Test Python bindings" step) -- on both cleanmainand this branch:main(baseline): 380 passed, 5 failed, 14 skipped.The 5 failures are identical on both and are all plugin-loading failures unrelated to this change (my local build didn't install/copy the plugin shared libraries the way CI's
copy_plugins_posixstep does):bindings_test.py::MuJoCoBindingsTest::test_copy_mjdata_with_plugin,test_deepcopy_mjdata_with_plugin,test_load_plugin,specs_test.py::SpecsTest::test_delete_unused_plugin,test_plugin. None of these touchpython/mujoco/egl/.The one extra skip on this branch is the new
egl_gl_context_test.py, which skips because this machine (macOS) has nolibEGL/libOpenGLformujoco.eglto import -- same reason the existingrender_test.py/renderer_test.pyGL tests are skipped here and in this repo's own CI (MUJOCO_GL=disable).I additionally verified the new test's logic outside of pytest by stubbing the
OpenGL.EGLimport chain in-process somujoco.eglbecomes importable on a machine without a real EGL driver, then running the test body directly against both the pre-fix and post-fixegl/__init__.py: it fails with the exactAttributeError: 'GLContext' object has no attribute '_context'from__del__on pre-fix code, and passes on post-fix code.