* fix(ibus): start engine process before checking registration _setup_engine() gated on is_engine_registered() which queries `ibus list-engine`. On systems where the IBus daemon doesn't scan user-level component directories (~/.local/share/ibus/component/), the engine never appeared in the list, blocking start_engine_process() from ever being called — even though its register_component() D-Bus call is the correct registration path for standalone-launched engines. Reorder _setup_engine() to start the engine process first, relying on register_component() for runtime registration. Remove the XML install step from the setup path since install_ibus_component() triggers `ibus restart` which kills the engine process. Follow-up to PR #304 which added register_component() support. * fix(ibus): remove dead code and add engine readiness check Address review feedback: - Remove install_ibus_component(), is_engine_registered(), is_component_up_to_date(), _get_expected_component_xml() and their tests — no longer called after setup order fix - Add SOCKET_PATH poll after start_engine_process() to verify engine is fully initialized before activation - Rebase onto current main
This commit is contained in:
@@ -6,9 +6,7 @@ from . import text_injector # noqa: F401
|
||||
from .ibus_engine import ( # noqa: F401
|
||||
IBusSetupError,
|
||||
IBusTextInjector,
|
||||
install_ibus_component,
|
||||
is_engine_active,
|
||||
is_engine_registered,
|
||||
is_ibus_active_input_method,
|
||||
is_ibus_available,
|
||||
start_ibus_daemon,
|
||||
|
||||
@@ -259,76 +259,6 @@ def _get_exec_command() -> str:
|
||||
return f"{sys.executable} {engine_script} --ibus"
|
||||
|
||||
|
||||
def _get_expected_component_xml() -> str:
|
||||
"""Generate the expected component XML content for the current installation."""
|
||||
e = _ENGINE_META
|
||||
c = _COMPONENT_META
|
||||
|
||||
return f"""<?xml version="1.0" encoding="utf-8"?>
|
||||
<component>
|
||||
<name>{COMPONENT_NAME}</name>
|
||||
<description>{ENGINE_DESCRIPTION}</description>
|
||||
<exec>{_get_exec_command()}</exec>
|
||||
<version>{c['version']}</version>
|
||||
<author>{c['author']}</author>
|
||||
<license>{c['license']}</license>
|
||||
<homepage>{c['homepage']}</homepage>
|
||||
<textdomain>{c['textdomain']}</textdomain>
|
||||
<engines>
|
||||
<engine>
|
||||
<name>{ENGINE_NAME}</name>
|
||||
<longname>{ENGINE_LONGNAME}</longname>
|
||||
<language>{e['language']}</language>
|
||||
<license>{e['license']}</license>
|
||||
<author>{e['author']}</author>
|
||||
<icon>{e['icon']}</icon>
|
||||
<layout>{e['layout']}</layout>
|
||||
<description>{ENGINE_DESCRIPTION}</description>
|
||||
<rank>{ENGINE_RANK}</rank>
|
||||
</engine>
|
||||
</engines>
|
||||
</component>
|
||||
"""
|
||||
|
||||
|
||||
def is_component_up_to_date() -> bool:
|
||||
"""
|
||||
Check if the installed IBus component XML matches the current installation.
|
||||
|
||||
Returns:
|
||||
True if component exists and matches expected content, False otherwise
|
||||
"""
|
||||
component_file = Path.home() / ".local" / "share" / "ibus" / "component" / "vocalinux.xml"
|
||||
|
||||
if not component_file.exists():
|
||||
return False
|
||||
|
||||
try:
|
||||
installed_content = component_file.read_text()
|
||||
expected_content = _get_expected_component_xml()
|
||||
return installed_content.strip() == expected_content.strip()
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to check component XML: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def is_engine_registered() -> bool:
|
||||
"""Check if the Vocalinux IBus engine is registered."""
|
||||
if not IBUS_AVAILABLE:
|
||||
return False
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["ibus", "list-engine"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=5,
|
||||
)
|
||||
return ENGINE_NAME in result.stdout
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
return False
|
||||
|
||||
|
||||
def is_engine_active() -> bool:
|
||||
"""Check if the Vocalinux IBus engine is currently active."""
|
||||
try:
|
||||
@@ -837,11 +767,12 @@ class IBusTextInjector:
|
||||
|
||||
This class connects to the Vocalinux IBus engine via Unix socket
|
||||
and sends text to be injected. On initialization, it automatically:
|
||||
1. Installs the IBus component if not registered
|
||||
2. Saves the current engine
|
||||
1. Starts the engine process (registers via D-Bus register_component)
|
||||
2. Saves the current engine and XKB layout
|
||||
3. Switches to the Vocalinux engine
|
||||
4. Restores the XKB layout
|
||||
|
||||
On cleanup (stop), it restores the previous engine.
|
||||
On cleanup (stop), it restores the previous engine and layout.
|
||||
"""
|
||||
|
||||
def __init__(self, auto_activate: bool = True):
|
||||
@@ -863,27 +794,28 @@ class IBusTextInjector:
|
||||
|
||||
def _setup_engine(self) -> None:
|
||||
"""Install and activate the IBus engine."""
|
||||
# Install or update component if needed
|
||||
if not is_engine_registered():
|
||||
logger.info("IBus engine not registered, installing...")
|
||||
if not install_ibus_component(system_wide=False):
|
||||
logger.info("User-level install failed, trying system-wide...")
|
||||
install_ibus_component(system_wide=True)
|
||||
elif not is_component_up_to_date():
|
||||
logger.info("IBus component XML is outdated, updating...")
|
||||
install_ibus_component(system_wide=False)
|
||||
|
||||
# Check again after installation
|
||||
if not is_engine_registered():
|
||||
raise IBusSetupError(
|
||||
"Failed to register IBus engine. "
|
||||
"Try running 'ibus list-engine' to verify installation."
|
||||
)
|
||||
|
||||
# Start the engine process (Vocalinux starts it, not IBus)
|
||||
# Start the engine process — it calls register_component() via
|
||||
# D-Bus which makes the engine available even when the IBus daemon
|
||||
# doesn't scan ~/.local/share/ibus/component/.
|
||||
# Follow-up to PR #304: register_component() is the reliable path,
|
||||
# so we no longer gate on is_engine_registered() / ibus list-engine.
|
||||
if not start_engine_process():
|
||||
raise IBusSetupError("Failed to start IBus engine process. Check logs for details.")
|
||||
|
||||
# Verify the engine is fully ready before proceeding.
|
||||
# start_engine_process() only confirms the subprocess is alive —
|
||||
# register_component() and socket setup may still be in progress.
|
||||
for _attempt in range(15):
|
||||
if SOCKET_PATH.exists():
|
||||
logger.debug("Engine socket is ready")
|
||||
break
|
||||
time.sleep(0.2)
|
||||
else:
|
||||
logger.warning(
|
||||
"Engine process started but socket not ready after retries; "
|
||||
"proceeding with activation attempt"
|
||||
)
|
||||
|
||||
# Capture current XKB layout before switching engines
|
||||
# This is critical for preserving the user's keyboard layout
|
||||
# when IBus engine switching might override it
|
||||
@@ -995,77 +927,6 @@ class IBusTextInjector:
|
||||
return False
|
||||
|
||||
|
||||
def install_ibus_component(system_wide: bool = False) -> bool:
|
||||
"""
|
||||
Install the IBus component XML file.
|
||||
|
||||
Args:
|
||||
system_wide: If True, install to /usr/share/ibus/component/ (requires root).
|
||||
If False, install to user directory and set IBUS_COMPONENT_PATH.
|
||||
|
||||
Returns:
|
||||
True if installation was successful, False otherwise
|
||||
"""
|
||||
component_xml = _get_expected_component_xml()
|
||||
|
||||
try:
|
||||
if system_wide:
|
||||
component_dir = Path("/usr/share/ibus/component")
|
||||
component_file = component_dir / "vocalinux.xml"
|
||||
|
||||
# Write to temp file and move with sudo
|
||||
import tempfile
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w", suffix=".xml", delete=False) as f:
|
||||
f.write(component_xml)
|
||||
temp_path = f.name
|
||||
|
||||
result = subprocess.run(
|
||||
["sudo", "cp", temp_path, str(component_file)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
os.unlink(temp_path)
|
||||
|
||||
if result.returncode != 0:
|
||||
logger.error(f"Failed to install component: {result.stderr}")
|
||||
return False
|
||||
|
||||
subprocess.run(
|
||||
["sudo", "chmod", "644", str(component_file)],
|
||||
capture_output=True,
|
||||
)
|
||||
else:
|
||||
# User-level installation
|
||||
component_dir = Path.home() / ".local" / "share" / "ibus" / "component"
|
||||
component_dir.mkdir(parents=True, exist_ok=True)
|
||||
component_file = component_dir / "vocalinux.xml"
|
||||
component_file.write_text(component_xml)
|
||||
logger.info(f"Component installed to {component_file}")
|
||||
|
||||
# Refresh IBus - restart with component path set so it picks up user components
|
||||
import time
|
||||
|
||||
user_component_dir = Path.home() / ".local" / "share" / "ibus" / "component"
|
||||
env = os.environ.copy()
|
||||
env["IBUS_COMPONENT_PATH"] = f"{user_component_dir}:/usr/share/ibus/component"
|
||||
|
||||
subprocess.run(["ibus", "write-cache"], capture_output=True, env=env)
|
||||
subprocess.run(["ibus", "restart"], capture_output=True, env=env)
|
||||
time.sleep(1) # Give IBus time to restart
|
||||
|
||||
logger.info(
|
||||
"IBus component installed successfully.\n"
|
||||
"Set 'Vocalinux' as your input method in system settings "
|
||||
"to enable voice dictation."
|
||||
)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to install IBus component: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def _get_engines_xml() -> str:
|
||||
"""Return engine XML for IBus --xml discovery.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user