* fix(ibus): restore engine after register_component teardown Stopping the Vocalinux IBus engine process destroys its register_component registration. IBus then runs check_global_engine(), which only searches register_engine_list and treats XML engines like xkb:es::spa as missing, clearing GlobalEngine even when Vocalinux was never selected. Capture a restorable engine before teardown and reselect it afterward so dead keys keep working after quit. Fixes #558 Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * fix(ibus): wait for engine exit before restoring GlobalEngine Adversarial review of #643 found that stop_engine_process() returned right after SIGTERM, so switch_engine() could race ahead of IBus check_global_engine() and still leave No global engine. Wait for the process to exit (SIGKILL fallback), retry engine restore, capture a shutdown fallback during prepare_engine, and re-apply XKB after the post-teardown engine switch. Addresses review of #558 / #643. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> * fix(ibus): prefer live engine over stale warmup on quit Bugbot on #643: stop() restored the warmup-cached engine even when the user had switched input source afterward, and inject_text refused to refresh that cache once set. Prefer the live non-Vocalinux engine at quit; fall back to the cache only when stuck on vocalinux/unavailable. Always refresh the shutdown fallback from inject_text. Also cover SIGKILL teardown escalation and restore-retry failure paths to lift patch coverage. Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Jatin K Malik <jatinkrmalik@users.noreply.github.com>
This commit is contained in:
co-authored by
Jatin K Malik
Cursor Agent
parent
45acb10d13
commit
7a5ddfde38
@@ -839,8 +839,13 @@ def start_engine_process() -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def stop_engine_process() -> None:
|
||||
"""Stop the IBus engine process if running."""
|
||||
def stop_engine_process(wait_timeout: float = 2.0) -> None:
|
||||
"""Stop the IBus engine process if running.
|
||||
|
||||
Waits for the process to actually exit so callers that restore the user's
|
||||
IBus engine afterward run after ``register_component`` teardown has been
|
||||
observed by ibus-daemon (issue #558).
|
||||
"""
|
||||
try:
|
||||
if not PID_FILE.exists():
|
||||
logger.debug("No PID file found, engine not running")
|
||||
@@ -857,6 +862,29 @@ def stop_engine_process() -> None:
|
||||
return
|
||||
|
||||
os.kill(pid, signal.SIGTERM)
|
||||
# Wait for exit: IBus clears/repairs GlobalEngine on component destroy,
|
||||
# which only runs after the process actually disconnects (#558).
|
||||
deadline = time.monotonic() + max(wait_timeout, 0.0)
|
||||
while time.monotonic() < deadline:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
# Still alive — escalate so teardown (and GlobalEngine repair) finishes.
|
||||
try:
|
||||
os.kill(pid, signal.SIGKILL)
|
||||
except OSError:
|
||||
pass
|
||||
kill_deadline = time.monotonic() + 0.5
|
||||
while time.monotonic() < kill_deadline:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
except OSError:
|
||||
break
|
||||
time.sleep(0.05)
|
||||
|
||||
logger.info(f"IBus engine process (PID {pid}) stopped")
|
||||
PID_FILE.unlink()
|
||||
except (OSError, ValueError, FileNotFoundError) as e:
|
||||
@@ -1206,7 +1234,18 @@ class IBusTextInjector:
|
||||
The Vocalinux engine is only needed while committing dictated text. Keeping
|
||||
the user's normal IBus/XKB engine active preserves dead-key composition and
|
||||
layout-specific behavior during ordinary typing.
|
||||
|
||||
Captures the current engine for shutdown restore only. Injection still
|
||||
resolves the restore target at commit time so a stale warmup value is not
|
||||
used mid-session (see #623); shutdown needs a fallback when quit leaves
|
||||
Vocalinux selected after a failed inject restore (#558).
|
||||
"""
|
||||
if not is_engine_active():
|
||||
captured = get_current_engine()
|
||||
if captured and captured != ENGINE_NAME:
|
||||
self._previous_engine = captured
|
||||
logger.debug(f"Captured current engine for shutdown restore: {captured}")
|
||||
|
||||
if not start_engine_process():
|
||||
raise IBusSetupError("Failed to start IBus engine process. Check logs for details.")
|
||||
|
||||
@@ -1303,18 +1342,49 @@ class IBusTextInjector:
|
||||
captured before injection (e.g. on GNOME/Wayland with no global IBus
|
||||
engine), restoration is skipped to avoid switching the user to a
|
||||
wrong layout (see issue #497).
|
||||
|
||||
Engine restoration must run *after* ``stop_engine_process()`` has
|
||||
observed process exit. Killing the process that called
|
||||
``register_component`` makes IBus run ``check_global_engine()``, which
|
||||
only searches ``register_engine_list`` and treats XML engines such as
|
||||
``xkb:es::spa`` as missing, clearing the global engine even when
|
||||
Vocalinux was never selected (issue #558).
|
||||
|
||||
Prefer the *live* engine at quit when it is a real non-Vocalinux
|
||||
engine, so a user who changed input source after warmup is not
|
||||
switched back to a stale cache. Fall back to ``_previous_engine`` only
|
||||
when the live engine is Vocalinux or unavailable (failed inject restore).
|
||||
"""
|
||||
if self._previous_engine:
|
||||
logger.info(f"Restoring previous engine: {self._previous_engine}")
|
||||
switch_engine(self._previous_engine)
|
||||
self._previous_engine = None
|
||||
current = get_current_engine()
|
||||
if current and current != ENGINE_NAME:
|
||||
restore_engine = current
|
||||
else:
|
||||
restore_engine = self._previous_engine
|
||||
if restore_engine == ENGINE_NAME:
|
||||
restore_engine = None
|
||||
|
||||
# Stop the engine process first and wait for exit — teardown can clear
|
||||
# IBus GlobalEngine (#558). Restore must land after that check.
|
||||
stop_engine_process()
|
||||
|
||||
if restore_engine:
|
||||
logger.info(f"Restoring previous engine after teardown: {restore_engine}")
|
||||
restored = False
|
||||
for attempt in range(3):
|
||||
if switch_engine(restore_engine):
|
||||
restored = True
|
||||
break
|
||||
time.sleep(0.15 * (attempt + 1))
|
||||
if not restored:
|
||||
logger.error(f"Failed to restore IBus engine after teardown: {restore_engine}")
|
||||
else:
|
||||
logger.info(
|
||||
"Skipping engine restoration — " "no valid engine was captured before injection"
|
||||
"Skipping engine restoration — no restorable engine was available at shutdown"
|
||||
)
|
||||
self._previous_engine = None
|
||||
|
||||
# Restore the XKB layout that was captured during setup
|
||||
# This ensures the user's original keyboard layout is preserved
|
||||
# XKB last: switching engines can override layout (#292). Re-apply after
|
||||
# the post-teardown engine restore so we don't lose the user's map.
|
||||
if self._previous_xkb_layout:
|
||||
layout, variant, option = self._previous_xkb_layout
|
||||
if layout:
|
||||
@@ -1322,9 +1392,6 @@ class IBusTextInjector:
|
||||
restore_xkb_layout(layout, variant, option)
|
||||
self._previous_xkb_layout = None
|
||||
|
||||
# Stop the engine process
|
||||
stop_engine_process()
|
||||
|
||||
def inject_text(self, text: str) -> bool:
|
||||
"""
|
||||
Inject text using the IBus engine.
|
||||
@@ -1352,6 +1419,9 @@ class IBusTextInjector:
|
||||
return False
|
||||
|
||||
restore_engine = current_engine
|
||||
# Always refresh the shutdown fallback to the live engine so a later
|
||||
# quit does not restore a stale warmup value (Bugbot on #643 / #558).
|
||||
self._previous_engine = current_engine
|
||||
logger.debug(
|
||||
f"Temporarily activating Vocalinux IBus engine (will restore {current_engine})"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user