Refactor windowed mode and subprocess handling

Removed redundant windowed mode setup from main.py and simplified subprocess creation flags logic across core and GUI modules. Centralized subprocess creation flags usage, eliminated custom run_subprocess_hidden wrapper, and improved logging handler setup for better compatibility with windowed applications.
This commit is contained in:
Your Name
2025-08-27 23:00:08 +03:00
parent b23be4335e
commit f390d23f54
5 changed files with 35 additions and 169 deletions
-46
View File
@@ -1,50 +1,4 @@
import sys import sys
import os
# Early detection and handling of windowed mode to prevent console window flicker
def setup_windowed_mode():
"""
Setup windowed mode handling to prevent console window flickering.
This must be called as early as possible in the application startup.
"""
if getattr(sys, 'frozen', False):
# Running as compiled executable (PyInstaller)
if sys.stdout is None or sys.stderr is None:
# Windowed mode - completely suppress console output
import io
null_stream = io.StringIO()
if sys.stdout is None:
sys.stdout = null_stream
if sys.stderr is None:
sys.stderr = null_stream
else:
# Console mode but compiled - redirect to null to prevent window flicker
if os.name == 'nt': # Windows
try:
import io
# Create a null output stream
null_stream = io.StringIO()
# Only redirect if we detect this might cause console window issues
# Check if we're not already in a console
try:
# Try to get console window handle - if this fails, we're likely windowed
import ctypes
kernel32 = ctypes.windll.kernel32
console_window = kernel32.GetConsoleWindow()
if console_window == 0:
# No console window exists, redirect to prevent one from appearing
sys.stdout = null_stream
sys.stderr = null_stream
except:
# If console detection fails, err on the side of caution
sys.stdout = null_stream
sys.stderr = null_stream
except:
# If all else fails, continue normally
pass
# Call windowed mode setup immediately
setup_windowed_mode()
from PySide6.QtWidgets import QApplication, QMessageBox from PySide6.QtWidgets import QApplication, QMessageBox
+12 -27
View File
@@ -94,25 +94,9 @@ def setup_logging():
# Console handler - INFO and above, with colors # Console handler - INFO and above, with colors
# Check if stdout is available (it might be None in PyInstaller windowed apps) # Check if stdout is available (it might be None in PyInstaller windowed apps)
# Also check if we're running in a windowed mode (likely PyInstaller --windowed)
stdout_available = sys.stdout is not None stdout_available = sys.stdout is not None
is_windowed_app = getattr(sys, 'frozen', False) and sys.stdout is None
# Additional check for windowed mode - check if console window exists on Windows if stdout_available:
is_console_hidden = False
if sys.platform == 'win32' and getattr(sys, 'frozen', False):
try:
import ctypes
kernel32 = ctypes.windll.kernel32
console_window = kernel32.GetConsoleWindow()
if console_window == 0:
is_console_hidden = True
except:
# If console detection fails, assume we might be windowed
is_console_hidden = True
# Skip console output if we're in a windowed application or console is hidden
if stdout_available and not is_windowed_app and not is_console_hidden:
try: try:
logger.add( logger.add(
sys.stdout, sys.stdout,
@@ -133,13 +117,13 @@ def setup_logging():
except Exception: except Exception:
stdout_available = False stdout_available = False
# If stdout is not available or we're in windowed mode, skip stderr too to prevent console windows # If stdout is not available, try stderr or skip console logging entirely
if (not stdout_available or is_windowed_app or is_console_hidden) and not (is_windowed_app or is_console_hidden): if not stdout_available:
try: try:
if sys.stderr is not None: if sys.stderr is not None:
logger.add( logger.add(
sys.stderr, sys.stderr,
level="ERROR", # Only errors to stderr to minimize console window appearance level="WARNING", # Only warnings and errors to stderr
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}", format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {message}",
catch=True, catch=True,
) )
@@ -175,21 +159,22 @@ def setup_logging():
# If file logging fails, just log to console # If file logging fails, just log to console
logger.warning(f"Could not set up file logging: {e}") logger.warning(f"Could not set up file logging: {e}")
# Log startup message # Log startup message if we have any handlers
try: if logger._core.handlers:
logger.info("YTSage logging system initialized") logger.info("YTSage logging system initialized")
if log_dir and log_dir.exists(): if log_dir and log_dir.exists():
logger.debug(f"Log directory: {log_dir}") logger.debug(f"Log directory: {log_dir}")
else: else:
logger.warning("File logging disabled - could not create log directory") logger.warning("File logging disabled - could not create log directory")
except Exception:
# If logging fails, continue silently
pass
# If no handlers were successfully added, add a null handler to prevent errors # If no handlers were successfully added, add a null handler to prevent errors
try: if not logger._core.handlers:
# Try to add a minimal fallback handler if needed # Add a minimal handler that just discards messages
# This prevents loguru from complaining about no handlers
import tempfile import tempfile
try:
# Try to add a temporary file handler as last resort
temp_log = Path(tempfile.gettempdir()) / "ytsage_temp.log" temp_log = Path(tempfile.gettempdir()) / "ytsage_temp.log"
logger.add(temp_log, level="ERROR", catch=True) logger.add(temp_log, level="ERROR", catch=True)
except Exception: except Exception:
+11 -43
View File
@@ -4,12 +4,8 @@ import subprocess
import sys import sys
import tempfile import tempfile
import time import time
import warnings
from pathlib import Path from pathlib import Path
# Suppress the pkg_resources deprecation warning to prevent console window flicker
warnings.filterwarnings("ignore", message=".*pkg_resources is deprecated.*", category=UserWarning)
import pkg_resources import pkg_resources
import requests import requests
from packaging import version from packaging import version
@@ -36,37 +32,6 @@ _version_cache = {
CACHE_EXPIRY = 300 CACHE_EXPIRY = 300
def run_subprocess_hidden(cmd_args, **kwargs):
"""
Run subprocess with proper console window hiding for windowed applications.
Args:
cmd_args: Command arguments list
**kwargs: Additional subprocess.run arguments
Returns:
subprocess.CompletedProcess result
"""
# Set up creation flags to hide console window
creation_flags = SUBPROCESS_CREATIONFLAGS
if getattr(sys, 'frozen', False):
# Running as compiled executable - use additional flags to prevent console flicker
creation_flags |= subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
# Set default parameters to suppress console
default_kwargs = {
'capture_output': True,
'text': True,
'creationflags': creation_flags,
'stdin': subprocess.DEVNULL
}
# Merge with provided kwargs (user kwargs take precedence)
default_kwargs.update(kwargs)
return subprocess.run(cmd_args, **default_kwargs)
def get_file_mtime(filepath) -> float: def get_file_mtime(filepath) -> float:
"""Get file modification time safely.""" """Get file modification time safely."""
try: try:
@@ -227,8 +192,8 @@ def get_ytdlp_version_direct(yt_dlp_path=None) -> str:
return "Not found" return "Not found"
# Extra logic moved to src\utils\ytsage_constants.py # Extra logic moved to src\utils\ytsage_constants.py
result = run_subprocess_hidden( result = subprocess.run(
[yt_dlp_path, "--version"], timeout=10 [yt_dlp_path, "--version"], capture_output=True, text=True, timeout=10, creationflags=SUBPROCESS_CREATIONFLAGS
) )
if result.returncode == 0: if result.returncode == 0:
@@ -244,8 +209,8 @@ def get_ffmpeg_version_direct() -> str:
"""Get FFmpeg version directly without caching.""" """Get FFmpeg version directly without caching."""
try: try:
# Extra logic moved to src\utils\ytsage_constants.py # Extra logic moved to src\utils\ytsage_constants.py
result = run_subprocess_hidden( result = subprocess.run(
["ffmpeg", "-version"], timeout=10 ["ffmpeg", "-version"], capture_output=True, text=True, timeout=10, creationflags=SUBPROCESS_CREATIONFLAGS
) )
if result.returncode == 0: if result.returncode == 0:
@@ -273,8 +238,8 @@ def get_ffmpeg_version_direct() -> str:
ffmpeg_exe = Path(ffmpeg_path).joinpath("ffmpeg") ffmpeg_exe = Path(ffmpeg_path).joinpath("ffmpeg")
if ffmpeg_exe.exists(): if ffmpeg_exe.exists():
result = run_subprocess_hidden( result = subprocess.run(
[ffmpeg_exe, "-version"], timeout=10 [ffmpeg_exe, "-version"], capture_output=True, text=True, timeout=10, creationflags=SUBPROCESS_CREATIONFLAGS
) )
if result.returncode == 0: if result.returncode == 0:
@@ -520,7 +485,7 @@ def update_yt_dlp() -> bool:
# Compare versions and update if needed # Compare versions and update if needed
if version.parse(latest_version) > version.parse(current_version): if version.parse(latest_version) > version.parse(current_version):
logger.info(f"Updating yt-dlp from {current_version} to {latest_version}...") logger.info(f"Updating yt-dlp from {current_version} to {latest_version}...")
update_result = run_subprocess_hidden( update_result = subprocess.run(
[ [
sys.executable, sys.executable,
"-m", "-m",
@@ -529,7 +494,10 @@ def update_yt_dlp() -> bool:
"--upgrade", "--upgrade",
"yt-dlp", "yt-dlp",
], ],
check=False capture_output=True,
text=True,
check=False,
creationflags=SUBPROCESS_CREATIONFLAGS,
) )
if update_result.returncode == 0: if update_result.returncode == 0:
logger.info("yt-dlp successfully updated") logger.info("yt-dlp successfully updated")
+4 -30
View File
@@ -1,7 +1,6 @@
import os import os
import shutil import shutil
import subprocess import subprocess
import sys
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -501,22 +500,10 @@ def check_ytdlp_binary() -> Optional[Path]:
# Use subprocess to check if yt-dlp is available # Use subprocess to check if yt-dlp is available
if OS_NAME == "Windows": if OS_NAME == "Windows":
# On Windows, use 'where' command and hide console window # On Windows, use 'where' command and hide console window
# For windowed applications, use additional flags to prevent console window flicker # Extra logic moved to src\utils\ytsage_constants.py
creation_flags = SUBPROCESS_CREATIONFLAGS
if getattr(sys, 'frozen', False):
# Running as compiled executable - use additional flags
creation_flags |= subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
result = subprocess.run( result = subprocess.run(
["where", "yt-dlp"], ["where", "yt-dlp"], capture_output=True, text=True, check=False, creationflags=SUBPROCESS_CREATIONFLAGS
capture_output=True,
text=True,
check=False,
creationflags=creation_flags,
# Additional parameters to suppress console
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
) )
if result.returncode == 0 and result.stdout.strip(): if result.returncode == 0 and result.stdout.strip():
yt_dlp_path = result.stdout.strip().split("\n")[0] yt_dlp_path = result.stdout.strip().split("\n")[0]
@@ -547,22 +534,9 @@ def check_ytdlp_installed() -> bool:
if ytdlp_path: if ytdlp_path:
# Try to run yt-dlp --version to verify it's working # Try to run yt-dlp --version to verify it's working
try: try:
# For windowed applications, use additional flags to prevent console window flicker # Extra logic moved to src\utils\ytsage_constants.py
creation_flags = SUBPROCESS_CREATIONFLAGS
if getattr(sys, 'frozen', False):
# Running as compiled executable - use additional flags
creation_flags |= subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
result = subprocess.run( result = subprocess.run(
[ytdlp_path, "--version"], [ytdlp_path, "--version"], capture_output=True, text=True, timeout=5, creationflags=SUBPROCESS_CREATIONFLAGS
capture_output=True,
text=True,
timeout=5,
creationflags=creation_flags,
# Additional parameters to suppress console
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
) )
return result.returncode == 0 return result.returncode == 0
except Exception: except Exception:
+1 -16
View File
@@ -1,6 +1,5 @@
import json import json
import subprocess import subprocess
import sys
import threading import threading
import webbrowser import webbrowser
from pathlib import Path from pathlib import Path
@@ -1736,21 +1735,7 @@ class YTSageApp(QMainWindow, FormatTableMixin, VideoInfoMixin): # Inherit from
# Execute command with hidden console window on Windows # Execute command with hidden console window on Windows
# Extra logic moved to src\utils\ytsage_constants.py # Extra logic moved to src\utils\ytsage_constants.py
creation_flags = SUBPROCESS_CREATIONFLAGS result = subprocess.run(cmd, capture_output=True, text=True, timeout=60, creationflags=SUBPROCESS_CREATIONFLAGS)
if getattr(sys, 'frozen', False):
# Running as compiled executable - use additional flags to prevent console flicker
creation_flags |= subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=60,
creationflags=creation_flags,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
if result.returncode != 0: if result.returncode != 0:
raise Exception(f"yt-dlp failed: {result.stderr}") raise Exception(f"yt-dlp failed: {result.stderr}")