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:
parent
19eab3a464
commit
26c8ca5551
51
main.py
51
main.py
|
|
@ -27,6 +27,7 @@ from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from loggerModule import Logger
|
from loggerModule import Logger
|
||||||
from modules.image_worker import worker_main
|
from modules.image_worker import worker_main
|
||||||
|
from updater.__version__ import __version__ as app_version
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -238,6 +239,35 @@ def _cleanup_runtime_files():
|
||||||
pass
|
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):
|
def _safe_rmtree_contents(target_dir: str, older_than_sec: int = 0):
|
||||||
"""대상 디렉터리 하위의 항목을 삭제.
|
"""대상 디렉터리 하위의 항목을 삭제.
|
||||||
|
|
||||||
|
|
@ -595,6 +625,10 @@ class WorkerManager:
|
||||||
if self._proc is None:
|
if self._proc is None:
|
||||||
return
|
return
|
||||||
self._running.clear()
|
self._running.clear()
|
||||||
|
|
||||||
|
# PID 백업 (join 후 객체 상태와 무관하게 정리하기 위함)
|
||||||
|
pid = self._proc.pid
|
||||||
|
|
||||||
# 워커에게 종료 신호(None)
|
# 워커에게 종료 신호(None)
|
||||||
try:
|
try:
|
||||||
self._task_q.put(None, timeout=1)
|
self._task_q.put(None, timeout=1)
|
||||||
|
|
@ -602,11 +636,14 @@ class WorkerManager:
|
||||||
pass
|
pass
|
||||||
# 프로세스 종료 대기
|
# 프로세스 종료 대기
|
||||||
self._proc.join(timeout=10)
|
self._proc.join(timeout=10)
|
||||||
if self._proc.is_alive():
|
|
||||||
|
# 프로세스 트리 전체 확실한 정리
|
||||||
|
if pid:
|
||||||
try:
|
try:
|
||||||
os.kill(self._proc.pid, signal.SIGTERM)
|
_kill_proc_tree(pid)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
self.logger.info("워커 프로세스 종료")
|
self.logger.info("워커 프로세스 종료")
|
||||||
finally:
|
finally:
|
||||||
self._proc = None
|
self._proc = None
|
||||||
|
|
@ -1050,12 +1087,14 @@ class WorkerManager:
|
||||||
self._task_q.put(None, timeout=1)
|
self._task_q.put(None, timeout=1)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
# 2) 종료 대기
|
# 2) 종료 대기 및 강제 정리
|
||||||
if self._proc is not None:
|
if self._proc is not None:
|
||||||
|
pid = self._proc.pid
|
||||||
self._proc.join(timeout=30)
|
self._proc.join(timeout=30)
|
||||||
if self._proc.is_alive():
|
if pid:
|
||||||
try:
|
try:
|
||||||
os.kill(self._proc.pid, signal.SIGTERM)
|
# 롤링 시에도 확실하게 트리 정리
|
||||||
|
_kill_proc_tree(pid)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -1147,7 +1186,7 @@ async def lifespan(app: FastAPI):
|
||||||
_cleanup_runtime_files()
|
_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")
|
@app.get("/health")
|
||||||
|
|
|
||||||
|
|
@ -261,6 +261,9 @@ class ImageProcessor3:
|
||||||
# 인페인팅 실행 정보(마지막 사용 방식/장치) 추적용 내부 상태
|
# 인페인팅 실행 정보(마지막 사용 방식/장치) 추적용 내부 상태
|
||||||
self._last_inpaint_used = None
|
self._last_inpaint_used = None
|
||||||
self._last_inpaint_device = None
|
self._last_inpaint_device = None
|
||||||
|
|
||||||
|
# 외부 서버 헬스 체크 플래그
|
||||||
|
self.is_external_server_alive = False
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.log(f"ImageProcessor3 초기화 중 치명적 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
self.logger.log(f"ImageProcessor3 초기화 중 치명적 오류 발생: {e}", level=logging.ERROR, exc_info=True)
|
||||||
|
|
@ -409,6 +412,11 @@ class ImageProcessor3:
|
||||||
else:
|
else:
|
||||||
self.logger.log("PostImageManager가 None이므로 toggle_states 업데이트를 건너뜁니다.", level=logging.WARNING)
|
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)
|
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):
|
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))
|
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))
|
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 \
|
if analysis['coverage_ratio'] <= area_small_thr and \
|
||||||
analysis['component_count'] >= 1 and \
|
analysis['component_count'] >= 1 and \
|
||||||
analysis['min_centroid_distance_ratio'] >= dist_thr:
|
analysis['min_centroid_distance_ratio'] >= dist_thr:
|
||||||
choose = 'migan'
|
choose = 'migan'
|
||||||
elif analysis['coverage_ratio'] >= area_large_thr:
|
elif analysis['coverage_ratio'] >= area_large_thr:
|
||||||
choose = 'request'
|
choose = 'external_request'
|
||||||
else:
|
else:
|
||||||
# 중간대: lama 우선 (품질 우선)
|
# 중간대: lama 우선 (품질 우선)
|
||||||
choose = 'request'
|
choose = 'external_request'
|
||||||
|
|
||||||
self.logger.log(
|
self.logger.log(
|
||||||
f"[AUTO Inpaint] coverage={analysis['coverage_ratio']:.3f}, comps={analysis['component_count']}, "
|
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.logger.log(f"is_member_valid: {self.is_member_valid}", level=logging.DEBUG)
|
||||||
|
|
||||||
# 인페인팅 방법 설정
|
# 인페인팅 방법 설정
|
||||||
# self.set_inpaint_method(file_prefix)
|
self.set_inpaint_method(file_prefix)
|
||||||
# self.logger.log(f"최종 inpaint_method: {self.inpaint_method}", level=logging.DEBUG)
|
self.logger.log(f"최종 inpaint_method: {self.inpaint_method}", level=logging.DEBUG)
|
||||||
self.inpaint_method = 'migan'
|
# self.inpaint_method = 'migan'
|
||||||
|
|
||||||
# 인페인팅 실행 (폴백 순서: 자체서버 > GPU > CPU)
|
# 인페인팅 실행 (폴백 순서: 자체서버 > GPU > CPU)
|
||||||
_t = _time.time()
|
_t = _time.time()
|
||||||
|
|
@ -890,7 +898,7 @@ class ImageProcessor3:
|
||||||
method_map = {
|
method_map = {
|
||||||
"CPU": "cv", # CPU 선택 시 OpenCV 인페인팅
|
"CPU": "cv", # CPU 선택 시 OpenCV 인페인팅
|
||||||
"GPU": "migan", # GPU 선택 시 MIGAN 인페인팅
|
"GPU": "migan", # GPU 선택 시 MIGAN 인페인팅
|
||||||
"자체서버": "request", # 자체서버 선택 시 Request 인페인팅
|
"자체서버": "external_request", # 자체서버 선택 시 Request 인페인팅
|
||||||
}
|
}
|
||||||
|
|
||||||
self.inpaint_method = method_map.get(trans_type, "cv") # 기타는 cv로 폴백
|
self.inpaint_method = method_map.get(trans_type, "cv") # 기타는 cv로 폴백
|
||||||
|
|
@ -917,14 +925,20 @@ class ImageProcessor3:
|
||||||
|
|
||||||
inpainted_image = None
|
inpainted_image = None
|
||||||
|
|
||||||
# 1. 사용자 설정 확인
|
# 1. 사용자 설정 확인 (self.inpaint_method가 설정되어 있으면 최우선, 없으면 토글값)
|
||||||
preferred_method = self.toggle_states.get("inpaint_method", "migan")
|
# 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", "")
|
server_url = self.toggle_states.get("request_inpainting_server_url", "")
|
||||||
|
|
||||||
# 2. External Request 모드일 때 처리
|
# 2. External Request 모드일 때 처리
|
||||||
if preferred_method == "external_request":
|
if preferred_method == "external_request":
|
||||||
if self.is_member_valid:
|
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'
|
self.inpaint_method = 'external_request'
|
||||||
inpainted_image = self._try_external_inpaint(local_image_path, masks, str(server_url).strip())
|
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)
|
result = self.request_ai_server.request_inpaint(local_image_path, masks, invert_mask=invert_mask, inpaint_model=inpaint_model)
|
||||||
if result is not None:
|
if result is not None:
|
||||||
self.logger.log("자체서버 인페인팅 성공", level=logging.DEBUG)
|
self.logger.log("자체서버 인페인팅 성공", level=logging.DEBUG)
|
||||||
self._last_inpaint_used = "request"
|
self._last_inpaint_used = "external_request"
|
||||||
self._last_inpaint_device = "SERVER"
|
self._last_inpaint_device = "SERVER"
|
||||||
return result
|
return result
|
||||||
else:
|
else:
|
||||||
|
|
|
||||||
|
|
@ -823,12 +823,11 @@ class Request_AI_Server:
|
||||||
|
|
||||||
def is_server_alive(self, base_url: str, timeout: int = 3) -> bool:
|
def is_server_alive(self, base_url: str, timeout: int = 3) -> bool:
|
||||||
"""서버 헬스체크(현재는 사용 안 함). base_url이 비어있으면 False."""
|
"""서버 헬스체크(현재는 사용 안 함). base_url이 비어있으면 False."""
|
||||||
import requests as _rq
|
|
||||||
try:
|
try:
|
||||||
if not base_url:
|
if not base_url:
|
||||||
return False
|
return False
|
||||||
model_url = base_url.rstrip('/') + '/api/v1/model'
|
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
|
return response.status_code == 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.log(f"서버 상태 확인 실패 ({base_url}): {e}", level=logging.WARNING)
|
self.logger.log(f"서버 상태 확인 실패 ({base_url}): {e}", level=logging.WARNING)
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,17 @@ import requests
|
||||||
|
|
||||||
from loggerModule import Logger
|
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:
|
class TrayController:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -55,7 +66,7 @@ class TrayController:
|
||||||
status_label = 'ACTIVE' if active else ('READY' if ready else 'STOP')
|
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 ''
|
perf = f" - avg. {avg_sec:.1f}(sec/장)" if isinstance(avg_sec, (int, float)) else ''
|
||||||
prov = f" [{provider}]" if provider 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:
|
if self.icon:
|
||||||
self.icon.title = title
|
self.icon.title = title
|
||||||
# 메뉴를 상태에 맞게 재구성
|
# 메뉴를 상태에 맞게 재구성
|
||||||
|
|
|
||||||
8
setup.py
8
setup.py
|
|
@ -8,7 +8,13 @@ ROOT_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
# 앱 메타
|
# 앱 메타
|
||||||
APP_NAME = "ImgWorker"
|
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 = [
|
INCLUDES = [
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
__version__ = "1.3.0"
|
||||||
|
|
||||||
|
|
@ -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
|
||||||
Loading…
Reference in New Issue