Skip to content

Fix race in nvdaController_speakSsml where speechCanceled could be missed#20220

Merged
seanbudd merged 1 commit into
nvaccess:betafrom
LeonarddeR:fixSsml
May 27, 2026
Merged

Fix race in nvdaController_speakSsml where speechCanceled could be missed#20220
seanbudd merged 1 commit into
nvaccess:betafrom
LeonarddeR:fixSsml

Conversation

@LeonarddeR

@LeonarddeR LeonarddeR commented May 26, 2026

Copy link
Copy Markdown
Collaborator

Link to issue number:

None

Summary of the issue:

When nvdaController_speakSsml is called with asynchronous=False, a race condition can cause the call to block indefinitely. onSpeechCanceled was registered inside prefixCallback, a CallbackCommand that runs on the synth thread only after the sequence starts. If speechCanceled fires in the window between queueHandler.queueFunction returning and the synth reaching that CallbackCommand, the signal is missed and markQueue.get() never unblocks.

Description of user facing changes:

No change under normal operation. Fixes a hang that could occur when speech is cancelled immediately after being queued via the controller client.

Description of developer facing changes:

speech.speechCanceled.register(onSpeechCanceled) is now called directly before queueHandler.queueFunction, ensuring the handler exists before any cancel signal can fire. onDoneSpeaking stays in prefixCallback because synthDoneSpeaking fires per sequence — registering it earlier would catch a previous sequence still finishing when priority is Next.

Description of development approach:

Minimal repositioning of one register call. No logic changes.

Testing strategy:

Call nvdaController_speakSsml with asynchronous=False and cancel speech immediately after queuing, repeatedly. Before this fix the call would occasionally hang; after it should always return promptly with ERROR_CANCELLED.

Known issues with pull request:

None.

Code Review Checklist:

  • Documentation:
    • Change log entry
    • User Documentation
    • Developer / Technical Documentation
    • Context sensitive help for GUI changes
  • Testing:
    • Unit tests
    • System (end to end) tests
    • Manual testing
  • UX of all users considered:
    • Speech
    • Braille
    • Low Vision
    • Different web browsers
    • Localization in other languages / culture than English
  • API is compatible with existing add-ons.
  • Security precautions taken.

Copilot AI review requested due to automatic review settings May 26, 2026 20:04
@LeonarddeR LeonarddeR requested a review from a team as a code owner May 26, 2026 20:04
@LeonarddeR LeonarddeR requested a review from seanbudd May 26, 2026 20:04
…ssed

When asynchronous=False, onSpeechCanceled was registered inside prefixCallback,
a CallbackCommand that runs on the synth thread after the sequence starts.
A cancel firing between queueFunction returning and the synth reaching that
CallbackCommand would not be caught, leaving markQueue.get() blocked indefinitely.

Move speech.speechCanceled.register(onSpeechCanceled) to just before
queueFunction so the handler exists before any cancel can fire.
onDoneSpeaking stays in prefixCallback because synthDoneSpeaking fires per
sequence; registering it earlier would catch a previous sequence still finishing
when priority is Next.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Updates NVDAHelper’s SSML speak path to avoid a deadlock when nvdaController_speakSsml(..., asynchronous=False) is canceled during a narrow timing window, and documents the fix in the user-facing changelog.

Changes:

  • Register speech.speechCanceled earlier for synchronous SSML speech to eliminate a race that could cause indefinite blocking.
  • Update release notes to mention the fixed race condition/deadlock in nvdaController_speakSsml.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
user_docs/en/changes.md Adds a changelog entry describing the synchronous SSML cancellation race fix.
source/NVDAHelper/init.py Moves speechCanceled handler registration earlier when asynchronous is False to prevent a deadlock.

Comment thread user_docs/en/changes.md
Comment thread source/NVDAHelper/__init__.py

@seanbudd seanbudd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @LeonarddeR

@seanbudd seanbudd merged commit 3207d48 into nvaccess:beta May 27, 2026
35 of 39 checks passed
@github-actions github-actions Bot added this to the 2026.3 milestone May 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants