From 3fdcffbc8c7c487cd1c550ba85861698a921cbf7 Mon Sep 17 00:00:00 2001 From: S4NKALP Date: Sun, 20 Jul 2025 22:30:35 +0545 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(system):=20optimi?= =?UTF-8?q?ze=20binary=20search,=20improve=20caching=20strategy,=20and=20a?= =?UTF-8?q?dd=20background=20cache=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/launcher/plugins/system.py | 209 +++++++++++++++++++++-------- 1 file changed, 154 insertions(+), 55 deletions(-) diff --git a/modules/launcher/plugins/system.py b/modules/launcher/plugins/system.py index 96296b8..caaf452 100644 --- a/modules/launcher/plugins/system.py +++ b/modules/launcher/plugins/system.py @@ -1,10 +1,13 @@ +import json import os import shlex +import threading import time -from typing import List, Union +from typing import List, Set, Union from fabric.utils import exec_shell_command_async +import config.data as data import utils.icons as icons from modules.launcher.plugin_base import PluginBase from modules.launcher.result import Result @@ -20,34 +23,118 @@ class SystemPlugin(PluginBase): self.display_name = "System" self.description = "System commands and actions" - # Cache for system binaries - self._bin_cache = set() + # JSON cache file for system binaries + self.bin_cache_file = os.path.join(data.CACHE_DIR, "system_binaries.json") + + # In-memory cache for system binaries + self._bin_cache: Set[str] = set() self._last_bin_update = 0 self._bin_update_interval = 300 # 5 minutes + # Background cache building + self._cache_building = False + self._cache_thread = None + def initialize(self): """Initialize the system plugin.""" self.set_triggers(["bin"]) - self._update_bin_cache() + self._load_bin_cache() + self._start_background_cache_update() def cleanup(self): """Cleanup the system plugin.""" self._bin_cache.clear() + if self._cache_thread and self._cache_thread.is_alive(): + # Note: We don't join the thread to avoid blocking cleanup + pass - def _update_bin_cache(self): - """Update the cache of available system binaries.""" + def _load_bin_cache(self): + """Load binary cache from JSON file.""" + try: + if os.path.exists(self.bin_cache_file): + with open(self.bin_cache_file, "r", encoding="utf-8") as f: + cache_data = json.load(f) + self._bin_cache = set(cache_data.get("binaries", [])) + self._last_bin_update = cache_data.get("last_update", 0) + print(f"SystemPlugin: Loaded {len(self._bin_cache)} binaries from cache") + else: + print("SystemPlugin: No cache file found, will build cache in background") + except Exception as e: + print(f"SystemPlugin: Error loading binary cache: {e}") + self._bin_cache = set() + self._last_bin_update = 0 + + def _save_bin_cache(self): + """Save binary cache to JSON file.""" + try: + # Ensure the cache directory exists + os.makedirs(data.CACHE_DIR, exist_ok=True) + + cache_data = { + "binaries": sorted(list(self._bin_cache)), + "last_update": self._last_bin_update, + "cache_version": "1.0" + } + + with open(self.bin_cache_file, "w", encoding="utf-8") as f: + json.dump(cache_data, f, indent=2) + print(f"SystemPlugin: Saved {len(self._bin_cache)} binaries to cache") + except Exception as e: + print(f"SystemPlugin: Error saving binary cache: {e}") + + def _start_background_cache_update(self): + """Start background thread to update binary cache.""" current_time = time.time() - if current_time - self._last_bin_update > self._bin_update_interval: - self._bin_cache.clear() + + # Check if cache needs updating + if (current_time - self._last_bin_update > self._bin_update_interval or + not self._bin_cache): + + if not self._cache_building: + self._cache_building = True + self._cache_thread = threading.Thread( + target=self._build_bin_cache_background, + daemon=True + ) + self._cache_thread.start() + + def _build_bin_cache_background(self): + """Build binary cache in background thread.""" + try: + print("SystemPlugin: Building binary cache in background...") + new_cache = set() + processed_paths = set() # Avoid duplicate paths + for path in os.environ["PATH"].split(":"): - if os.path.exists(path): + # Skip empty paths and duplicates + if not path or path in processed_paths: + continue + processed_paths.add(path) + + if os.path.exists(path) and os.path.isdir(path): try: - for file in os.listdir(path): - if os.access(os.path.join(path, file), os.X_OK): - self._bin_cache.add(file) - except (PermissionError, FileNotFoundError): + # Use os.scandir for better performance than os.listdir + with os.scandir(path) as entries: + for entry in entries: + if (entry.is_file(follow_symlinks=False) and + os.access(entry.path, os.X_OK)): + new_cache.add(entry.name) + except (PermissionError, FileNotFoundError, OSError): continue - self._last_bin_update = current_time + + # Update cache atomically + self._bin_cache = new_cache + self._last_bin_update = time.time() + + # Save to disk + self._save_bin_cache() + + print(f"SystemPlugin: Background cache update completed. Found {len(self._bin_cache)} binaries") + + except Exception as e: + print(f"SystemPlugin: Error building binary cache: {e}") + finally: + self._cache_building = False def query(self, query_string: str) -> List[Result]: """Search for system commands matching the query.""" @@ -67,55 +154,67 @@ class SystemPlugin(PluginBase): full_command = query # Keep the original case and spacing # Check system binaries - self._update_bin_cache() + # Start background update if needed (non-blocking) - but only if cache is empty or very old + if not self._bin_cache or (time.time() - self._last_bin_update > self._bin_update_interval): + self._start_background_cache_update() + + # Optimize search with early termination and result limiting + exact_matches = [] + prefix_matches = [] + partial_matches = [] + max_results = 20 # Limit total results for performance + for binary in self._bin_cache: - if binary_query in binary.lower(): - relevance = self._calculate_binary_relevance(binary, binary_query) - if relevance > 0: - # If the query starts with this binary, use the full command - if binary.lower() == binary_query: - # Exact binary match - use full command with arguments - display_command = full_command - command_to_execute = full_command - elif binary.lower().startswith(binary_query): - # Binary starts with query - suggest the binary name only - display_command = binary - command_to_execute = binary - else: - # Partial match - suggest the binary name only - display_command = binary - command_to_execute = binary + # Pre-compute lowercase once + binary_lower = binary.lower() - result = Result( - title=display_command, - subtitle=f"Execute: {display_command}", - icon_markup=icons.terminal, - action=self._create_action(command_to_execute), - relevance=relevance, - plugin_name=self.display_name, - data={"command": command_to_execute, "id": binary}, - ) - results.append(result) + # Skip if no match at all + if binary_query not in binary_lower: + continue - return sorted(results, key=lambda x: x.relevance, reverse=True) + # Categorize matches for better sorting + if binary_lower == binary_query: + # Exact match - highest priority + display_command = full_command + command_to_execute = full_command + relevance = 1.0 + exact_matches.append((binary, display_command, command_to_execute, relevance)) + elif binary_lower.startswith(binary_query): + # Prefix match - high priority + display_command = binary + command_to_execute = binary + relevance = 0.9 + prefix_matches.append((binary, display_command, command_to_execute, relevance)) + else: + # Partial match - lower priority + display_command = binary + command_to_execute = binary + relevance = 0.7 + partial_matches.append((binary, display_command, command_to_execute, relevance)) - def _calculate_binary_relevance(self, binary: str, query: str) -> float: - """Calculate relevance score for a binary.""" - binary_lower = binary.lower() + # Early termination if we have enough good matches + if len(exact_matches) + len(prefix_matches) >= max_results: + break - # Exact match - if query == binary_lower: - return 1.0 + # Combine results in priority order + all_matches = exact_matches + prefix_matches + partial_matches - # Starts with query - if binary_lower.startswith(query): - return 0.9 + # Convert to Result objects (limit to max_results) + for binary, display_command, command_to_execute, relevance in all_matches[:max_results]: + result = Result( + title=display_command, + subtitle=f"Execute: {display_command}", + icon_markup=icons.terminal, + action=self._create_action(command_to_execute), + relevance=relevance, + plugin_name=self.display_name, + data={"command": command_to_execute, "id": binary}, + ) + results.append(result) + + return results # Already sorted by priority - # Contains query - if query in binary_lower: - return 0.7 - return 0.0 def _create_action(self, command: Union[str, List[str]]): """Create an action function for the given command."""