218 lines
7.4 KiB
Python
218 lines
7.4 KiB
Python
import os
|
|
import sys
|
|
import threading
|
|
import time
|
|
from typing import Optional
|
|
import json
|
|
|
|
import pystray
|
|
from PIL import Image, ImageDraw
|
|
import requests
|
|
|
|
from loggerModule import Logger
|
|
|
|
# 상위 디렉토리를 sys.path에 추가하여 updater 모듈을 찾을 수 있게 함
|
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
parent_dir = os.path.dirname(current_dir)
|
|
if parent_dir not in sys.path:
|
|
sys.path.insert(0, parent_dir)
|
|
|
|
try:
|
|
from updater.__version__ import __version__ as app_version
|
|
except ImportError:
|
|
app_version = "unknown"
|
|
|
|
|
|
class TrayController:
|
|
def __init__(self):
|
|
logs_root = os.path.join(os.environ.get("PROGRAMDATA", r"C:\\ProgramData"), "ImgWorker")
|
|
os.makedirs(logs_root, exist_ok=True)
|
|
self.logger = Logger(log_file=os.path.join(logs_root, "tray.log"), logger_name="img_worker_tray")
|
|
self.api = _TrayAPI(self.logger)
|
|
self.icon: Optional[pystray.Icon] = None
|
|
self._stop_event = threading.Event()
|
|
self._last_ready: bool = False
|
|
|
|
def _create_image(self, color=(0, 120, 215)):
|
|
img = Image.new('RGB', (64, 64), color)
|
|
draw = ImageDraw.Draw(img)
|
|
draw.rectangle([8, 8, 56, 56], outline=(255, 255, 255), width=3)
|
|
draw.ellipse([22, 22, 42, 42], fill=(255, 255, 255))
|
|
return img
|
|
|
|
def _set_icon_color(self, state: str):
|
|
# state: 'ready' -> green, 'stopped' -> red, 'active' -> blink green/yellow
|
|
green = (0, 160, 0)
|
|
red = (180, 0, 0)
|
|
yellow = (200, 160, 0)
|
|
color = red
|
|
if state == 'ready':
|
|
color = green
|
|
elif state == 'active':
|
|
# 깜박임은 폴링 스레드에서 교차로 변경
|
|
color = green if int(time.time()) % 2 == 0 else yellow
|
|
if self.icon:
|
|
self.icon.icon = self._create_image(color)
|
|
|
|
def _update_title(self):
|
|
try:
|
|
st = self.api.worker_status()
|
|
ready = bool(st.get("ready", False))
|
|
active = bool(st.get("active", False))
|
|
avg_sec = st.get("avg_sec_per_image")
|
|
provider = (st.get("provider") or "").upper()
|
|
pid = st.get("pid")
|
|
self._last_ready = ready
|
|
status_label = 'ACTIVE' if active else ('READY' if ready else 'STOP')
|
|
perf = f" - avg. {avg_sec:.1f}(sec/장)" if isinstance(avg_sec, (int, float)) else ''
|
|
prov = f" [{provider}]" if provider else ''
|
|
title = f"ImgWorker v{app_version} [{status_label}]{perf}{prov}"
|
|
if self.icon:
|
|
self.icon.title = title
|
|
# 메뉴를 상태에 맞게 재구성
|
|
self.icon.menu = self._build_menu()
|
|
try:
|
|
self.icon.update_menu()
|
|
except Exception:
|
|
pass
|
|
self._set_icon_color('active' if active else ('ready' if ready else 'stopped'))
|
|
except Exception:
|
|
if self.icon:
|
|
self.icon.title = "ImgWorker (offline)"
|
|
|
|
def _build_menu(self) -> pystray.Menu:
|
|
status_text = f"워커 상태: {'실행 중' if self._last_ready else '중지'}"
|
|
# 상태 라벨(비활성), 시작/중지 버튼은 상태에 따라 enable
|
|
return pystray.Menu(
|
|
pystray.MenuItem(status_text, lambda: None, enabled=False),
|
|
pystray.MenuItem("워커 시작", self._action_start, enabled=(not self._last_ready)),
|
|
pystray.MenuItem("워커 중지", self._action_stop, enabled=self._last_ready),
|
|
pystray.MenuItem("로그 열기", self._action_open_log),
|
|
pystray.Menu.SEPARATOR,
|
|
pystray.MenuItem("상태 새로고침", self._action_info),
|
|
pystray.MenuItem("서버 종료", self._action_exit),
|
|
)
|
|
|
|
def _action_start(self, icon, item):
|
|
self.logger.info("Tray: start worker")
|
|
self.api.worker_start()
|
|
time.sleep(0.3)
|
|
self._update_title()
|
|
|
|
def _action_stop(self, icon, item):
|
|
self.logger.info("Tray: stop worker")
|
|
self.api.worker_stop()
|
|
time.sleep(0.3)
|
|
self._update_title()
|
|
|
|
def _action_info(self, icon, item):
|
|
self.logger.info("Tray: info refresh")
|
|
self._update_title()
|
|
|
|
def _action_exit(self, icon, item):
|
|
self.logger.info("Tray: exit clicked")
|
|
try:
|
|
self.api.shutdown_server()
|
|
except Exception:
|
|
pass
|
|
self._stop_event.set()
|
|
if self.icon:
|
|
self.icon.stop()
|
|
|
|
def _action_open_log(self, icon, item):
|
|
try:
|
|
logs_root = os.path.join(os.environ.get("PROGRAMDATA", r"C:\\ProgramData"), "ImgWorker")
|
|
log_path = os.path.join(logs_root, "logs", "api_server.log")
|
|
# 기본 편집기로 열기
|
|
if os.name == "nt":
|
|
os.startfile(log_path) # type: ignore[attr-defined]
|
|
else:
|
|
import subprocess
|
|
subprocess.Popen(["xdg-open", log_path])
|
|
except Exception as e:
|
|
self.logger.error(f"로그 열기 실패: {e}")
|
|
|
|
def run(self):
|
|
self.icon = pystray.Icon("imgworker_tray", self._create_image(), "ImgWorker", self._build_menu())
|
|
|
|
# 상태 폴링 쓰레드로 제목/메뉴 갱신
|
|
def _poll():
|
|
while not self._stop_event.is_set():
|
|
try:
|
|
self._update_title()
|
|
except Exception:
|
|
pass
|
|
time.sleep(2.0)
|
|
|
|
t = threading.Thread(target=_poll, daemon=True)
|
|
t.start()
|
|
# 최초 상태 반영
|
|
self._update_title()
|
|
self.icon.run()
|
|
|
|
|
|
def main():
|
|
TrayController().run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|
|
|
|
|
|
def _read_server_info() -> Optional[dict]:
|
|
try:
|
|
program_data = os.environ.get("PROGRAMDATA", r"C:\\ProgramData")
|
|
info_path = os.path.join(program_data, "ImgWorker", "server.json")
|
|
if os.path.isfile(info_path):
|
|
with open(info_path, "r", encoding="utf-8") as f:
|
|
return json.load(f)
|
|
except Exception:
|
|
return None
|
|
return None
|
|
|
|
|
|
class _TrayAPI:
|
|
def __init__(self, logger: Logger):
|
|
self.logger = logger
|
|
base = os.environ.get("IMGWK_API_BASE", "")
|
|
if not base:
|
|
info = _read_server_info() or {}
|
|
base = info.get("base") or f"http://{info.get('host','127.0.0.1')}:{info.get('port',8009)}"
|
|
if not base:
|
|
base = "http://127.0.0.1:8009"
|
|
self.base = base.rstrip("/")
|
|
|
|
def worker_status(self) -> dict:
|
|
try:
|
|
r = requests.get(f"{self.base}/v1/worker/status", timeout=5)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except Exception as e:
|
|
return {"ready": False, "error": str(e)}
|
|
|
|
def worker_start(self) -> dict:
|
|
try:
|
|
r = requests.post(f"{self.base}/v1/worker/start", timeout=10)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except Exception as e:
|
|
return {"ok": False, "error": str(e)}
|
|
|
|
def worker_stop(self) -> dict:
|
|
try:
|
|
r = requests.post(f"{self.base}/v1/worker/stop", timeout=10)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except Exception as e:
|
|
return {"ok": False, "error": str(e)}
|
|
|
|
def shutdown_server(self) -> dict:
|
|
try:
|
|
r = requests.post(f"{self.base}/v1/server/shutdown", timeout=5)
|
|
r.raise_for_status()
|
|
return r.json()
|
|
except Exception as e:
|
|
return {"ok": False, "error": str(e)}
|
|
|
|
|