Attempt to fix bug where speech could not be interrupted during some race conditions.

This commit is contained in:
Storm Dragon
2026-07-06 12:15:45 -04:00
parent dfef847fb5
commit 3926ed0044
10 changed files with 534 additions and 146 deletions
+37 -15
View File
@@ -196,6 +196,8 @@ class Parser(argparse.ArgumentParser):
help=messages.CLI_DEBUG_FILE, metavar=messages.CLI_DEBUG_FILE_NAME)
self.add_argument(
"--debug", action="store_true", help=messages.CLI_ENABLE_DEBUG)
self.add_argument(
"--debug-startup", action="store_true", help=messages.CLI_ENABLE_STARTUP_DEBUG)
self._optionals.title = messages.CLI_OPTIONAL_ARGUMENTS
@@ -323,6 +325,8 @@ def cleanup(sigval):
time.sleep(0.5)
def main():
debug.reset_startup_timing()
setProcessName('cthulhu')
parser = Parser()
@@ -333,16 +337,26 @@ def main():
debug.eventDebugLevel = debug.LEVEL_OFF
debug.debugFile = open(args.debug_file, 'w')
debug.set_startup_timing_enabled(args.debug or args.debug_startup)
debug.print_startup_timing("launcher main start")
if args.replace:
cleanup(signal.SIGKILL)
with debug.startup_timing("replace cleanup"):
cleanup(signal.SIGKILL)
settingsDict = getattr(args, 'settings', {})
if not inGraphicalDesktop():
with debug.startup_timing("graphical desktop check"):
graphicalDesktop = inGraphicalDesktop()
if not graphicalDesktop:
print(messages.CLI_NO_DESKTOP_ERROR)
return 1
sessionType = getSessionType()
with debug.startup_timing("session check"):
sessionType = getSessionType()
xServerVendor = None
if sessionType == "x11":
xServerVendor = getXServerVendor()
sessionDetails = []
xdgSessionType = os.environ.get("XDG_SESSION_TYPE")
if xdgSessionType:
@@ -355,9 +369,8 @@ def main():
display = os.environ.get("DISPLAY")
if display:
sessionDetails.append(f"DISPLAY={display}")
vendor = getXServerVendor()
if vendor:
sessionDetails.append(f"X server vendor={vendor}")
if xServerVendor:
sessionDetails.append(f"X server vendor={xServerVendor}")
if sessionDetails:
msg = f"INFO: Session: {sessionType} ({', '.join(sessionDetails)})"
@@ -367,7 +380,9 @@ def main():
debug.printMessage(debug.LEVEL_INFO, "INFO: Preparing to launch.", True)
debug.print_startup_timing("cthulhu import start")
from cthulhu import cthulhu
debug.print_startup_timing("cthulhu import complete")
manager = cthulhu.cthulhuApp.settingsManager
if not manager:
@@ -375,25 +390,32 @@ def main():
return 1
debug.printMessage(debug.LEVEL_INFO, "INFO: About to activate settings manager.", True)
manager.activate(args.user_prefs, settingsDict)
with debug.startup_timing("settings manager launcher activation"):
manager.activate(args.user_prefs, settingsDict)
sys.path.insert(0, manager.getPrefsDir())
if args.profile:
try:
manager.setProfile(args.profile)
except Exception:
print(messages.CLI_LOAD_PROFILE_ERROR % args.profile)
manager.setProfile()
with debug.startup_timing("profile selection"):
try:
manager.setProfile(args.profile)
except Exception:
print(messages.CLI_LOAD_PROFILE_ERROR % args.profile)
manager.setProfile()
if args.setup:
cleanup(signal.SIGKILL)
cthulhu.showPreferencesGUI()
with debug.startup_timing("setup cleanup"):
cleanup(signal.SIGKILL)
with debug.startup_timing("preferences GUI startup"):
cthulhu.showPreferencesGUI()
if otherCthulhus():
with debug.startup_timing("other instance check"):
otherInstances = otherCthulhus()
if otherInstances:
print(messages.CLI_OTHER_CTHULHUS_ERROR)
return 1
debug.printMessage(debug.LEVEL_INFO, "INFO: About to launch Cthulhu.", True)
debug.print_startup_timing("cthulhu main handoff")
return cthulhu.main()
if __name__ == "__main__":
+153 -105
View File
@@ -402,53 +402,59 @@ def loadUserSettings(script: Optional[Any] = None, inputEvent: Optional[Any] = N
"""
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Loading User Settings', True)
debug.print_startup_timing("load user settings start")
global _userSettings
# Shutdown the output drivers and give them a chance to die.
player = sound.getPlayer()
player.shutdown()
speech.shutdown()
braille.shutdown()
with debug.startup_timing("output driver shutdown"):
player = sound.getPlayer()
player.shutdown()
speech.shutdown()
braille.shutdown()
cthulhuApp.scriptManager.deactivate()
cthulhuApp.getSignalManager().emitSignal('load-setting-begin')
with debug.startup_timing("script manager deactivate for settings load"):
cthulhuApp.scriptManager.deactivate()
cthulhuApp.getSignalManager().emitSignal('load-setting-begin')
reloaded: bool = False
if _userSettings:
_profile = cthulhuApp.settingsManager.getSetting('activeProfile')[1]
try:
_userSettings = cthulhuApp.settingsManager.getGeneralSettings(_profile)
cthulhuApp.settingsManager.setProfile(_profile)
reloaded = True
except ImportError:
debug.printException(debug.LEVEL_INFO)
except Exception:
debug.printException(debug.LEVEL_SEVERE)
else:
_profile = cthulhuApp.settingsManager.profile
try:
_userSettings = cthulhuApp.settingsManager.getGeneralSettings(_profile)
except ImportError:
debug.printException(debug.LEVEL_INFO)
except Exception:
debug.printException(debug.LEVEL_SEVERE)
with debug.startup_timing("settings profile load"):
if _userSettings:
_profile = cthulhuApp.settingsManager.getSetting('activeProfile')[1]
try:
_userSettings = cthulhuApp.settingsManager.getGeneralSettings(_profile)
cthulhuApp.settingsManager.setProfile(_profile)
reloaded = True
except ImportError:
debug.printException(debug.LEVEL_INFO)
except Exception:
debug.printException(debug.LEVEL_SEVERE)
else:
_profile = cthulhuApp.settingsManager.profile
try:
_userSettings = cthulhuApp.settingsManager.getGeneralSettings(_profile)
except ImportError:
debug.printException(debug.LEVEL_INFO)
except Exception:
debug.printException(debug.LEVEL_SEVERE)
if not script:
script = cthulhuApp.scriptManager.get_default_script()
cthulhuApp.settingsManager.loadAppSettings(script)
with debug.startup_timing("app settings load"):
cthulhuApp.settingsManager.loadAppSettings(script)
if cthulhuApp.settingsManager.getSetting('enableSpeech'):
msg = 'CTHULHU: About to enable speech'
debug.printMessage(debug.LEVEL_INFO, msg, True)
try:
speech.init()
if reloaded and not skipReloadMessage:
script.speakMessage(messages.SETTINGS_RELOADED)
except Exception:
debug.printException(debug.LEVEL_SEVERE)
with debug.startup_timing("speech initialization"):
try:
speech.init()
if reloaded and not skipReloadMessage:
script.speakMessage(messages.SETTINGS_RELOADED)
except Exception:
debug.printException(debug.LEVEL_SEVERE)
else:
msg = 'CTHULHU: Speech is not enabled in settings'
debug.printMessage(debug.LEVEL_INFO, msg, True)
@@ -456,31 +462,36 @@ def loadUserSettings(script: Optional[Any] = None, inputEvent: Optional[Any] = N
if cthulhuApp.settingsManager.getSetting('enableBraille'):
msg = 'CTHULHU: About to enable braille'
debug.printMessage(debug.LEVEL_INFO, msg, True)
try:
braille.init(_processBrailleEvent)
except Exception:
debug.printException(debug.LEVEL_WARNING)
msg = 'CTHULHU: Could not initialize connection to braille.'
debug.printMessage(debug.LEVEL_WARNING, msg, True)
with debug.startup_timing("braille initialization"):
try:
braille.init(_processBrailleEvent)
except Exception:
debug.printException(debug.LEVEL_WARNING)
msg = 'CTHULHU: Could not initialize connection to braille.'
debug.printMessage(debug.LEVEL_WARNING, msg, True)
else:
msg = 'CTHULHU: Braille is not enabled in settings'
debug.printMessage(debug.LEVEL_INFO, msg, True)
if cthulhuApp.settingsManager.getSetting('enableMouseReview'):
mouse_review.getReviewer().activate()
else:
mouse_review.getReviewer().deactivate()
with debug.startup_timing("mouse review update"):
if cthulhuApp.settingsManager.getSetting('enableMouseReview'):
mouse_review.getReviewer().activate()
else:
mouse_review.getReviewer().deactivate()
if cthulhuApp.settingsManager.getSetting('enableSound'):
player.init()
with debug.startup_timing("sound initialization"):
player.init()
cthulhu_modifier_manager.getManager().refreshCthulhuModifiers("Loading user settings.")
with debug.startup_timing("cthulhu modifier refresh"):
cthulhu_modifier_manager.getManager().refreshCthulhuModifiers("Loading user settings.")
# Activate core systems FIRST before loading plugins
cthulhuApp.compositorStateAdapter.activate()
cthulhuApp.scriptManager.activate()
cthulhuApp.eventManager.activate()
with debug.startup_timing("core manager activation"):
cthulhuApp.compositorStateAdapter.activate()
cthulhuApp.scriptManager.activate()
cthulhuApp.eventManager.activate()
cthulhuApp.getSignalManager().emitSignal('load-setting-begin')
@@ -498,14 +509,17 @@ def loadUserSettings(script: Optional[Any] = None, inputEvent: Optional[Any] = N
)
activePlugins = list(activePluginsSetting)
debug.printMessage(debug.LEVEL_INFO, f'CTHULHU: Loading active plugins: {activePlugins}', True)
cthulhuApp.getPluginSystemManager().setActivePlugins(activePlugins)
with debug.startup_timing("active plugin load"):
cthulhuApp.getPluginSystemManager().setActivePlugins(activePlugins)
# Refresh keybindings to include plugin bindings (after script manager is active)
cthulhuApp.getPluginSystemManager().refresh_active_script_keybindings()
with debug.startup_timing("plugin keybinding refresh"):
cthulhuApp.getPluginSystemManager().refresh_active_script_keybindings()
cthulhuApp.getSignalManager().emitSignal('load-setting-completed')
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: User Settings Loaded', True)
debug.print_startup_timing("load user settings complete")
return True
@@ -612,6 +626,7 @@ def init() -> bool:
"""
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Initializing', True)
debug.print_startup_timing("cthulhu init start")
global _initialized
@@ -627,15 +642,18 @@ def init() -> bool:
# Activate settings manager before loading user settings
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Activating settings manager', True)
cthulhuApp.settingsManager.activate()
with debug.startup_timing("settings manager init activation"):
cthulhuApp.settingsManager.activate()
loadUserSettings()
with debug.startup_timing("load user settings"):
loadUserSettings()
if settings.timeoutCallback and (settings.timeoutTime > 0):
signal.alarm(0)
_initialized = True
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Initialized', True)
debug.print_startup_timing("cthulhu init complete")
return True
@@ -672,6 +690,7 @@ def start() -> None:
"""Starts Cthulhu."""
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting', True)
debug.print_startup_timing("cthulhu start begin")
if not _initialized:
init()
@@ -689,21 +708,25 @@ def start() -> None:
# Used to re-create the Xmodmap when a new keyboard is plugged in.
# Necessary, because plugging in a new keyboard resets the Xmodmap
# and stomps our changes
display = Gdk.Display.get_default()
devmanager=display.get_device_manager()
devmanager.connect("device-added", deviceChangeHandler)
devmanager.connect("device-removed", deviceChangeHandler)
with debug.startup_timing("input device monitor setup"):
display = Gdk.Display.get_default()
devmanager = display.get_device_manager()
devmanager.connect("device-added", deviceChangeHandler)
devmanager.connect("device-removed", deviceChangeHandler)
Gdk.notify_startup_complete()
with debug.startup_timing("desktop startup notification"):
Gdk.notify_startup_complete()
msg = 'CTHULHU: Startup complete notification made'
debug.printMessage(debug.LEVEL_INFO, msg, True)
debug.printMessage(debug.LEVEL_INFO, 'CTHULHU: Starting Atspi main event loop', True)
# Start D-Bus remote controller service after ATSPI is ready
cthulhuApp.getMakoNotificationMonitor().start()
GObject.idle_add(_start_dbus_service)
with debug.startup_timing("startup idle services"):
cthulhuApp.getMakoNotificationMonitor().start()
GObject.idle_add(_start_dbus_service)
debug.print_startup_timing("AT-SPI main loop start")
Atspi.event_main()
def die(exitCode: int = 1) -> None:
@@ -851,11 +874,13 @@ def main() -> int:
if session:
msg += f" session: {session}"
debug.printMessage(debug.LEVEL_INFO, msg, True)
debug.print_startup_timing("cthulhu main start")
if debug.debugFile and os.path.exists(debug.debugFile.name):
faulthandler.enable(file=debug.debugFile, all_threads=True)
else:
faulthandler.enable(all_threads=False)
with debug.startup_timing("faulthandler setup"):
if debug.debugFile and os.path.exists(debug.debugFile.name):
faulthandler.enable(file=debug.debugFile, all_threads=True)
else:
faulthandler.enable(all_threads=False)
# Method to call when we think something might be hung.
#
@@ -863,53 +888,59 @@ def main() -> int:
# Various signal handlers we want to listen for.
#
signal.signal(signal.SIGHUP, shutdownOnSignal)
signal.signal(signal.SIGINT, shutdownOnSignal)
signal.signal(signal.SIGTERM, shutdownOnSignal)
signal.signal(signal.SIGQUIT, shutdownOnSignal)
signal.signal(signal.SIGSEGV, crashOnSignal)
with debug.startup_timing("signal handler setup"):
signal.signal(signal.SIGHUP, shutdownOnSignal)
signal.signal(signal.SIGINT, shutdownOnSignal)
signal.signal(signal.SIGTERM, shutdownOnSignal)
signal.signal(signal.SIGQUIT, shutdownOnSignal)
signal.signal(signal.SIGSEGV, crashOnSignal)
debug.printMessage(debug.LEVEL_INFO, "CTHULHU: Enabling accessibility (if needed).", True)
if not cthulhuApp.settingsManager.isAccessibilityEnabled():
cthulhuApp.settingsManager.setAccessibility(True)
with debug.startup_timing("accessibility enable check"):
if not cthulhuApp.settingsManager.isAccessibilityEnabled():
cthulhuApp.settingsManager.setAccessibility(True)
debug.printMessage(debug.LEVEL_INFO, "CTHULHU: Initializing.", True)
init()
with debug.startup_timing("cthulhu init"):
init()
debug.printMessage(debug.LEVEL_INFO, "CTHULHU: Initialized.", True)
script = cthulhu_state.activeScript
sound_theme_manager.getManager().playStartSound(wait=False)
soundFailureReason = sound.getSoundSystemFailureReason()
with debug.startup_timing("startup sound"):
sound_theme_manager.getManager().playStartSound(wait=False)
soundFailureReason = sound.getSoundSystemFailureReason()
if soundFailureReason:
debug.printMessage(
debug.LEVEL_INFO,
f"CTHULHU: Startup sound failed. Continuing without sound. Reason: {soundFailureReason}",
True
)
cthulhuApp.getSignalManager().emitSignal('start-application-completed')
if script:
window = script.utilities.activeWindow()
with debug.startup_timing("startup focus recovery"):
cthulhuApp.getSignalManager().emitSignal('start-application-completed')
if script:
window = script.utilities.activeWindow()
if window and not cthulhu_state.locusOfFocus:
app = AXObject.get_application(window)
setActiveWindow(window, app, alsoSetLocusOfFocus=True, notifyScript=True)
if window and not cthulhu_state.locusOfFocus:
app = AXObject.get_application(window)
setActiveWindow(window, app, alsoSetLocusOfFocus=True, notifyScript=True)
# setActiveWindow does some corrective work needed thanks to
# mutter-x11-frames. So retrieve the window just in case.
window = cthulhu_state.activeWindow
cthulhuApp.scriptManager.activate_script_for_context(app, window, "startup: launch")
# setActiveWindow does some corrective work needed thanks to
# mutter-x11-frames. So retrieve the window just in case.
window = cthulhu_state.activeWindow
cthulhuApp.scriptManager.activate_script_for_context(app, window, "startup: launch")
focusedObject = AXUtilities.get_focused_object(window)
tokens = ["CTHULHU: Focused object is:", focusedObject]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if focusedObject:
setLocusOfFocus(None, focusedObject)
cthulhuApp.scriptManager.activate_script_for_context(
AXObject.get_application(focusedObject), focusedObject, "startup: focused-object")
focusedObject = AXUtilities.get_focused_object(window)
tokens = ["CTHULHU: Focused object is:", focusedObject]
debug.printTokens(debug.LEVEL_INFO, tokens, True)
if focusedObject:
setLocusOfFocus(None, focusedObject)
cthulhuApp.scriptManager.activate_script_for_context(
AXObject.get_application(focusedObject), focusedObject, "startup: focused-object")
try:
msg = "CTHULHU: Starting ATSPI registry."
debug.printMessage(debug.LEVEL_INFO, msg, True)
debug.print_startup_timing("AT-SPI registry start")
start() # waits until we stop the registry
except Exception:
msg = "CTHULHU: Exception starting ATSPI registry."
@@ -930,27 +961,44 @@ class Cthulhu(GObject.Object):
"active-script-changed": (GObject.SignalFlags.RUN_LAST, None, (GObject.TYPE_PYOBJECT,)), # New signal to indicate active script change
}
def __init__(self) -> None:
debug.print_startup_timing("cthulhuApp construction start")
GObject.Object.__init__(self)
# add members
self.resourceManager: resource_manager.ResourceManager = resource_manager.ResourceManager(self)
self.settingsManager: SettingsManager = settings_manager.SettingsManager(self) # Directly instantiate
self.eventManager: EventManager = event_manager.EventManager(self) # Directly instantiate
self.compositorStateAdapter: CompositorStateAdapter = compositor_state_adapter.CompositorStateAdapter()
self.eventManager.set_compositor_state_adapter(self.compositorStateAdapter)
self.scriptManager: ScriptManager = script_manager.ScriptManager(self) # Directly instantiate
script_manager._manager = self.scriptManager
self.logger: logger.Logger = logger.Logger() # Directly instantiate
self.signalManager: SignalManager = signal_manager.SignalManager(self)
self.dynamicApiManager: DynamicApiManager = dynamic_api_manager.DynamicApiManager(self)
self.translationManager: TranslationManager = translation_manager.TranslationManager(self)
with debug.startup_timing("resource manager construction"):
self.resourceManager: resource_manager.ResourceManager = resource_manager.ResourceManager(self)
with debug.startup_timing("settings manager construction"):
self.settingsManager: SettingsManager = settings_manager.SettingsManager(self)
with debug.startup_timing("event manager construction"):
self.eventManager: EventManager = event_manager.EventManager(self)
with debug.startup_timing("compositor adapter construction"):
self.compositorStateAdapter: CompositorStateAdapter = \
compositor_state_adapter.CompositorStateAdapter()
self.eventManager.set_compositor_state_adapter(self.compositorStateAdapter)
with debug.startup_timing("script manager construction"):
self.scriptManager: ScriptManager = script_manager.ScriptManager(self)
script_manager._manager = self.scriptManager
with debug.startup_timing("logger construction"):
self.logger: logger.Logger = logger.Logger()
with debug.startup_timing("signal manager construction"):
self.signalManager: SignalManager = signal_manager.SignalManager(self)
with debug.startup_timing("dynamic API manager construction"):
self.dynamicApiManager: DynamicApiManager = dynamic_api_manager.DynamicApiManager(self)
with debug.startup_timing("translation manager construction"):
self.translationManager: TranslationManager = translation_manager.TranslationManager(self)
self.debugManager: Any = debug
self.APIHelper: APIHelper = APIHelper(self)
self.makoNotificationMonitor: mako_notification_monitor.MakoNotificationMonitor = \
mako_notification_monitor.MakoNotificationMonitor(self)
self.createCompatAPI()
self.pluginSystemManager: PluginSystemManager = plugin_system_manager.PluginSystemManager(self)
with debug.startup_timing("API helper construction"):
self.APIHelper: APIHelper = APIHelper(self)
with debug.startup_timing("notification monitor construction"):
self.makoNotificationMonitor: mako_notification_monitor.MakoNotificationMonitor = \
mako_notification_monitor.MakoNotificationMonitor(self)
with debug.startup_timing("compat API registration"):
self.createCompatAPI()
with debug.startup_timing("plugin manager construction"):
self.pluginSystemManager: PluginSystemManager = plugin_system_manager.PluginSystemManager(self)
# Scan for available plugins at startup
self.pluginSystemManager.rescanPlugins()
with debug.startup_timing("plugin scan"):
self.pluginSystemManager.rescanPlugins()
debug.print_startup_timing("cthulhuApp construction complete")
def getAPIHelper(self) -> APIHelper:
return self.APIHelper
+37 -15
View File
@@ -154,6 +154,8 @@ class Parser(argparse.ArgumentParser):
help=messages.CLI_DEBUG_FILE, metavar=messages.CLI_DEBUG_FILE_NAME)
self.add_argument(
"--debug", action="store_true", help=messages.CLI_ENABLE_DEBUG)
self.add_argument(
"--debug-startup", action="store_true", help=messages.CLI_ENABLE_STARTUP_DEBUG)
self._optionals.title = messages.CLI_OPTIONAL_ARGUMENTS
@@ -281,6 +283,8 @@ def cleanup(sigval):
time.sleep(0.5)
def main():
debug.reset_startup_timing()
setProcessName('cthulhu')
parser = Parser()
@@ -291,16 +295,26 @@ def main():
debug.eventDebugLevel = debug.LEVEL_OFF
debug.debugFile = open(args.debug_file, 'w')
debug.set_startup_timing_enabled(args.debug or args.debug_startup)
debug.print_startup_timing("launcher main start")
if args.replace:
cleanup(signal.SIGKILL)
with debug.startup_timing("replace cleanup"):
cleanup(signal.SIGKILL)
settingsDict = getattr(args, 'settings', {})
if not inGraphicalDesktop():
with debug.startup_timing("graphical desktop check"):
graphicalDesktop = inGraphicalDesktop()
if not graphicalDesktop:
print(messages.CLI_NO_DESKTOP_ERROR)
return 1
sessionType = getSessionType()
with debug.startup_timing("session check"):
sessionType = getSessionType()
xServerVendor = None
if sessionType == "x11":
xServerVendor = getXServerVendor()
sessionDetails = []
xdgSessionType = os.environ.get("XDG_SESSION_TYPE")
if xdgSessionType:
@@ -313,9 +327,8 @@ def main():
display = os.environ.get("DISPLAY")
if display:
sessionDetails.append(f"DISPLAY={display}")
vendor = getXServerVendor()
if vendor:
sessionDetails.append(f"X server vendor={vendor}")
if xServerVendor:
sessionDetails.append(f"X server vendor={xServerVendor}")
if sessionDetails:
msg = f"INFO: Session: {sessionType} ({', '.join(sessionDetails)})"
@@ -325,7 +338,9 @@ def main():
debug.printMessage(debug.LEVEL_INFO, "INFO: Preparing to launch.", True)
debug.print_startup_timing("cthulhu import start")
from cthulhu import cthulhu
debug.print_startup_timing("cthulhu import complete")
manager = cthulhu.cthulhuApp.settingsManager
if not manager:
@@ -333,25 +348,32 @@ def main():
return 1
debug.printMessage(debug.LEVEL_INFO, "INFO: About to activate settings manager.", True)
manager.activate(args.user_prefs, settingsDict)
with debug.startup_timing("settings manager launcher activation"):
manager.activate(args.user_prefs, settingsDict)
sys.path.insert(0, manager.getPrefsDir())
if args.profile:
try:
manager.setProfile(args.profile)
except Exception:
print(messages.CLI_LOAD_PROFILE_ERROR % args.profile)
manager.setProfile()
with debug.startup_timing("profile selection"):
try:
manager.setProfile(args.profile)
except Exception:
print(messages.CLI_LOAD_PROFILE_ERROR % args.profile)
manager.setProfile()
if args.setup:
cleanup(signal.SIGKILL)
cthulhu.showPreferencesGUI()
with debug.startup_timing("setup cleanup"):
cleanup(signal.SIGKILL)
with debug.startup_timing("preferences GUI startup"):
cthulhu.showPreferencesGUI()
if otherCthulhus():
with debug.startup_timing("other instance check"):
otherInstances = otherCthulhus()
if otherInstances:
print(messages.CLI_OTHER_CTHULHUS_ERROR)
return 1
debug.printMessage(debug.LEVEL_INFO, "INFO: About to launch Cthulhu.", True)
debug.print_startup_timing("cthulhu main handoff")
return cthulhu.main()
if __name__ == "__main__":
+74 -1
View File
@@ -37,13 +37,15 @@ __copyright__ = "Copyright (c) 2005-2008 Sun Microsystems Inc."
__license__ = "LGPL"
import inspect
from contextlib import contextmanager
import traceback
import os
import re
import subprocess
import sys
import time
import types
from typing import Any, Optional, TextIO, Pattern, TYPE_CHECKING
from typing import Any, Generator, Optional, TextIO, Pattern, TYPE_CHECKING
from datetime import datetime
import gi
@@ -149,6 +151,13 @@ eventDebugFilter: Optional[Pattern[str]] = None
#
debugEventQueue: bool = False
# Startup timing is intentionally separate from normal event tracing. When
# explicitly enabled, it can print concise phase timings without turning on the
# full debug stream.
startupTimingEnabled: bool = False
_startupTimingStartTime: Optional[float] = None
_startupTimingLastTime: Optional[float] = None
# What module(s) should be traced if traceit is being used. By default
# we'll just attend to ourself. (And by default, we will not enable
# traceit.) Note that enabling this functionality will drag your system
@@ -282,6 +291,70 @@ def print_log_tokens(level: int, prefix: str, tokens: list[Any], reason: Optiona
text = format_log_message(prefix, _format_tokens(tokens), reason)
printMessage(level, text, timestamp, stack)
def reset_startup_timing() -> None:
"""Reset the startup timing baseline."""
global _startupTimingStartTime
global _startupTimingLastTime
now = time.monotonic()
_startupTimingStartTime = now
_startupTimingLastTime = now
def set_startup_timing_enabled(enabled: bool = True) -> None:
"""Enable or disable concise startup phase timing output."""
global startupTimingEnabled
startupTimingEnabled = enabled
if enabled and _startupTimingStartTime is None:
reset_startup_timing()
def is_startup_timing_enabled() -> bool:
"""Returns True when startup timing output should be emitted."""
return startupTimingEnabled or debugLevel <= LEVEL_INFO
def _startup_timing_level() -> int:
if debugLevel <= LEVEL_INFO:
return LEVEL_INFO
return LEVEL_SEVERE
def print_startup_timing(phase: str) -> None:
"""Print elapsed startup timing for a named phase."""
global _startupTimingStartTime
global _startupTimingLastTime
if not is_startup_timing_enabled():
return
now = time.monotonic()
if _startupTimingStartTime is None or _startupTimingLastTime is None:
_startupTimingStartTime = now
_startupTimingLastTime = now
delta = now - _startupTimingLastTime
total = now - _startupTimingStartTime
_startupTimingLastTime = now
text = f"STARTUP TIMING: {phase}: +{delta:.3f}s, total {total:.3f}s"
printMessage(_startup_timing_level(), text, True)
@contextmanager
def startup_timing(phase: str) -> Generator[None, None, None]:
"""Context manager for timing a startup phase."""
if not is_startup_timing_enabled():
yield
return
print_startup_timing(f"{phase} start")
try:
yield
finally:
print_startup_timing(f"{phase} end")
def printTokens(level: int, tokens: list[Any], timestamp: bool = False, stack: bool = False) -> None:
if level < debugLevel:
return
+5
View File
@@ -293,6 +293,11 @@ CLI_HELP = _("Show this help message and exit")
# start with 'debug' and end with '.out', regardless of the locale.).
CLI_ENABLE_DEBUG = _("Send debug output to debug-YYYY-MM-DD-HH:MM:SS.out")
# Translators: This is the description of command line option '--debug-startup'
# which causes Cthulhu to print concise startup timing information without
# enabling the full debug output stream.
CLI_ENABLE_STARTUP_DEBUG = _("Send concise startup timing output")
# Translators: This is the description of command line option '--debug-file'
# which allows the user to override the default date-based name of the debugging
# output file.
+15 -6
View File
@@ -545,31 +545,40 @@ class SpeechServer(speechserver.SpeechServer):
- utteranceIterator: Iterator yielding [SayAllContext, acss] tuples
- progressCallback: Called with progress updates
"""
GLib.idle_add(self._sayAllWorker, utteranceIterator, progressCallback)
with self._lock:
self._stopEvent.clear()
self._speakGeneration += 1
generation = self._speakGeneration
GLib.idle_add(self._sayAllWorker, utteranceIterator, progressCallback, generation)
def _sayAllWorker(self, iterator, callback):
def _sayAllWorker(self, iterator, callback, generation):
"""Process one utterance at a time (called via GLib.idle_add).
Arguments:
- iterator: Utterance iterator
- callback: Progress callback
"""
with self._lock:
if self._stopEvent.is_set() or generation != self._speakGeneration:
return False
try:
context, acss = next(iterator)
except StopIteration:
return False
def onComplete():
with self._lock:
if self._stopEvent.is_set() or generation != self._speakGeneration:
return False
context.currentOffset = context.endOffset
callback(context.copy(), speechserver.SayAllContext.COMPLETED)
GLib.idle_add(self._sayAllWorker, iterator, callback)
GLib.idle_add(self._sayAllWorker, iterator, callback, generation)
return False
context.currentOffset = context.startOffset
callback(context.copy(), speechserver.SayAllContext.PROGRESS)
with self._lock:
self._stopEvent.clear()
generation = self._speakGeneration
self._currentFuture = self._executor.submit(
self._synthesizeAndPlay, context.utterance, acss, onComplete, generation
)
+34 -4
View File
@@ -165,6 +165,8 @@ class SpeechServer(speechserver.SpeechServer):
SpeechServer._active_servers[serverId] = self
self._lastKeyEchoTime = None
self._say_all_generation = 0
self._cancelled_say_all_generation = None
def _init(self):
self._client = client = speechd.SSIPClient('Cthulhu', component=self._id)
@@ -489,22 +491,42 @@ class SpeechServer(speechserver.SpeechServer):
self._debug_sd_values(f"Speaking '{ssml}' ")
self._send_command(self._client.speak, ssml, **kwargs)
def _say_all(self, iterator, cthulhu_callback):
def _say_all(self, iterator, cthulhu_callback, generation):
"""Process another sayAll chunk.
Called by the gidle thread.
"""
if generation != self._say_all_generation:
return False
if generation == self._cancelled_say_all_generation:
return False
try:
context, acss = next(iterator)
except StopIteration:
pass
else:
def dispatch_callback(context_copy, progress_type, allow_interrupted=False):
if not allow_interrupted and generation != self._say_all_generation:
return False
cthulhu_callback(context_copy, progress_type)
return False
def callback(callbackType, index_mark=None):
# This callback is called in Speech Dispatcher listener thread.
# No subsequent Speech Dispatcher interaction is allowed here,
# so we pass the calls to the gidle thread.
t = self._CALLBACK_TYPE_MAP[callbackType]
if generation != self._say_all_generation:
return
if generation == self._cancelled_say_all_generation:
if t == speechserver.SayAllContext.INTERRUPTED:
self._say_all_generation += 1
self._cancelled_say_all_generation = None
GLib.idle_add(dispatch_callback, context.copy(), t, True)
return
if t == speechserver.SayAllContext.PROGRESS:
if index_mark:
index = index_mark.split(':')
@@ -524,14 +546,15 @@ class SpeechServer(speechserver.SpeechServer):
elif t == speechserver.SayAllContext.COMPLETED:
context.currentOffset = context.endOffset
context.currentEndOffset = None
GLib.idle_add(cthulhu_callback, context.copy(), t)
GLib.idle_add(dispatch_callback, context.copy(), t)
if t == speechserver.SayAllContext.COMPLETED:
GLib.idle_add(self._say_all, iterator, cthulhu_callback)
GLib.idle_add(self._say_all, iterator, cthulhu_callback, generation)
self._speak(context.utterance, acss, callback=callback,
event_types=list(self._CALLBACK_TYPE_MAP.keys()))
return False # to indicate, that we don't want to be called again.
def _cancel(self):
self._cancelled_say_all_generation = self._say_all_generation
self._send_command(self._client.cancel)
def _change_default_speech_rate(self, step, decrease=False):
@@ -657,7 +680,14 @@ class SpeechServer(speechserver.SpeechServer):
self._speak(text, acss)
def sayAll(self, utteranceIterator, progressCallback):
GLib.idle_add(self._say_all, utteranceIterator, progressCallback)
self._say_all_generation += 1
self._cancelled_say_all_generation = None
GLib.idle_add(
self._say_all,
utteranceIterator,
progressCallback,
self._say_all_generation,
)
def speakCharacter(self, character, acss=None):
self._apply_acss(acss)