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:
claudi 2026-02-19 15:48:59 +01:00
parent 0c276b9022
commit 8f3f859e5b
9 changed files with 3068 additions and 2905 deletions

File diff suppressed because one or more lines are too long

View file

@ -5,7 +5,7 @@
Manufacturer="HIM-Tools" Manufacturer="HIM-Tools"
UpgradeCode="12345678-1234-1234-1234-123456789012"> UpgradeCode="12345678-1234-1234-1234-123456789012">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" Platform="x64" /> <Package InstallerVersion="200" Compressed="yes" InstallScope="perUser" Platform="x64" />
<Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" /> <Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
<!-- Required property for WixUI_InstallDir dialog set --> <!-- Required property for WixUI_InstallDir dialog set -->
@ -30,7 +30,7 @@
</Feature> </Feature>
<Directory Id="TARGETDIR" Name="SourceDir"> <Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder"> <Directory Id="LocalAppDataFolder">
<Directory Id="INSTALLFOLDER" Name="WebDrop Bridge" /> <Directory Id="INSTALLFOLDER" Name="WebDrop Bridge" />
</Directory> </Directory>
<Directory Id="ProgramMenuFolder"> <Directory Id="ProgramMenuFolder">

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -307,7 +307,7 @@ class WindowsBuilder:
Manufacturer="HIM-Tools" Manufacturer="HIM-Tools"
UpgradeCode="12345678-1234-1234-1234-123456789012"> UpgradeCode="12345678-1234-1234-1234-123456789012">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" Platform="x64" /> <Package InstallerVersion="200" Compressed="yes" InstallScope="perUser" Platform="x64" />
<Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" /> <Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
<!-- Required property for WixUI_InstallDir dialog set --> <!-- Required property for WixUI_InstallDir dialog set -->
@ -332,7 +332,7 @@ class WindowsBuilder:
</Feature> </Feature>
<Directory Id="TARGETDIR" Name="SourceDir"> <Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder"> <Directory Id="LocalAppDataFolder">
<Directory Id="INSTALLFOLDER" Name="WebDrop Bridge" /> <Directory Id="INSTALLFOLDER" Name="WebDrop Bridge" />
</Directory> </Directory>
<Directory Id="ProgramMenuFolder"> <Directory Id="ProgramMenuFolder">

View file

@ -284,7 +284,9 @@ class Config:
"""Save configuration to JSON file. """Save configuration to JSON file.
Args: Args:
config_path: Path to save configuration config_path: Path to save configuration to
Creates parent directories if they don't exist.
""" """
data = { data = {
"app_name": self.app_name, "app_name": self.app_name,

View file

@ -448,32 +448,72 @@ class MainWindow(QMainWindow):
config_code = self._generate_config_injection_script() config_code = self._generate_config_injection_script()
# Load bridge script from file # Load bridge script from file
# Using intercept script - prevents browser drag, hands off to Qt # Try multiple paths to support dev mode, PyInstaller bundle, and MSI installation
# Support both development mode and PyInstaller bundle # 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'): if hasattr(sys, '_MEIPASS'):
# Running as PyInstaller bundle search_paths.append(Path(sys._MEIPASS) / "webdrop_bridge" / "ui" / "bridge_script_intercept.js") # type: ignore
script_path = Path(sys._MEIPASS) / "webdrop_bridge" / "ui" / "bridge_script_intercept.js" # type: ignore
else: # 3. Installed executable's directory (handles MSI installation where all files are packaged together)
# Running in development mode exe_dir = Path(sys.executable).parent
script_path = Path(__file__).parent / "bridge_script_intercept.js" 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: 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: with open(script_path, 'r', encoding='utf-8') as f:
bridge_code = f.read() 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'): if hasattr(sys, '_MEIPASS'):
download_interceptor_path = Path(sys._MEIPASS) / "webdrop_bridge" / "ui" / "download_interceptor.js" # type: ignore download_search_paths.append(Path(sys._MEIPASS) / "webdrop_bridge" / "ui" / "download_interceptor.js") # type: ignore
else: download_search_paths.append(exe_dir / "webdrop_bridge" / "ui" / "download_interceptor.js")
download_interceptor_path = Path(__file__).parent / "download_interceptor.js"
download_interceptor_code = "" download_interceptor_code = ""
for path in download_search_paths:
if path.exists():
download_interceptor_path = path
break
if download_interceptor_path:
try: try:
with open(download_interceptor_path, 'r', encoding='utf-8') as f: with open(download_interceptor_path, 'r', encoding='utf-8') as f:
download_interceptor_code = f.read() download_interceptor_code = f.read()
logger.debug(f"Loaded download interceptor from {download_interceptor_path}") logger.debug(f"Loaded download interceptor from {download_interceptor_path}")
except (OSError, IOError) as e: except (OSError, IOError) as e:
logger.warning(f"Download interceptor not found: {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 # Combine: qwebchannel.js + config + bridge script + download interceptor
combined_code = qwebchannel_code + "\n\n" + config_code + "\n\n" + bridge_code combined_code = qwebchannel_code + "\n\n" + config_code + "\n\n" + bridge_code
@ -492,9 +532,13 @@ class MainWindow(QMainWindow):
script.setSourceCode(combined_code) script.setSourceCode(combined_code)
self.web_view.page().scripts().insert(script) 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: 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: def _generate_config_injection_script(self) -> str:
"""Generate JavaScript code that injects configuration. """Generate JavaScript code that injects configuration.

View file

@ -1,5 +1,6 @@
"""Settings dialog for configuration management.""" """Settings dialog for configuration management."""
import logging
from pathlib import Path from pathlib import Path
from typing import List, Optional from typing import List, Optional
@ -22,6 +23,9 @@ from PySide6.QtWidgets import (
from webdrop_bridge.config import Config, ConfigurationError from webdrop_bridge.config import Config, ConfigurationError
from webdrop_bridge.core.config_manager import ConfigExporter, ConfigProfile, ConfigValidator 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): class SettingsDialog(QDialog):
@ -76,6 +80,63 @@ class SettingsDialog(QDialog):
self.setLayout(layout) 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: def _create_paths_tab(self) -> QWidget:
"""Create paths configuration tab.""" """Create paths configuration tab."""
widget = QWidget() widget = QWidget()

View file

@ -154,6 +154,66 @@ def setup_logging(
return logger 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: def get_logger(name: str = __name__) -> logging.Logger:
"""Get a logger instance for a module. """Get a logger instance for a module.