Refactor Windows installer configuration and improve logging functionality
- Changed installation scope from "perMachine" to "perUser" in the Windows installer configuration. - Updated installation directory from "ProgramFiles64Folder" to "LocalAppDataFolder" for user-specific installations. - Enhanced the configuration saving method to create parent directories if they don't exist. - Improved the main window script loading logic to support multiple installation scenarios (development, PyInstaller, MSI). - Added detailed logging for script loading failures and success messages. - Implemented a new method to reconfigure logging settings at runtime, allowing dynamic updates from the settings dialog. - Enhanced the settings dialog to handle configuration saving, including log level changes and error handling.
This commit is contained in:
parent
0c276b9022
commit
8f3f859e5b
9 changed files with 3068 additions and 2905 deletions
|
|
@ -284,7 +284,9 @@ class Config:
|
|||
"""Save configuration to JSON file.
|
||||
|
||||
Args:
|
||||
config_path: Path to save configuration
|
||||
config_path: Path to save configuration to
|
||||
|
||||
Creates parent directories if they don't exist.
|
||||
"""
|
||||
data = {
|
||||
"app_name": self.app_name,
|
||||
|
|
|
|||
|
|
@ -448,32 +448,72 @@ class MainWindow(QMainWindow):
|
|||
config_code = self._generate_config_injection_script()
|
||||
|
||||
# Load bridge script from file
|
||||
# Using intercept script - prevents browser drag, hands off to Qt
|
||||
# Support both development mode and PyInstaller bundle
|
||||
# Try multiple paths to support dev mode, PyInstaller bundle, and MSI installation
|
||||
# 1. Development mode: __file__.parent / script.js
|
||||
# 2. PyInstaller bundle: sys._MEIPASS / webdrop_bridge / ui / script.js
|
||||
# 3. MSI installation: Same directory as executable
|
||||
script_path = None
|
||||
download_interceptor_path = None
|
||||
|
||||
# List of paths to try in order of preference
|
||||
search_paths = []
|
||||
|
||||
# 1. Development mode
|
||||
search_paths.append(Path(__file__).parent / "bridge_script_intercept.js")
|
||||
|
||||
# 2. PyInstaller bundle (via sys._MEIPASS)
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
# Running as PyInstaller bundle
|
||||
script_path = Path(sys._MEIPASS) / "webdrop_bridge" / "ui" / "bridge_script_intercept.js" # type: ignore
|
||||
else:
|
||||
# Running in development mode
|
||||
script_path = Path(__file__).parent / "bridge_script_intercept.js"
|
||||
search_paths.append(Path(sys._MEIPASS) / "webdrop_bridge" / "ui" / "bridge_script_intercept.js") # type: ignore
|
||||
|
||||
# 3. Installed executable's directory (handles MSI installation where all files are packaged together)
|
||||
exe_dir = Path(sys.executable).parent
|
||||
search_paths.append(exe_dir / "webdrop_bridge" / "ui" / "bridge_script_intercept.js")
|
||||
|
||||
# Find the bridge script
|
||||
for path in search_paths:
|
||||
if path.exists():
|
||||
script_path = path
|
||||
logger.debug(f"Found bridge script at: {script_path}")
|
||||
break
|
||||
|
||||
if script_path is None:
|
||||
# Log all attempted paths for debugging
|
||||
logger.error("Bridge script NOT found at any expected location:")
|
||||
for i, path in enumerate(search_paths, 1):
|
||||
logger.error(f" [{i}] {path} (exists: {path.exists()})")
|
||||
logger.error(f"sys._MEIPASS: {getattr(sys, '_MEIPASS', 'NOT SET')}")
|
||||
logger.error(f"sys.executable: {sys.executable}")
|
||||
logger.error(f"__file__: {__file__}")
|
||||
|
||||
try:
|
||||
if script_path is None:
|
||||
raise FileNotFoundError("bridge_script_intercept.js not found in any expected location")
|
||||
|
||||
with open(script_path, 'r', encoding='utf-8') as f:
|
||||
bridge_code = f.read()
|
||||
|
||||
# Load download interceptor
|
||||
# Load download interceptor using similar search path logic
|
||||
download_search_paths = []
|
||||
download_search_paths.append(Path(__file__).parent / "download_interceptor.js")
|
||||
if hasattr(sys, '_MEIPASS'):
|
||||
download_interceptor_path = Path(sys._MEIPASS) / "webdrop_bridge" / "ui" / "download_interceptor.js" # type: ignore
|
||||
else:
|
||||
download_interceptor_path = Path(__file__).parent / "download_interceptor.js"
|
||||
download_search_paths.append(Path(sys._MEIPASS) / "webdrop_bridge" / "ui" / "download_interceptor.js") # type: ignore
|
||||
download_search_paths.append(exe_dir / "webdrop_bridge" / "ui" / "download_interceptor.js")
|
||||
|
||||
download_interceptor_code = ""
|
||||
try:
|
||||
with open(download_interceptor_path, 'r', encoding='utf-8') as f:
|
||||
download_interceptor_code = f.read()
|
||||
logger.debug(f"Loaded download interceptor from {download_interceptor_path}")
|
||||
except (OSError, IOError) as e:
|
||||
logger.warning(f"Download interceptor not found: {e}")
|
||||
for path in download_search_paths:
|
||||
if path.exists():
|
||||
download_interceptor_path = path
|
||||
break
|
||||
|
||||
if download_interceptor_path:
|
||||
try:
|
||||
with open(download_interceptor_path, 'r', encoding='utf-8') as f:
|
||||
download_interceptor_code = f.read()
|
||||
logger.debug(f"Loaded download interceptor from {download_interceptor_path}")
|
||||
except (OSError, IOError) as e:
|
||||
logger.warning(f"Download interceptor exists but failed to load: {e}")
|
||||
else:
|
||||
logger.debug("Download interceptor not found (optional)")
|
||||
|
||||
# Combine: qwebchannel.js + config + bridge script + download interceptor
|
||||
combined_code = qwebchannel_code + "\n\n" + config_code + "\n\n" + bridge_code
|
||||
|
|
@ -492,9 +532,13 @@ class MainWindow(QMainWindow):
|
|||
|
||||
script.setSourceCode(combined_code)
|
||||
self.web_view.page().scripts().insert(script)
|
||||
logger.debug(f"Installed bridge script from {script_path}")
|
||||
logger.debug(f"✅ Successfully installed bridge script")
|
||||
logger.debug(f" Script size: {len(combined_code)} chars")
|
||||
logger.debug(f" Loaded from: {script_path}")
|
||||
except (OSError, IOError) as e:
|
||||
logger.warning(f"Failed to load bridge script: {e}")
|
||||
logger.error(f"❌ Failed to load bridge script: {e}")
|
||||
logger.error(f" This will break drag-and-drop functionality!")
|
||||
# Don't re-raise - allow app to start (will show error in logs)
|
||||
|
||||
def _generate_config_injection_script(self) -> str:
|
||||
"""Generate JavaScript code that injects configuration.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Settings dialog for configuration management."""
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
|
|
@ -22,6 +23,9 @@ from PySide6.QtWidgets import (
|
|||
|
||||
from webdrop_bridge.config import Config, ConfigurationError
|
||||
from webdrop_bridge.core.config_manager import ConfigExporter, ConfigProfile, ConfigValidator
|
||||
from webdrop_bridge.utils.logging import reconfigure_logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class SettingsDialog(QDialog):
|
||||
|
|
@ -76,6 +80,63 @@ class SettingsDialog(QDialog):
|
|||
|
||||
self.setLayout(layout)
|
||||
|
||||
def accept(self) -> None:
|
||||
"""Handle OK button - save configuration changes to file.
|
||||
|
||||
Validates configuration and saves to the default config path.
|
||||
Applies log level changes immediately in the running application.
|
||||
If validation or save fails, shows error and stays in dialog.
|
||||
"""
|
||||
try:
|
||||
# Get updated configuration data from UI
|
||||
config_data = self.get_config_data()
|
||||
|
||||
# Preserve URL mappings from original config (not editable in UI yet)
|
||||
config_data["url_mappings"] = [
|
||||
{
|
||||
"url_prefix": m.url_prefix,
|
||||
"local_path": m.local_path
|
||||
}
|
||||
for m in self.config.url_mappings
|
||||
]
|
||||
|
||||
# Update the config object with new values
|
||||
old_log_level = self.config.log_level
|
||||
self.config.log_level = config_data["log_level"]
|
||||
self.config.log_file = Path(config_data["log_file"]) if config_data["log_file"] else None
|
||||
self.config.allowed_roots = [Path(r).resolve() for r in config_data["allowed_roots"]]
|
||||
self.config.allowed_urls = config_data["allowed_urls"]
|
||||
self.config.window_width = config_data["window_width"]
|
||||
self.config.window_height = config_data["window_height"]
|
||||
|
||||
# Save to file (creates parent dirs if needed)
|
||||
config_path = Config.get_default_config_path()
|
||||
self.config.to_file(config_path)
|
||||
|
||||
logger.info(f"Configuration saved to {config_path}")
|
||||
logger.info(f" Log level: {self.config.log_level} (was: {old_log_level})")
|
||||
logger.info(f" Window size: {self.config.window_width}x{self.config.window_height}")
|
||||
|
||||
# Apply log level change immediately to running application
|
||||
if old_log_level != self.config.log_level:
|
||||
logger.info(f"🔄 Updating log level: {old_log_level} → {self.config.log_level}")
|
||||
reconfigure_logging(
|
||||
logger_name="webdrop_bridge",
|
||||
level=self.config.log_level,
|
||||
log_file=self.config.log_file
|
||||
)
|
||||
logger.info(f"✅ Log level updated to {self.config.log_level}")
|
||||
|
||||
# Call parent accept to close dialog
|
||||
super().accept()
|
||||
|
||||
except ConfigurationError as e:
|
||||
logger.error(f"Configuration error: {e}")
|
||||
self._show_error(f"Configuration Error:\n\n{e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save configuration: {e}", exc_info=True)
|
||||
self._show_error(f"Failed to save configuration:\n\n{e}")
|
||||
|
||||
def _create_paths_tab(self) -> QWidget:
|
||||
"""Create paths configuration tab."""
|
||||
widget = QWidget()
|
||||
|
|
|
|||
|
|
@ -154,6 +154,66 @@ def setup_logging(
|
|||
return logger
|
||||
|
||||
|
||||
def reconfigure_logging(
|
||||
logger_name: str = "webdrop_bridge",
|
||||
level: str = "INFO",
|
||||
log_file: Optional[Path] = None,
|
||||
) -> None:
|
||||
"""Reconfigure existing logger at runtime (e.g., from settings dialog).
|
||||
|
||||
Updates the log level and log file for a running logger.
|
||||
Useful when user changes logging settings in the UI.
|
||||
|
||||
Args:
|
||||
logger_name: Name of logger to reconfigure
|
||||
level: New logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
log_file: Optional path to log file
|
||||
|
||||
Raises:
|
||||
KeyError: If level is not a valid logging level
|
||||
"""
|
||||
try:
|
||||
numeric_level = getattr(logging, level.upper())
|
||||
except AttributeError as e:
|
||||
raise KeyError(f"Invalid logging level: {level}") from e
|
||||
|
||||
logger = logging.getLogger(logger_name)
|
||||
logger.setLevel(numeric_level)
|
||||
|
||||
# Update level on all existing handlers
|
||||
for handler in logger.handlers:
|
||||
handler.setLevel(numeric_level)
|
||||
|
||||
# If log file changed, remove old file handler and add new one
|
||||
if log_file:
|
||||
# Remove old file handlers
|
||||
for handler in logger.handlers[:]:
|
||||
if isinstance(handler, logging.handlers.RotatingFileHandler):
|
||||
handler.close()
|
||||
logger.removeHandler(handler)
|
||||
|
||||
try:
|
||||
# Create parent directories if needed
|
||||
log_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create new file handler
|
||||
file_handler = logging.handlers.RotatingFileHandler(
|
||||
log_file,
|
||||
maxBytes=10 * 1024 * 1024, # 10 MB
|
||||
backupCount=5,
|
||||
encoding="utf-8",
|
||||
)
|
||||
file_handler.setLevel(numeric_level)
|
||||
|
||||
# Use same formatter as existing handlers
|
||||
if logger.handlers:
|
||||
file_handler.setFormatter(logger.handlers[0].formatter)
|
||||
|
||||
logger.addHandler(file_handler)
|
||||
except (OSError, IOError) as e:
|
||||
raise ValueError(f"Cannot write to log file {log_file}: {e}") from e
|
||||
|
||||
|
||||
def get_logger(name: str = __name__) -> logging.Logger:
|
||||
"""Get a logger instance for a module.
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue