AutoPercenty3/main.py

314 lines
12 KiB
Python

import multiprocessing as mp
import sys
import ctypes
import os
import time
import logging
import atexit
from PySide6.QtWidgets import QApplication, QMessageBox
from PySide6.QtGui import QIcon, QKeySequence, QShortcut
from sleep_control import prevent_sleep_mode, restore_sleep_mode
from login_dialog import LoginDialog
from mainUI_SP import MAIN_GUI
# from limited_gui import shuffleMAIN_GUI
from src.modules.settings_manager import SettingsManager
from loggerModule import Logger
from startup_guard import safe_create_log_dir, guard_startup, show_startup_error
# from src.loggerModules.loggerModule_structlog import StructlogImprovedLogger
from updateManager.__version__ import __file_log_level__, __gui_log_level__, __version__
from src.ui.global_style import apply_global_theme, toggle_global_theme
import socket
import re
import traceback as _traceback_mod
# Windows DPI Awareness Constants
DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4
# 윈도우 플랫폼 여부 확인
IS_WIN = sys.platform.startswith('win')
# ============================================================
# [보안] 패키징 환경에서 개발 PC 경로 노출 방지
# .pyc 파일에 빌드 시 개발 PC 경로가 co_filename 으로 박혀 있어,
# 예외 발생 시 traceback 에 해당 경로가 그대로 출력됩니다.
# 아래 핸들러는 유저에게 보이는 메시지에서 경로를 <app> 으로 치환하고,
# 로그 파일에는 원본 전체 내용을 남겨 디버깅이 가능하게 합니다.
# ============================================================
_DEV_PATH_PATTERN = re.compile(
r'(?:[A-Za-z]:\\|/)(?:[^"\n]+?[\\/])*([A-Za-z]:\\[^"\n]+|/[^"\n]+)',
re.IGNORECASE
)
def _sanitize_path(text: str) -> str:
"""traceback 문자열에서 절대 경로를 <app> 으로 치환합니다."""
return re.sub(
r'(?i)(?:[A-Za-z]:\\|/)(?:[^"\n<>|?*]+[\\/])+',
'<app>\\\\',
text
)
_original_excepthook = sys.excepthook
_app_logger_ref = None # main() 에서 logger 참조를 주입받음
def _safe_excepthook(exc_type, exc_value, exc_tb):
"""패키징 환경용 전역 예외 핸들러."""
lines = _traceback_mod.format_exception(exc_type, exc_value, exc_tb)
full_tb = ''.join(lines) # 원본 (경로 포함)
# 1) 로그 파일에는 원본 전체 기록
if _app_logger_ref is not None:
try:
_app_logger_ref.log(
f"[UnhandledException]\n{full_tb}",
level=logging.ERROR
)
except Exception:
pass
# 2) 콘솔/화면에는 경로를 가려서 출력
sanitized = _sanitize_path(full_tb)
print(sanitized, file=sys.stderr)
def install_safe_excepthook():
"""패키징 환경(frozen)일 때만 안전한 예외 핸들러를 설치합니다."""
if getattr(sys, 'frozen', False):
sys.excepthook = _safe_excepthook
def is_already_running(port=56387):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind(("127.0.0.1", port))
return False # 정상 바인딩 = 최초 실행
except OSError:
return True # 이미 바인딩 되어 있음 = 실행 중
def run_single_instance_check(logger):
# 포트 / Mutex 두 방식 모두 여기로 이동
if is_already_running():
app = QApplication([])
logger.log("이미 프로그램이 실행 중입니다! - 중복실행", level=logging.INFO)
QMessageBox.warning(None, "중복 실행", "이미 프로그램이 실행 중입니다!")
sys.exit(0)
# Mutex 방식 (Windows에서만)
if IS_WIN:
try:
import win32event, win32api, winerror
mutex = win32event.CreateMutex(None, False, "Edit_PartTimer3Mutex")
if win32api.GetLastError() == winerror.ERROR_ALREADY_EXISTS:
app = QApplication([])
QMessageBox.warning(None, "중복 실행", "이미 프로그램이 실행 중입니다!")
sys.exit(0)
except ImportError:
logger.log("win32 모듈을 찾을 수 없습니다. Mutex 사용 불가", level=logging.WARNING)
def set_dpi_awareness():
"""애플리케이션 DPI 설정 (Windows에서만)"""
if IS_WIN:
try:
ctypes.windll.shcore.SetProcessDpiAwareness(1) # 프로세스 DPI 인식 설정
except Exception:
pass # 실패할 경우 무시
# COM 초기화·해제 헬퍼 (Windows에서만)
def initialize_com():
"""Win32 OLE(COM) 라이브러리 초기화"""
if IS_WIN:
ctypes.windll.ole32.OleInitialize(None)
def uninitialize_com():
"""Win32 OLE(COM) 라이브러리 해제"""
if IS_WIN:
ctypes.windll.ole32.OleUninitialize()
def get_application_path():
"""
애플리케이션 실행 경로를 반환합니다.
개발 환경과 패키지 배포 환경 모두에서 작동합니다.
"""
if getattr(sys, 'frozen', False): # 패키징된 경우
base_dir = os.path.dirname(sys.executable)
internal_dir = os.path.join(base_dir, 'lib', 'src') # lib 디렉토리 포함
if os.path.exists(internal_dir): # lib 디렉토리가 존재하면 base_dir로 설정
return internal_dir
else: # 일반 Python 실행 환경
base_dir = os.path.dirname(os.path.abspath(__file__))
debug_dir = os.path.join(base_dir, 'src') # lib 디렉토리 포함
return debug_dir
def setup_logging():
"""
로그 디렉토리와 파일을 설정하고 경로를 반환합니다.
- 1순위: 앱 설치 경로 내 logs/ 폴더
- 2순위: %LOCALAPPDATA%\\Edit_PartTimer3\\logs\ (권한 없을 때 자동 폴백)
- 모두 실패: 관리자 권한 안내 메시지 표시 후 종료
"""
base_path = get_application_path()
preferred_logs_dir = os.path.join(base_path, "logs")
# startup_guard 를 통해 안전하게 디렉토리 생성 (폴백 + 권한 오류 안내 포함)
logs_dir = safe_create_log_dir(preferred_logs_dir)
log_file_path = os.path.join(logs_dir, "Edit_PartTimer3.log")
return {
"logs_dir": logs_dir,
"log_file_path": log_file_path
}
@guard_startup
def main():
# 패키징 환경 경로 노출 방지 핸들러 설치 (frozen 일 때만 작동)
install_safe_excepthook()
# 로그 설정
log_paths = setup_logging()
# 로거 초기화 - 경로 정보 활용
logger = Logger(
log_file=log_paths["log_file_path"],
logger_name="EP3_Logger",
file_log_level=__file_log_level__
)
# 예외 핸들러에 로거 주입 (로그 파일에 원본 traceback 기록용)
global _app_logger_ref
_app_logger_ref = logger
# try:
# if mp.current_process().name == "MainProcess":
# run_single_instance_check(logger)
# except ImportError:
# pass
logger.log("===== 프로그램 시작 =====", level=logging.INFO)
logger.log(f"로그 파일 경로: {log_paths['log_file_path']}", level=logging.INFO)
settings_manager = SettingsManager(logger=logger, organization="WhenRideMycar", application="EditPartTimer3")
# DPI 관련 경고 제거 (Qt 기본값 설정)
set_dpi_awareness()
# ▶ COM 초기화 (한 번만 실행)
initialize_com()
"""프로그램 시작점"""
app = QApplication([])
# 전역 라이트 테마 강제 적용 (OS 다크/라이트 설정 무시)
apply_global_theme(app)
# 애플리케이션 아이콘 설정
try:
app_path = get_application_path()
icon_path = os.path.join(app_path, "Edit_PartTimer3.ico")
if os.path.exists(icon_path):
app.setWindowIcon(QIcon(icon_path))
logger.log(f"애플리케이션 아이콘 설정: {icon_path}", level=logging.INFO)
else:
logger.log(f"아이콘 파일을 찾을 수 없습니다: {icon_path}", level=logging.WARNING)
except Exception as e:
logger.log(f"아이콘 설정 실패: {e}", level=logging.ERROR)
# 슬립 방지 활성화
prevent_sleep_mode()
# 업데이트 여부 체크
changelog = ''
# 로그인 다이얼로그 실행 (모달)
login_dialog = LoginDialog(logger, settings_manager)
if login_dialog.exec() == LoginDialog.Accepted:
user_info = login_dialog.get_user_info()
supabase_manager = login_dialog.get_supabase_manager()
system_info = login_dialog.get_system_info()
# run_type = login_dialog.get_run_type() # <- 추가
update_selection_data = login_dialog.get_update_selection_data() # 업데이트 선택 정보 추가
# logger.log(f"run_type: {run_type}", level=logging.INFO)
logger.log(f"update_selection: {update_selection_data}", level=logging.DEBUG)
# run_type 값에 따라 분기
# if run_type == "편집알바생":
# 알바생소집: 기존 메인UI 실행
mainWindow = MAIN_GUI(
logger=logger,
user_info=user_info,
supabase_manager=supabase_manager,
settings_manager=settings_manager,
system_info=system_info,
update_log=changelog,
app=app,
version=__version__,
log_paths=log_paths
)
logger.log("편집알바생 실행", level=logging.INFO)
# if run_type == "업로드알바생":
# mainWindow = shuffleMAIN_GUI(
# logger=logger,
# user_info=user_info,
# supabase_manager=supabase_manager,
# settings_manager=settings_manager,
# system_info=system_info,
# update_log=changelog,
# app=app,
# version=__version__,
# log_paths=log_paths
# )
# logger.log("업로드알바생 실행", level=logging.INFO)
else:
sys.exit("로그인 실패 또는 취소되었습니다.")
# 윈도우에도 아이콘 설정
try:
app_path = get_application_path()
icon_path = os.path.join(app_path, "Edit_PartTimer3.ico")
if os.path.exists(icon_path):
app_icon = QIcon(icon_path)
mainWindow.setWindowIcon(app_icon)
logger.log("메인 윈도우에 아이콘 설정 완료", level=logging.INFO)
except Exception as e:
logger.log(f"메인 윈도우 아이콘 설정 실패: {e}", level=logging.ERROR)
# 애플리케이션 종료 시 세션 정리 코드 추가
def cleanup_on_exit():
# 프로그램 종료 시 COM 해제
atexit.register(uninitialize_com)
# 세션 종료 처리
try:
logger.log(f"프로그램 종료: 현재 세션 종료 중 ...", level=logging.INFO)
# 세션 종료
supabase_manager.close_session()
# # 저장된 세션 ID 제거
# settings_manager.remove_session_id()
logger.log("세션이 성공적으로 종료되었습니다.", level=logging.INFO)
except Exception as e:
logger.log(f"세션 종료 오류: {e}", level=logging.ERROR)
# 슬립 모드 복원
restore_sleep_mode()
# 애플리케이션 종료 이벤트 연결
app.aboutToQuit.connect(cleanup_on_exit)
# 전역 테마 토글 단축키 (Ctrl+Alt+T)
try:
theme_shortcut = QShortcut(QKeySequence("Ctrl+Alt+T"), mainWindow)
theme_shortcut.setContext(3) # Qt.ApplicationShortcut
theme_shortcut.activated.connect(lambda: toggle_global_theme(app))
except Exception:
pass
mainWindow.show()
sys.exit(app.exec())
if __name__ == "__main__":
import multiprocessing as mp
mp.freeze_support() # ← 추가
main()