Fix script manager resets and isolate startup sound
This commit is contained in:
@@ -894,6 +894,13 @@ def main() -> int:
|
||||
|
||||
script = cthulhu_state.activeScript
|
||||
sound_theme_manager.getManager().playStartSound(wait=True)
|
||||
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()
|
||||
@@ -944,6 +951,7 @@ class Cthulhu(GObject.Object):
|
||||
self.settingsManager: SettingsManager = settings_manager.SettingsManager(self) # Directly instantiate
|
||||
self.eventManager: EventManager = event_manager.EventManager(self) # Directly instantiate
|
||||
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)
|
||||
|
||||
@@ -277,6 +277,23 @@ class InputEventManager:
|
||||
self._last_input_event = event
|
||||
self._last_non_modifier_key_event = None
|
||||
|
||||
@staticmethod
|
||||
def _get_top_level_window(obj: Optional[Atspi.Accessible]) -> Optional[Atspi.Accessible]:
|
||||
"""Returns the top-level window containing obj, if one can be found."""
|
||||
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
if AXUtilities.is_frame(obj) or AXUtilities.is_window(obj) or AXUtilities.is_dialog_or_alert(obj):
|
||||
return obj
|
||||
|
||||
return AXObject.find_ancestor(
|
||||
obj,
|
||||
lambda x: AXUtilities.is_frame(x)
|
||||
or AXUtilities.is_window(x)
|
||||
or AXUtilities.is_dialog_or_alert(x),
|
||||
)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
# pylint: disable=too-many-positional-arguments
|
||||
def process_keyboard_event(
|
||||
@@ -320,11 +337,25 @@ class InputEventManager:
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
manager.set_active_window(window)
|
||||
else:
|
||||
# One example: Brave's popup menus live in frames which lack the active state.
|
||||
tokens = ["WARNING:", window, "cannot be active window. No alternative found."]
|
||||
debug.print_tokens(debug.LEVEL_WARNING, tokens, True)
|
||||
window = None
|
||||
manager.set_active_window(None, notify_script=True)
|
||||
focus_window = self._get_top_level_window(pendingFocus or manager.get_locus_of_focus())
|
||||
if focus_window is not None:
|
||||
window = focus_window
|
||||
tokens = [
|
||||
"INPUT EVENT MANAGER: Recovering active window from locus of focus:",
|
||||
window,
|
||||
]
|
||||
debug.print_tokens(debug.LEVEL_INFO, tokens, True)
|
||||
manager.set_active_window(window)
|
||||
else:
|
||||
# One example: Brave's popup menus live in frames which lack the active
|
||||
# state. Failing to revalidate the window on a key press is inconclusive;
|
||||
# do not wipe out the last known window and focus state.
|
||||
tokens = [
|
||||
"WARNING:",
|
||||
window,
|
||||
"cannot be confirmed as active. No alternative found; preserving existing context.",
|
||||
]
|
||||
debug.print_tokens(debug.LEVEL_WARNING, tokens, True)
|
||||
event.set_window(window)
|
||||
event.set_object(pendingFocus or manager.get_locus_of_focus())
|
||||
event.set_script(script_manager.get_manager().get_active_script())
|
||||
|
||||
@@ -86,6 +86,7 @@ cthulhu_python_sources = files([
|
||||
'signal_manager.py',
|
||||
'sleep_mode_manager.py',
|
||||
'sound.py',
|
||||
'sound_helper.py',
|
||||
'sound_generator.py',
|
||||
'sound_theme_manager.py',
|
||||
'speech_and_verbosity_manager.py',
|
||||
|
||||
@@ -473,5 +473,5 @@ def get_manager() -> Optional[ScriptManager]:
|
||||
if _manager is None:
|
||||
from . import cthulhu
|
||||
if cthulhu.cthulhuApp:
|
||||
_manager = ScriptManager(cthulhu.cthulhuApp)
|
||||
_manager = cthulhu.cthulhuApp.scriptManager
|
||||
return _manager
|
||||
|
||||
+79
-1
@@ -31,9 +31,13 @@ __date__ = "$Date:$"
|
||||
__copyright__ = "Copyright (c) 2016 Cthulhu Team"
|
||||
__license__ = "LGPL"
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import gi
|
||||
from gi.repository import GLib
|
||||
from typing import Optional, Any
|
||||
from typing import Optional, Any, Tuple
|
||||
|
||||
try:
|
||||
gi.require_version('Gst', '1.0')
|
||||
@@ -46,6 +50,8 @@ else:
|
||||
from . import debug
|
||||
from .sound_generator import Icon, Tone
|
||||
|
||||
_soundSystemFailureReason: Optional[str] = None
|
||||
|
||||
class Player:
|
||||
"""Plays Icons and Tones."""
|
||||
|
||||
@@ -233,3 +239,75 @@ _player = Player()
|
||||
|
||||
def getPlayer():
|
||||
return _player
|
||||
|
||||
def disableSoundSystem(reason: str) -> None:
|
||||
global _soundSystemFailureReason
|
||||
|
||||
if _soundSystemFailureReason == reason:
|
||||
return
|
||||
|
||||
_soundSystemFailureReason = reason
|
||||
msg = f"SOUND: Disabling sound system. Reason: {reason}"
|
||||
debug.printMessage(debug.LEVEL_INFO, msg, True)
|
||||
|
||||
def getSoundSystemFailureReason() -> Optional[str]:
|
||||
return _soundSystemFailureReason
|
||||
|
||||
def isSoundSystemAvailable() -> bool:
|
||||
return _soundSystemFailureReason is None
|
||||
|
||||
def _playIconInSubprocess(icon: Icon, timeoutSeconds: int = 10) -> Tuple[bool, Optional[str]]:
|
||||
if not icon.isValid():
|
||||
return False, f"Invalid sound icon: {icon.path}"
|
||||
|
||||
pythonPathEntries = []
|
||||
for path in sys.path:
|
||||
if not path:
|
||||
path = os.getcwd()
|
||||
if os.path.isdir(path) and path not in pythonPathEntries:
|
||||
pythonPathEntries.append(path)
|
||||
|
||||
environment = os.environ.copy()
|
||||
if pythonPathEntries:
|
||||
environment["PYTHONPATH"] = os.pathsep.join(pythonPathEntries)
|
||||
|
||||
command = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"cthulhu.sound_helper",
|
||||
"--play-file",
|
||||
icon.path,
|
||||
"--timeout-seconds",
|
||||
str(timeoutSeconds),
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
command,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeoutSeconds + 2,
|
||||
check=False,
|
||||
env=environment,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
return False, f"Sound probe timed out after {timeoutSeconds} seconds"
|
||||
except Exception as error:
|
||||
return False, f"Sound probe failed to launch: {error}"
|
||||
|
||||
if result.returncode == 0:
|
||||
return True, None
|
||||
|
||||
if result.returncode < 0:
|
||||
reason = f"Sound probe exited via signal {-result.returncode}"
|
||||
else:
|
||||
reason = f"Sound probe exited with status {result.returncode}"
|
||||
|
||||
stderr = (result.stderr or "").strip()
|
||||
if stderr:
|
||||
reason = f"{reason}: {stderr.splitlines()[0]}"
|
||||
|
||||
return False, reason
|
||||
|
||||
def playIconSafely(icon: Icon, timeoutSeconds: int = 10) -> Tuple[bool, Optional[str]]:
|
||||
return _playIconInSubprocess(icon, timeoutSeconds)
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2026 Stormux
|
||||
#
|
||||
# This library is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU Lesser General Public
|
||||
# License as published by the Free Software Foundation; either
|
||||
# version 2.1 of the License, or (at your option) any later version.
|
||||
#
|
||||
# This library is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
|
||||
"""Minimal sound playback helper used to isolate startup sound crashes."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version('Gst', '1.0')
|
||||
from gi.repository import Gst
|
||||
|
||||
|
||||
def play_file(soundPath: str, timeoutSeconds: int = 10) -> int:
|
||||
available, _args = Gst.init_check(None)
|
||||
if not available:
|
||||
return 1
|
||||
|
||||
player = Gst.ElementFactory.make('playbin', 'cthulhu-sound-helper')
|
||||
if player is None:
|
||||
return 1
|
||||
|
||||
player.set_property('uri', pathlib.Path(soundPath).resolve().as_uri())
|
||||
player.set_state(Gst.State.PLAYING)
|
||||
|
||||
bus = player.get_bus()
|
||||
if bus is None:
|
||||
player.set_state(Gst.State.NULL)
|
||||
return 1
|
||||
|
||||
timeoutNs = int(timeoutSeconds * Gst.SECOND)
|
||||
try:
|
||||
message = bus.timed_pop_filtered(
|
||||
timeoutNs,
|
||||
Gst.MessageType.EOS | Gst.MessageType.ERROR
|
||||
)
|
||||
if message is None:
|
||||
return 1
|
||||
if message.type == Gst.MessageType.ERROR:
|
||||
return 1
|
||||
return 0
|
||||
finally:
|
||||
player.set_state(Gst.State.NULL)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(add_help=False)
|
||||
parser.add_argument("--play-file")
|
||||
parser.add_argument("--timeout-seconds", type=int, default=10)
|
||||
args, _unknown = parser.parse_known_args()
|
||||
|
||||
if args.play_file:
|
||||
return play_file(args.play_file, args.timeout_seconds)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -349,9 +349,14 @@ class SoundThemeManager:
|
||||
try:
|
||||
icon = Icon(os.path.dirname(soundPath), os.path.basename(soundPath))
|
||||
if icon.isValid():
|
||||
if wait:
|
||||
success, reason = sound.playIconSafely(icon)
|
||||
if not success:
|
||||
if reason:
|
||||
sound.disableSoundSystem(reason)
|
||||
self.app.getSettingsManager().setSetting('enableSound', False)
|
||||
return success
|
||||
player = sound.getPlayer()
|
||||
if wait and hasattr(player, "playAndWait"):
|
||||
return player.playAndWait(icon, interrupt=interrupt)
|
||||
player.play(icon, interrupt=interrupt)
|
||||
return True
|
||||
except Exception as e:
|
||||
@@ -374,7 +379,8 @@ class SoundThemeManager:
|
||||
return self._playThemeSound(
|
||||
soundName,
|
||||
interrupt=interrupt,
|
||||
wait=wait
|
||||
wait=wait,
|
||||
requireSoundSetting=True
|
||||
)
|
||||
|
||||
def playFocusModeSound(self):
|
||||
|
||||
Reference in New Issue
Block a user