Enhance process management and versioning in the application

- Added `_kill_proc_tree` function to ensure complete termination of process trees.
- Updated `WorkerManager` to utilize the new process termination method for better cleanup.
- Integrated dynamic versioning by importing `__version__` from the updater module.
- Modified FastAPI app initialization to reflect the dynamic version.
- Enhanced `ImageProcessor3` to include external server health checks and updated inpainting method handling.
- Adjusted tray application title to display the current app version.
This commit is contained in:
Your Name 2025-11-23 22:37:18 +09:00
parent 19eab3a464
commit 26c8ca5551
9 changed files with 107 additions and 21 deletions

51
main.py
View File

@ -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")

View File

@ -262,6 +262,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)
# 치명적 오류 발생 시에도 기본 속성들이 None으로라도 설정되도록 보장
@ -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. 사용자 설정 확인
# 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:

View File

@ -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)

View File

@ -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
# 메뉴를 상태에 맞게 재구성

1
query Normal file
View File

@ -0,0 +1 @@
ImgWorker

View File

@ -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 = [

0
updater/__init__.py Normal file
View File

2
updater/__version__.py Normal file
View File

@ -0,0 +1,2 @@
__version__ = "1.3.0"

14
updater/update_Log.MD Normal file
View File

@ -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