314 lines
12 KiB
Python
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()
|
|
|