Skip to content

Unbind recorder keyboard callbacks on release - #9424

Merged
kuzeyron merged 4 commits into
kivy:masterfrom
charan-rathore:fix/recorder-release-keyboard
Oct 19, 2026
Merged

kuzeyron merged 4 commits into
kivy:masterfrom
charan-rathore:fix/recorder-release-keyboard

Conversation

@charan-rathore

@charan-rathore charan-rathore commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Recorder.__init__() binds three partial callbacks for key-up, key-down and keyboard events. release() tries to unbind the underlying methods instead, which never matches those partials, and it skips the keyboard event entirely. A released recorder keeps receiving input and stays retained by the window.

Fix

  • Keep the exact event/callback mapping used at bind time and unbind that same mapping in release(), then clear the recorder's callback references.
  • Releasing twice stays safe, motion cleanup is untouched, and a second recorder bound to the same window keeps receiving events.
  • Recording, replay, stop behavior and file closing are unchanged.

Testing

  • Four new unittest methods in kivy/tests/test_recorder.py cover the observer identities, keyboard dispatch before and after release, repeated release, a second recorder on the same window, and garbage collection.
  • On master these fail 6 times across the 4 methods: the recorder still gets key events after release(). With the fix, all 4 pass.
  • Tests ran against master sources with the released wheel's compiled dependencies; no full GL suite.

@ElliotGarbus

Copy link
Copy Markdown
Contributor

Thanks for the PR. The Recorder.release() fix is correct and the tests look good. I confirmed they fail on master (6 failures) and pass with the fix.

However, the leak is still present in kivy/modules/recorder.py which is where release() is actually called from."
start() also does:

win.bind(on_key_down=partial(on_recorder_key, ctx.recorder))

and stop() never unbinds it. I ran the module's start() / stop() against a stand-in window with this PR applied: after stop() there is still one on_key_down observer, pressing F8 sets recorder.record = True (and creates the output file), and the recorder is not garbage collected. So a stopped recorder module still records, and is still retained by the window.

Suggested change in kivy/modules/recorder.py (I checked that with this plus your change there are 0 observers after stop(), F8 does nothing, no file is created and the recorder is collected):

def start(win, ctx):
    ...
    ctx.recorder = Recorder(window=win, **keys)
    ctx.key_handler = partial(on_recorder_key, ctx.recorder)
    win.bind(on_key_down=ctx.key_handler)


def stop(win, ctx):
    if hasattr(ctx, 'recorder'):
        win.unbind(on_key_down=ctx.key_handler)
        ctx.recorder.release()

Please also add a test that calls the module's start() / stop() with a stand-in window and checks that no observers remain and that F8 no longer starts recording.

Separately: I couldn't find an issue or user report for this. If you have one, please link it in the description.

@ElliotGarbus
ElliotGarbus requested a review from kuzeyron October 18, 2026 13:23
start() kept a partial-bound on_key_down observer that stop() never
removed, so a stopped recorder module still recorded (F8) and the
recorder stayed retained by the window. Keep the handler on the context
and unbind it before releasing the recorder.

Signed-off-by: Charan Rathore <[email protected]>
Start/stop the recorder module against a stand-in window: F8 records
and creates the file while started; after stop() no on_key_down
observers remain, F8 does nothing, no file is created, and the
recorder is collectable.

Signed-off-by: Charan Rathore <[email protected]>
@charan-rathore

Copy link
Copy Markdown
Contributor Author

Thanks @ElliotGarbus - extended the fix to the module in 13e8fd8 and 038a9f3, exactly along your verified snippet:

  • kivy/modules/recorder.py: start() now keeps the handler as ctx.key_handler and stop() unbinds it before calling release().
  • kivy/tests/test_recorder.py: new ModuleStartStopTest runs the module's start()/stop() against a stand-in window. Before stop, F8 records and creates the file; after stop, zero on_key_down observers remain, F8 does nothing, no file is created, and the recorder is collectable.

On the issue link: there is no user report - I found this while auditing the recorder's release path, same class as the release() leak. Happy to file a tracking issue and link it if you'd like the paper trail.

@ElliotGarbus

Copy link
Copy Markdown
Contributor

@charan-rathore It's not about the "paper trail" - it is really about priorities. The project has over 700 open issues. That is quite a backlog. If a user has reported the issue it provides a way to let them know it is fixed.

CI is failing on every platform with the same test: ModuleStartStopTest.test_stop_unbinds_module_key_handler (AssertionError: 2 != 1). It fails locally too. The module fix itself is right; there are three problems in the test:

  1. After start() there are two on_key_down observers (the Recorder's own and the module's F8 handler), so the first assertion should expect 2.
  2. window.dispatch('on_key_down', window, 289) passes the window as an extra argument. Kivy already passes the dispatcher as the first argument, so the handler receives the window as key and F8 is never recognized. Use window.dispatch('on_key_down', 289). As written, the "F8 does nothing after stop" check would pass even without the fix.
  3. The final garbage-collection check fails because ctx.key_handler (a partial) still references the recorder. Add del ctx.key_handler next to del ctx.recorder.

With those three changes the test passes with your fix (5 passed) and fails without the module change (I checked both). Please run the new test locally before pushing, since the description says it passes.

Expect both on_key_down observers after start(), dispatch F8 with the
key argument only so the check fails without the fix, and drop the
stored key handler before the garbage-collection check.

Signed-off-by: Charan Rathore <[email protected]>
@charan-rathore

Copy link
Copy Markdown
Contributor Author

All three fixed in 4573477: expect 2 on_key_down observers after start(), dispatch F8 with the key only, and drop ctx.key_handler before the GC check. You're right on the process point too - I pushed that test without running it; this environment can't build kivy, which is an explanation, not an excuse. Thanks for running it.

@ElliotGarbus

Copy link
Copy Markdown
Contributor

@kuzeyron Ready to merge.

@kuzeyron
kuzeyron merged commit a2d7035 into kivy:master Oct 19, 2026
33 checks passed
@kivybot

kivybot commented Nov 12, 2026

Copy link
Copy Markdown

Successfully created backport PR for devel-2.3.x:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants