KINDNICK d1f6791b96 Embed updater.exe into main.exe (single-file deployment)
- main.spec: include dist/updater.exe in datas (when present)
- main.py: extract embedded updater.exe next to main.exe on launch
- updater_client.py: _MEIPASS fallback to TEMP if extraction fails
- release.ps1: build updater FIRST so main.spec can embed it

Now users can deploy main.exe alone; auto-update still works because
main.exe self-extracts updater.exe on first launch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 13:53:31 +09:00

171 lines
5.1 KiB
Python

"""
Clock-out Time Calculator
퇴근시간 계산 프로그램 - 메인 실행 파일
"""
import sys
import os
# PyQt5 임포트
from PyQt5.QtWidgets import QApplication, QMessageBox
from PyQt5.QtGui import QFont
from PyQt5.QtCore import QLockFile, QDir
from PyQt5.QtNetwork import QLocalServer, QLocalSocket
# 프로젝트 루트를 경로에 추가
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from ui.main_window import MainWindow
from core.database import Database
def _ensure_updater_extracted():
"""main.exe에 내장된 updater.exe를 같은 폴더에 추출.
main.exe만 단독 배포해도 자동 업데이트가 동작하도록, 시작 시 한 번
내장본을 같은 폴더로 복사. 이미 같은 사이즈로 있으면 건너뜀.
개발 환경(.py 실행)에서는 별도 빌드된 updater.exe를 사용해야 하므로 no-op.
"""
if not getattr(sys, 'frozen', False):
return
import shutil
from pathlib import Path
main_dir = Path(sys.executable).parent
target = main_dir / 'updater.exe'
bundled = Path(getattr(sys, '_MEIPASS', '')) / 'updater.exe'
if not bundled.exists():
return # 빌드 시 포함 안 됨
needs_extract = (not target.exists()
or target.stat().st_size != bundled.stat().st_size)
if needs_extract:
try:
shutil.copy2(str(bundled), str(target))
except OSError:
pass # 권한 부족 등은 silent — 자동 업데이트만 안 됨
def check_requirements():
"""필수 요구사항 확인"""
try:
import win32evtlog
return True
except ImportError:
return False
def main():
"""메인 함수"""
# PyInstaller frozen exe에 내장된 updater.exe를 옆 폴더에 자동 추출
_ensure_updater_extracted()
app = QApplication(sys.argv)
# 애플리케이션 정보
app.setApplicationName("Clock-out Time Calculator")
app.setOrganizationName("DevUtil")
app.setApplicationVersion("1.0.0")
# 중복 실행 방지 - 로컬 서버로 체크
server_name = "ClockOutCalculatorInstance"
# 이미 실행 중인지 확인
socket = QLocalSocket()
socket.connectToServer(server_name)
if socket.waitForConnected(500):
# 이미 실행 중 - 기존 인스턴스에 "show" 신호 전송
socket.write(b"show")
socket.flush()
socket.waitForBytesWritten(1000)
socket.disconnectFromServer()
return 0
# 새로운 인스턴스 - 서버 시작
server = QLocalServer()
# 기존 서버가 남아있을 수 있으므로 제거
QLocalServer.removeServer(server_name)
if not server.listen(server_name):
QMessageBox.warning(
None,
"서버 오류",
"프로그램 인스턴스 서버를 시작할 수 없습니다."
)
return 1
# 폰트 설정
app.setFont(QFont("Segoe UI", 9))
# 필수 패키지 확인
if not check_requirements():
QMessageBox.critical(
None,
"요구사항 오류",
"필수 패키지가 설치되지 않았습니다.\n\n"
"다음 명령어를 실행하세요:\n"
"pip install -r requirements.txt"
)
return 1
# 데이터베이스 초기화 — db_path_override 설정 시 그 경로 사용 (클라우드 폴더 등)
# 부트스트랩: 기본 DB로 한 번 열어 override 키 확인
from core.settings_keys import DB_PATH_OVERRIDE
bootstrap = Database()
override_path = bootstrap.get_setting(DB_PATH_OVERRIDE, '') or ''
if override_path and os.path.exists(os.path.dirname(override_path) or '.'):
db = Database(override_path)
else:
db = bootstrap
# 1일 1회 자동 백업 (조용히 실패 — 백업 실패가 앱 실행을 막으면 안 됨)
try:
from utils.backup import backup_db_if_needed
backup_db_if_needed(db)
except Exception as e:
from utils.debug_log import dlog
dlog(f"backup failed: {e}")
# 메인 윈도우 생성 및 표시
try:
window = MainWindow()
# 서버 연결 처리 - 다른 인스턴스에서 show 신호를 받으면 창을 보여줌
def on_new_connection():
client_socket = server.nextPendingConnection()
if client_socket:
client_socket.waitForReadyRead(1000)
data = client_socket.readAll().data()
if data == b"show":
# 창 표시
window.show()
window.raise_()
window.activateWindow()
client_socket.disconnectFromServer()
server.newConnection.connect(on_new_connection)
window.show()
result = app.exec_()
# 서버 종료
server.close()
return result
except Exception as e:
QMessageBox.critical(
None,
"오류",
f"프로그램 실행 중 오류가 발생했습니다:\n\n{str(e)}"
)
server.close()
return 1
if __name__ == "__main__":
sys.exit(main())