diff --git a/main.py b/main.py index 618c522..ba8f5f4 100644 --- a/main.py +++ b/main.py @@ -27,6 +27,7 @@ from pydantic import BaseModel, Field from loggerModule import Logger from modules.image_worker import worker_main +from updater.__version__ import __version__ as app_version @@ -238,6 +239,35 @@ def _cleanup_runtime_files(): pass +def _kill_proc_tree(pid: int): + """자식 프로세스까지 포함하여 프로세스 트리를 강제 종료(KILL).""" + try: + if not pid: + return + parent = psutil.Process(pid) + children = parent.children(recursive=True) + + # 자식들 먼저 KILL + for child in children: + try: + child.kill() + except psutil.NoSuchProcess: + pass + + # 부모 KILL + try: + parent.kill() + except psutil.NoSuchProcess: + pass + + # 확실한 정리를 위해 잠시 대기 + psutil.wait_procs(children + [parent], timeout=3) + except psutil.NoSuchProcess: + pass + except Exception: + pass + + def _safe_rmtree_contents(target_dir: str, older_than_sec: int = 0): """대상 디렉터리 하위의 항목을 삭제. @@ -595,6 +625,10 @@ class WorkerManager: if self._proc is None: return self._running.clear() + + # PID 백업 (join 후 객체 상태와 무관하게 정리하기 위함) + pid = self._proc.pid + # 워커에게 종료 신호(None) try: self._task_q.put(None, timeout=1) @@ -602,11 +636,14 @@ class WorkerManager: pass # 프로세스 종료 대기 self._proc.join(timeout=10) - if self._proc.is_alive(): + + # 프로세스 트리 전체 확실한 정리 + if pid: try: - os.kill(self._proc.pid, signal.SIGTERM) + _kill_proc_tree(pid) except Exception: pass + self.logger.info("워커 프로세스 종료") finally: self._proc = None @@ -1050,12 +1087,14 @@ class WorkerManager: self._task_q.put(None, timeout=1) except Exception: pass - # 2) 종료 대기 + # 2) 종료 대기 및 강제 정리 if self._proc is not None: + pid = self._proc.pid self._proc.join(timeout=30) - if self._proc.is_alive(): + if pid: try: - os.kill(self._proc.pid, signal.SIGTERM) + # 롤링 시에도 확실하게 트리 정리 + _kill_proc_tree(pid) except Exception: pass except Exception as e: @@ -1147,7 +1186,7 @@ async def lifespan(app: FastAPI): _cleanup_runtime_files() -app = FastAPI(title="Image Worker API", version="1.0.0", lifespan=lifespan) +app = FastAPI(title="Image Worker API", version=app_version, lifespan=lifespan) @app.get("/health") diff --git a/modules/image_processor3.py b/modules/image_processor3.py index 719ee95..e13c5c9 100644 --- a/modules/image_processor3.py +++ b/modules/image_processor3.py @@ -261,6 +261,9 @@ class ImageProcessor3: # 인페인팅 실행 정보(마지막 사용 방식/장치) 추적용 내부 상태 self._last_inpaint_used = None self._last_inpaint_device = None + + # 외부 서버 헬스 체크 플래그 + self.is_external_server_alive = False except Exception as e: self.logger.log(f"ImageProcessor3 초기화 중 치명적 오류 발생: {e}", level=logging.ERROR, exc_info=True) @@ -409,6 +412,11 @@ class ImageProcessor3: else: self.logger.log("PostImageManager가 None이므로 toggle_states 업데이트를 건너뜁니다.", level=logging.WARNING) + # 외부 서버 헬스 체크 (toggle_states 업데이트 시마다 수행) + if self.inpaint_method == 'external_request': + self.is_external_server_alive = self.check_external_server_availability() + self.logger.log(f"외부 인페인팅 서버 상태 확인: {self.is_external_server_alive}", level=logging.DEBUG) + self.logger.log(f"[UpdateToggle] 완료: member={self.is_member_valid}, inpaint={self.inpaint_method}, font={os.path.basename(self.font_path)}", level=logging.DEBUG) def update_unwanted_texts(self, texts): @@ -747,16 +755,16 @@ class ImageProcessor3: area_small_thr = float(self.toggle_states.get('inpaint_auto_area_small_thresh', 0.03)) dist_thr = float(self.toggle_states.get('inpaint_auto_min_distance_ratio', 0.10)) - choose = 'request' # 기본: lama(서버) + choose = 'external_request' # 기본: lama(서버) if analysis['coverage_ratio'] <= area_small_thr and \ analysis['component_count'] >= 1 and \ analysis['min_centroid_distance_ratio'] >= dist_thr: choose = 'migan' elif analysis['coverage_ratio'] >= area_large_thr: - choose = 'request' + choose = 'external_request' else: # 중간대: lama 우선 (품질 우선) - choose = 'request' + choose = 'external_request' self.logger.log( f"[AUTO Inpaint] coverage={analysis['coverage_ratio']:.3f}, comps={analysis['component_count']}, " @@ -775,9 +783,9 @@ class ImageProcessor3: self.logger.log(f"is_member_valid: {self.is_member_valid}", level=logging.DEBUG) # 인페인팅 방법 설정 - # self.set_inpaint_method(file_prefix) - # self.logger.log(f"최종 inpaint_method: {self.inpaint_method}", level=logging.DEBUG) - self.inpaint_method = 'migan' + self.set_inpaint_method(file_prefix) + self.logger.log(f"최종 inpaint_method: {self.inpaint_method}", level=logging.DEBUG) + # self.inpaint_method = 'migan' # 인페인팅 실행 (폴백 순서: 자체서버 > GPU > CPU) _t = _time.time() @@ -890,7 +898,7 @@ class ImageProcessor3: method_map = { "CPU": "cv", # CPU 선택 시 OpenCV 인페인팅 "GPU": "migan", # GPU 선택 시 MIGAN 인페인팅 - "자체서버": "request", # 자체서버 선택 시 Request 인페인팅 + "자체서버": "external_request", # 자체서버 선택 시 Request 인페인팅 } self.inpaint_method = method_map.get(trans_type, "cv") # 기타는 cv로 폴백 @@ -917,14 +925,20 @@ class ImageProcessor3: inpainted_image = None - # 1. 사용자 설정 확인 - preferred_method = self.toggle_states.get("inpaint_method", "migan") + # 1. 사용자 설정 확인 (self.inpaint_method가 설정되어 있으면 최우선, 없으면 토글값) + # set_inpaint_method() 또는 자동 로직에 의해 설정된 값이 있으면 그것을 따름 + preferred_method = getattr(self, 'inpaint_method', None) + if not preferred_method: + preferred_method = self.toggle_states.get("inpaint_method", "migan") + server_url = self.toggle_states.get("request_inpainting_server_url", "") # 2. External Request 모드일 때 처리 if preferred_method == "external_request": if self.is_member_valid: - if server_url and str(server_url).strip().startswith("http"): + if not self.is_external_server_alive: + self.logger.log("외부 서버 상태 비정상(헬스 체크 실패) -> 로컬 MIGAN으로 폴백", level=logging.WARNING) + elif server_url and str(server_url).strip().startswith("http"): # 외부 서버 시도 self.inpaint_method = 'external_request' inpainted_image = self._try_external_inpaint(local_image_path, masks, str(server_url).strip()) @@ -979,7 +993,7 @@ class ImageProcessor3: result = self.request_ai_server.request_inpaint(local_image_path, masks, invert_mask=invert_mask, inpaint_model=inpaint_model) if result is not None: self.logger.log("자체서버 인페인팅 성공", level=logging.DEBUG) - self._last_inpaint_used = "request" + self._last_inpaint_used = "external_request" self._last_inpaint_device = "SERVER" return result else: diff --git a/modules/request_inpaint.py b/modules/request_inpaint.py index 18b0a64..f5c046b 100644 --- a/modules/request_inpaint.py +++ b/modules/request_inpaint.py @@ -823,12 +823,11 @@ class Request_AI_Server: def is_server_alive(self, base_url: str, timeout: int = 3) -> bool: """서버 헬스체크(현재는 사용 안 함). base_url이 비어있으면 False.""" - import requests as _rq try: if not base_url: return False model_url = base_url.rstrip('/') + '/api/v1/model' - response = _rq.get(model_url, timeout=timeout) + response = requests.get(model_url, timeout=timeout) return response.status_code == 200 except Exception as e: self.logger.log(f"서버 상태 확인 실패 ({base_url}): {e}", level=logging.WARNING) diff --git a/modules/tray_app.py b/modules/tray_app.py index e1a5071..fc1b5d6 100644 --- a/modules/tray_app.py +++ b/modules/tray_app.py @@ -11,6 +11,17 @@ 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): @@ -55,7 +66,7 @@ class TrayController: 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 [{status_label}]{perf}{prov}" + title = f"ImgWorker v{app_version} [{status_label}]{perf}{prov}" if self.icon: self.icon.title = title # 메뉴를 상태에 맞게 재구성 diff --git a/query b/query new file mode 100644 index 0000000..c8d482e --- /dev/null +++ b/query @@ -0,0 +1 @@ +ImgWorker diff --git a/setup.py b/setup.py index ab65bff..fdd8846 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,13 @@ ROOT_DIR = os.path.abspath(os.path.dirname(__file__)) # 앱 메타 APP_NAME = "ImgWorker" -APP_VERSION = "1.0.0" + +# 버전 정보 로드 +about = {} +with open(os.path.join(ROOT_DIR, "updater", "__version__.py"), "r", encoding="utf-8") as f: + exec(f.read(), about) +APP_VERSION = about["__version__"] + # 기본 포함 패키지 목록(동적 로딩되는 많은 모듈을 고려하여 넉넉히 포함) INCLUDES = [ diff --git a/updater/__init__.py b/updater/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/updater/__version__.py b/updater/__version__.py new file mode 100644 index 0000000..1794957 --- /dev/null +++ b/updater/__version__.py @@ -0,0 +1,2 @@ +__version__ = "1.3.0" + diff --git a/updater/update_Log.MD b/updater/update_Log.MD new file mode 100644 index 0000000..3fbb4b0 --- /dev/null +++ b/updater/update_Log.MD @@ -0,0 +1,14 @@ +###1.3.0 ChangeLog + - 서버코드정리 + - 메모리관리 최적화 + +###1.2.0 ChangeLog + - 폰트추가 + - update toggle 엔드포인트 추가 + - external server 코드 추가 + +###1.1.0 ChangeLog + - 트레이 추가 + +###1.0.0 ChangeLog + - First Relaese \ No newline at end of file