mirror of
https://github.com/j93es/browser-use-oauth.git
synced 2026-06-04 01:21:52 +09:00
브라우저 리소스 정리 및 종료 처리 개선, 진행 상황 저장 기능 추가
This commit is contained in:
parent
0f5ab6dea1
commit
d6803ad20e
13 changed files with 551 additions and 172 deletions
97
run.py
97
run.py
|
|
@ -14,49 +14,107 @@ PYTHON_SCRIPT = "./src/main.py"
|
|||
DOMAIN_FILE = "./data/domains.txt"
|
||||
# ─────────────
|
||||
|
||||
|
||||
def download_domains():
|
||||
"""도메인 파일 다운로드"""
|
||||
try:
|
||||
print("도메인 파일 다운로드 중...")
|
||||
response = requests.get("https://f.imnya.ng/.whs/tp-domains/data/domains/latest.txt")
|
||||
response = requests.get(
|
||||
"https://f.imnya.ng/.whs/tp-domains/data/domains/latest.txt"
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
# 디렉토리가 없으면 생성
|
||||
os.makedirs(os.path.dirname("./data"), exist_ok=True)
|
||||
|
||||
with open(DOMAIN_FILE, 'w', encoding='utf-8') as f:
|
||||
with open(DOMAIN_FILE, "w", encoding="utf-8") as f:
|
||||
f.write(response.text)
|
||||
print("도메인 파일 다운로드 완료")
|
||||
except requests.RequestException as e:
|
||||
print(f"도메인 파일 다운로드 실패: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def run_script(start_line, end_line, skh_option):
|
||||
"""Python 스크립트 실행"""
|
||||
current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
print(f"[{current_time}] Processing lines {start_line} to {end_line}...")
|
||||
|
||||
process = None
|
||||
signal_handled = False
|
||||
|
||||
def signal_handler(sig, frame):
|
||||
nonlocal signal_handled
|
||||
if signal_handled:
|
||||
return
|
||||
signal_handled = True
|
||||
|
||||
print("\n🛑 종료 신호를 받았습니다. 정리 작업을 진행합니다...")
|
||||
|
||||
if process:
|
||||
try:
|
||||
# 자식 프로세스에 SIGTERM 전송
|
||||
print("📤 서브프로세스에 종료 신호를 전달합니다...")
|
||||
process.terminate()
|
||||
# 5초간 대기
|
||||
process.wait(timeout=5)
|
||||
print("✅ 서브프로세스가 정상적으로 종료되었습니다.")
|
||||
except subprocess.TimeoutExpired:
|
||||
print("⚠️ 서브프로세스가 응답하지 않아 강제 종료합니다...")
|
||||
process.kill()
|
||||
try:
|
||||
process.wait(timeout=3)
|
||||
print("✅ 서브프로세스가 강제 종료되었습니다.")
|
||||
except subprocess.TimeoutExpired:
|
||||
print("❌ 서브프로세스 강제 종료 실패")
|
||||
except Exception as e:
|
||||
print(f"❌ 프로세스 종료 중 오류: {e}")
|
||||
|
||||
print("✅ 런처 종료 완료.")
|
||||
sys.exit(0)
|
||||
|
||||
# 원래 시그널 핸들러 저장
|
||||
original_sigint = signal.signal(signal.SIGINT, signal_handler)
|
||||
original_sigterm = signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
try:
|
||||
command = [
|
||||
"uv", "run", PYTHON_SCRIPT,
|
||||
"-f", DOMAIN_FILE,
|
||||
"-s", str(start_line),
|
||||
"-e", str(end_line),
|
||||
"uv",
|
||||
"run",
|
||||
PYTHON_SCRIPT,
|
||||
"-f",
|
||||
DOMAIN_FILE,
|
||||
"-s",
|
||||
str(start_line),
|
||||
"-e",
|
||||
str(end_line),
|
||||
]
|
||||
if skh_option:
|
||||
command.append("--skip-html-check")
|
||||
|
||||
# KeyboardInterrupt를 subprocess에 전달하도록 수정
|
||||
process = subprocess.Popen(command)
|
||||
process.wait()
|
||||
returncode = process.wait()
|
||||
|
||||
if process.returncode != 0:
|
||||
print("Python 스크립트 실행 실패")
|
||||
sys.exit(1)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Python 스크립트 실행 실패")
|
||||
if returncode != 0:
|
||||
print(f"❌ Python 스크립트가 오류 코드 {returncode}로 종료되었습니다.")
|
||||
sys.exit(returncode)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
signal_handler(signal.SIGINT, None)
|
||||
except Exception as e:
|
||||
print(f"❌ 스크립트 실행 중 오류: {e}")
|
||||
if process:
|
||||
try:
|
||||
process.terminate()
|
||||
process.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
process.kill()
|
||||
sys.exit(1)
|
||||
finally:
|
||||
# 시그널 핸들러 복원
|
||||
signal.signal(signal.SIGINT, original_sigint)
|
||||
signal.signal(signal.SIGTERM, original_sigterm)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
|
|
@ -67,13 +125,15 @@ def main():
|
|||
uv run run.py 10000 11000 # 10000~11000 라인 처리
|
||||
uv run run.py 10000 11000 --skh # SKH 옵션 활성화
|
||||
uv run run.py 10000 11000 --no-download # 다운로드 생략
|
||||
"""
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument("start_line", type=int, help="시작 라인 번호")
|
||||
parser.add_argument("end_line", type=int, help="종료 라인 번호")
|
||||
parser.add_argument("--skh", action="store_true", help="SKH 옵션 활성화")
|
||||
parser.add_argument("--no-download", action="store_true", help="도메인 파일 다운로드 생략")
|
||||
parser.add_argument(
|
||||
"--no-download", action="store_true", help="도메인 파일 다운로드 생략"
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
|
@ -90,7 +150,9 @@ def main():
|
|||
if not args.no_download:
|
||||
download_domains()
|
||||
elif not os.path.exists(DOMAIN_FILE):
|
||||
print(f"도메인 파일({DOMAIN_FILE})이 존재하지 않습니다. --no-download 옵션을 제거하거나 파일을 준비해주세요.")
|
||||
print(
|
||||
f"도메인 파일({DOMAIN_FILE})이 존재하지 않습니다. --no-download 옵션을 제거하거나 파일을 준비해주세요."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
# 스크립트 실행
|
||||
|
|
@ -98,5 +160,6 @@ def main():
|
|||
|
||||
print("처리 완료.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
110
src/lib/browser_use/cleanup.py
Normal file
110
src/lib/browser_use/cleanup.py
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
"""
|
||||
브라우저 리소스 정리를 위한 모듈
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
async def cleanup_browser_resources(agent=None, session=None, user_data_dir=None):
|
||||
"""브라우저 관련 리소스를 정리하는 함수"""
|
||||
print("🔄 브라우저 리소스 정리를 시작합니다...")
|
||||
|
||||
# 에이전트 리소스 정리
|
||||
if agent:
|
||||
try:
|
||||
print("<EFBFBD> 에이전트 리소스 정리 중...")
|
||||
# 브라우저 종료 대기 시간 설정
|
||||
await asyncio.wait_for(agent.close(), timeout=10.0)
|
||||
print("✅ 에이전트 리소스 정리 완료.")
|
||||
except asyncio.TimeoutError:
|
||||
print("⚠️ 에이전트 종료 시간 초과. 강제 종료합니다.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 에이전트 리소스 정리 실패: {e}")
|
||||
|
||||
# 세션 리소스 정리
|
||||
if session:
|
||||
try:
|
||||
print("🔄 세션 리소스 정리 중...")
|
||||
await asyncio.wait_for(session.close(), timeout=5.0)
|
||||
print("✅ 세션 리소스 정리 완료.")
|
||||
except asyncio.TimeoutError:
|
||||
print("⚠️ 세션 종료 시간 초과.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 세션 리소스 정리 실패: {e}")
|
||||
|
||||
# 임시 스토리지 상태 파일 삭제
|
||||
storage_state_temp_path = Path("./data/storage_state_temp.json").resolve()
|
||||
if storage_state_temp_path.exists():
|
||||
try:
|
||||
print(f"<EFBFBD>️ 임시 스토리지 상태 파일 삭제 중: {storage_state_temp_path}")
|
||||
storage_state_temp_path.unlink()
|
||||
print("✅ 임시 스토리지 상태 파일 삭제 완료.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 임시 스토리지 상태 파일 삭제 실패: {e}")
|
||||
|
||||
# 임시 사용자 데이터 디렉토리 정리
|
||||
if user_data_dir and os.path.exists(user_data_dir):
|
||||
try:
|
||||
print(f"🗑️ 임시 사용자 데이터 디렉토리 삭제 중: {user_data_dir}")
|
||||
await asyncio.sleep(0.5) # 브라우저가 완전히 종료될 시간 제공
|
||||
shutil.rmtree(user_data_dir)
|
||||
print("✅ 임시 사용자 데이터 디렉토리 삭제 완료.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 임시 사용자 데이터 디렉토리 삭제 실패: {e}")
|
||||
|
||||
# userdata.dump 파일에서 기록된 디렉토리 정리
|
||||
log_file = "./data/userdata.dump"
|
||||
if os.path.exists(log_file):
|
||||
try:
|
||||
with open(log_file, "r") as f:
|
||||
tmp_user_data_dir = f.read().strip()
|
||||
if tmp_user_data_dir and os.path.exists(tmp_user_data_dir):
|
||||
print(f"🗑️ 기록된 임시 사용자 데이터 디렉토리 삭제 중: {tmp_user_data_dir}")
|
||||
await asyncio.sleep(0.5) # 브라우저가 완전히 종료될 시간 제공
|
||||
shutil.rmtree(tmp_user_data_dir)
|
||||
print("✅ 기록된 임시 사용자 데이터 디렉토리 삭제 완료.")
|
||||
os.remove(log_file)
|
||||
print("✅ userdata.dump 파일 삭제 완료.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ userdata.dump 관련 정리 실패: {e}")
|
||||
|
||||
print("✅ 브라우저 리소스 정리가 완료되었습니다.")
|
||||
|
||||
|
||||
def cleanup_all_running_tasks():
|
||||
"""실행 중인 모든 asyncio 태스크를 정리"""
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
tasks = [task for task in asyncio.all_tasks(loop) if not task.done()]
|
||||
|
||||
if tasks:
|
||||
print(f"🔄 {len(tasks)}개의 실행 중인 태스크를 정리합니다...")
|
||||
for task in tasks:
|
||||
task.cancel()
|
||||
|
||||
# 태스크들이 정리될 때까지 잠시 대기
|
||||
async def wait_for_tasks():
|
||||
await asyncio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
asyncio.create_task(wait_for_tasks())
|
||||
print("✅ 모든 태스크 정리 완료.")
|
||||
except RuntimeError:
|
||||
# 이벤트 루프가 실행 중이 아닌 경우
|
||||
pass
|
||||
except Exception as e:
|
||||
print(f"⚠️ 태스크 정리 중 오류: {e}")
|
||||
|
||||
|
||||
async def emergency_cleanup():
|
||||
"""긴급 종료 시 최소한의 리소스 정리"""
|
||||
print("🚨 긴급 리소스 정리 실행 중...")
|
||||
|
||||
# 모든 태스크 취소
|
||||
cleanup_all_running_tasks()
|
||||
|
||||
# 기본 리소스 정리
|
||||
await cleanup_browser_resources()
|
||||
|
||||
print("✅ 긴급 리소스 정리 완료.")
|
||||
|
|
@ -10,51 +10,74 @@ proxy_url = setup_proxy()
|
|||
|
||||
|
||||
async def GetProfile(headless=False):
|
||||
"""브라우저 프로필을 생성하고 임시 사용자 데이터 디렉토리를 관리합니다."""
|
||||
user_data_dir = None
|
||||
tmp_user_data_dir = None
|
||||
|
||||
if USER_DATA_DIR and os.path.isdir(USER_DATA_DIR):
|
||||
try:
|
||||
tmp_user_data_dir = tempfile.mkdtemp()
|
||||
# write path in user_data_dir_path
|
||||
print(f"🔧 Using user data dir: {USER_DATA_DIR}")
|
||||
print(f"🔧 Temporary user data dir: {tmp_user_data_dir}")
|
||||
tmp_user_data_dir = tempfile.mkdtemp(prefix="browser_use_")
|
||||
print(f"🔧 기본 사용자 데이터 디렉토리: {USER_DATA_DIR}")
|
||||
print(f"🔧 임시 사용자 데이터 디렉토리: {tmp_user_data_dir}")
|
||||
|
||||
log_file = os.path.join("./data", "userdata.dump")
|
||||
if not os.path.exists("./data"):
|
||||
os.makedirs("./data")
|
||||
|
||||
# 기존 로그 파일이 있다면 해당 디렉토리 정리
|
||||
if os.path.exists(log_file):
|
||||
try:
|
||||
with open(log_file, "r") as f:
|
||||
old_tmp_dir = f.read().strip()
|
||||
if old_tmp_dir and os.path.exists(old_tmp_dir):
|
||||
shutil.rmtree(old_tmp_dir)
|
||||
print(f"🗑️ 이전 임시 디렉토리 정리: {old_tmp_dir}")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 이전 임시 디렉토리 정리 실패: {e}")
|
||||
os.remove(log_file)
|
||||
|
||||
# Log current browser use directory
|
||||
# 새 임시 디렉토리 경로 로깅
|
||||
with open(log_file, "w") as f:
|
||||
f.write(f"{tmp_user_data_dir}")
|
||||
f.write(tmp_user_data_dir)
|
||||
|
||||
# Copy USER_DATA_DIR to tmp_user_data_dir
|
||||
# 사용자 데이터 디렉토리 복사
|
||||
if os.path.exists(tmp_user_data_dir):
|
||||
shutil.rmtree(tmp_user_data_dir)
|
||||
shutil.copytree(USER_DATA_DIR, tmp_user_data_dir, dirs_exist_ok=False, ignore_dangling_symlinks=True)
|
||||
shutil.copytree(
|
||||
USER_DATA_DIR,
|
||||
tmp_user_data_dir,
|
||||
dirs_exist_ok=False,
|
||||
ignore_dangling_symlinks=True
|
||||
)
|
||||
user_data_dir = tmp_user_data_dir
|
||||
print(f"✅ Copied user data dir to temporary location: {user_data_dir}")
|
||||
print(f"✅ 사용자 데이터 디렉토리 복사 완료: {user_data_dir}")
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to copy user data dir: {e}")
|
||||
print(f"❌ 사용자 데이터 디렉토리 복사 실패: {e}")
|
||||
# 실패 시 임시 디렉토리 정리
|
||||
if tmp_user_data_dir and os.path.exists(tmp_user_data_dir):
|
||||
try:
|
||||
shutil.rmtree(tmp_user_data_dir)
|
||||
except Exception:
|
||||
pass
|
||||
tmp_user_data_dir = None
|
||||
user_data_dir = None
|
||||
|
||||
profile = BrowserProfile(
|
||||
# Security settings
|
||||
disable_security=True,
|
||||
#stealth=True,
|
||||
# Display settings
|
||||
headless=headless,
|
||||
#device_scale_factor=1,
|
||||
#window_size={"width": 1600, "height": 900},
|
||||
#viewport={"width": 1600, "height": 900},
|
||||
# Data persistence
|
||||
user_data_dir=user_data_dir,
|
||||
#storage_state=storage_state,
|
||||
# Network settings
|
||||
proxy={"server": proxy_url} if proxy_url else None,
|
||||
# Additional arguments
|
||||
#args=get_browser_args(),
|
||||
ignore_default_args=['--enable-automation', '--disable-extensions', '--hide-scrollbars', '--disable-features=AcceptCHFrame,AutoExpandDetailsElement,AvoidUnnecessaryBeforeUnloadCheckSync,CertificateTransparencyComponentUpdater,DeferRendererTasksAfterInput,DestroyProfileOnBrowserClose,DialMediaRouteProvider,ExtensionManifestV2Disabled,GlobalMediaControls,HttpsUpgrades,ImprovedCookieControls,LazyFrameLoading,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate'],
|
||||
ignore_default_args=[
|
||||
'--enable-automation',
|
||||
'--disable-extensions',
|
||||
'--hide-scrollbars',
|
||||
'--disable-features=AcceptCHFrame,AutoExpandDetailsElement,AvoidUnnecessaryBeforeUnloadCheckSync,CertificateTransparencyComponentUpdater,DeferRendererTasksAfterInput,DestroyProfileOnBrowserClose,DialMediaRouteProvider,ExtensionManifestV2Disabled,GlobalMediaControls,HttpsUpgrades,ImprovedCookieControls,LazyFrameLoading,LensOverlay,MediaRouter,PaintHolding,ThirdPartyStoragePartitioning,Translate'
|
||||
],
|
||||
)
|
||||
|
||||
return [profile, tmp_user_data_dir] if tmp_user_data_dir else [profile]
|
||||
|
|
|
|||
|
|
@ -8,13 +8,14 @@ from lib.browser_use.agents import (
|
|||
start_retry_queue_processor,
|
||||
test_oauth_login,
|
||||
)
|
||||
from lib.browser_use.cleanup import cleanup_browser_resources
|
||||
from lib.utils import is_html_url, notify_backend, read_lines_between
|
||||
from lib.utils.progress import (
|
||||
current_progress,
|
||||
is_shutdown_requested,
|
||||
load_progress,
|
||||
progress_file,
|
||||
save_progress,
|
||||
is_shutdown_requested,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -76,106 +77,115 @@ async def main_loop(
|
|||
filepath: str, start_line: int, end_line: int, skip_html_check: bool = False
|
||||
):
|
||||
"""지정된 URL 목록에 대해 스캔을 실행하는 메인 루프"""
|
||||
# 재시도 큐 처리기 시작
|
||||
await start_retry_queue_processor()
|
||||
try:
|
||||
# 재시도 큐 처리기 시작
|
||||
await start_retry_queue_processor()
|
||||
|
||||
target_list = read_lines_between(
|
||||
filepath=filepath, start_line=start_line, end_line=end_line
|
||||
)
|
||||
|
||||
# 전체 목록 길이를 저장 (재개 시에도 유지되어야 함)
|
||||
total_count = len(target_list)
|
||||
current_progress["total"] = total_count
|
||||
current_progress["start_line"] = start_line
|
||||
current_progress["current_index"] = 0
|
||||
|
||||
prev_progress = load_progress()
|
||||
if prev_progress and prev_progress.get("start_line") == start_line:
|
||||
print("📋 이전 진행 상황을 발견했습니다:")
|
||||
print(
|
||||
f" - 이전 완료: {prev_progress['current_index']}/{prev_progress['total']}"
|
||||
)
|
||||
print(f" - 마지막 처리: {prev_progress.get('current_url', 'N/A')}")
|
||||
|
||||
resume = input("이어서 진행하시겠습니까? (y/n): ").lower().strip()
|
||||
if resume == "y":
|
||||
start_index = prev_progress.get("current_index", 0)
|
||||
current_progress["current_index"] = start_index
|
||||
# 전체 개수는 원래 목록 길이로 유지
|
||||
current_progress["total"] = total_count
|
||||
target_list = target_list[start_index:]
|
||||
print(f"✅ {start_index}번째부터 재개합니다.")
|
||||
|
||||
for i, url in enumerate(target_list):
|
||||
# 종료 요청 체크
|
||||
if is_shutdown_requested():
|
||||
print("🛑 종료 요청으로 인해 스캔을 중단합니다.")
|
||||
break
|
||||
|
||||
# current_index는 전체 목록에서의 현재 위치를 나타냄
|
||||
current_url_index = current_progress["current_index"]
|
||||
current_progress["current_url"] = url
|
||||
|
||||
print(
|
||||
f"\n🔄 Processing {current_url_index + 1}/{current_progress['total']}: {url}"
|
||||
)
|
||||
print(
|
||||
f"📍 {os.path.basename(filepath)}의 {start_line + current_url_index}번째 줄"
|
||||
target_list = read_lines_between(
|
||||
filepath=filepath, start_line=start_line, end_line=end_line
|
||||
)
|
||||
|
||||
# 재시도 큐 상태 확인 및 출력
|
||||
retry_status = await get_retry_queue_status()
|
||||
if retry_status["queue_length"] > 0:
|
||||
print(f"⏳ 재시도 큐에 {retry_status['queue_length']}개 작업 대기 중")
|
||||
# 전체 목록 길이를 저장 (재개 시에도 유지되어야 함)
|
||||
total_count = len(target_list)
|
||||
current_progress["total"] = total_count
|
||||
current_progress["start_line"] = start_line
|
||||
current_progress["current_index"] = 0
|
||||
|
||||
if i > 0:
|
||||
print("⏳ API 쿼터 보호를 위해 30초 대기 중...")
|
||||
# 대기 중에도 종료 요청 체크
|
||||
for _ in range(30):
|
||||
if is_shutdown_requested():
|
||||
print("🛑 대기 중 종료 요청으로 스캔을 중단합니다.")
|
||||
return
|
||||
await asyncio.sleep(1)
|
||||
|
||||
try:
|
||||
await scan_one_url(url, skip_html_check=skip_html_check)
|
||||
except Exception as e:
|
||||
print(f"❌ {url} 스캔 중 오류 발생: {e}")
|
||||
continue
|
||||
|
||||
# 스캔 완료 후 재시도 큐 상태 확인
|
||||
retry_status_after = await get_retry_queue_status()
|
||||
if retry_status_after["queue_length"] > 0:
|
||||
prev_progress = load_progress()
|
||||
if prev_progress and prev_progress.get("start_line") == start_line:
|
||||
print("📋 이전 진행 상황을 발견했습니다:")
|
||||
print(
|
||||
f"📊 스캔 완료 후 재시도 큐 상태: {retry_status_after['queue_length']}개 작업 대기 중"
|
||||
f" - 이전 완료: {prev_progress['current_index']}/{prev_progress['total']}"
|
||||
)
|
||||
print(f" - 마지막 처리: {prev_progress.get('current_url', 'N/A')}")
|
||||
|
||||
# 다음 URL로 진행
|
||||
current_progress["current_index"] = current_url_index + 1
|
||||
save_progress()
|
||||
resume = input("이어서 진행하시겠습니까? (y/n): ").lower().strip()
|
||||
if resume == "y":
|
||||
start_index = prev_progress.get("current_index", 0)
|
||||
current_progress["current_index"] = start_index
|
||||
# 전체 개수는 원래 목록 길이로 유지
|
||||
current_progress["total"] = total_count
|
||||
target_list = target_list[start_index:]
|
||||
print(f"✅ {start_index}번째부터 재개합니다.")
|
||||
|
||||
# 모든 URL 처리 완료 후 재시도 큐가 빌 때까지 대기
|
||||
if not is_shutdown_requested():
|
||||
print("\n🔄 모든 URL 처리 완료. 재시도 큐 처리 대기 중...")
|
||||
while True:
|
||||
for i, url in enumerate(target_list):
|
||||
# 종료 요청 체크
|
||||
if is_shutdown_requested():
|
||||
print("🛑 재시도 큐 대기 중 종료 요청으로 중단합니다.")
|
||||
return
|
||||
|
||||
retry_status = await get_retry_queue_status()
|
||||
if retry_status["queue_length"] == 0:
|
||||
print("🛑 종료 요청으로 인해 스캔을 중단합니다.")
|
||||
break
|
||||
|
||||
# current_index는 전체 목록에서의 현재 위치를 나타냄
|
||||
current_url_index = current_progress["current_index"]
|
||||
current_progress["current_url"] = url
|
||||
|
||||
print(
|
||||
f"⏳ 재시도 큐에 {retry_status['queue_length']}개 작업 남음. 30초 후 다시 확인..."
|
||||
f"\n🔄 Processing {current_url_index + 1}/{current_progress['total']}: {url}"
|
||||
)
|
||||
# 대기 중에도 종료 요청 체크
|
||||
for _ in range(30):
|
||||
print(
|
||||
f"📍 {os.path.basename(filepath)}의 {start_line + current_url_index}번째 줄"
|
||||
)
|
||||
|
||||
# 재시도 큐 상태 확인 및 출력
|
||||
retry_status = await get_retry_queue_status()
|
||||
if retry_status["queue_length"] > 0:
|
||||
print(f"⏳ 재시도 큐에 {retry_status['queue_length']}개 작업 대기 중")
|
||||
|
||||
if i > 0:
|
||||
print("⏳ API 쿼터 보호를 위해 30초 대기 중...")
|
||||
# 대기 중에도 종료 요청 체크
|
||||
for _ in range(30):
|
||||
if is_shutdown_requested():
|
||||
print("🛑 대기 중 종료 요청으로 스캔을 중단합니다.")
|
||||
return
|
||||
await asyncio.sleep(1)
|
||||
|
||||
try:
|
||||
await scan_one_url(url, skip_html_check=skip_html_check)
|
||||
except Exception as e:
|
||||
print(f"❌ {url} 스캔 중 오류 발생: {e}")
|
||||
continue
|
||||
|
||||
# 스캔 완료 후 재시도 큐 상태 확인
|
||||
retry_status_after = await get_retry_queue_status()
|
||||
if retry_status_after["queue_length"] > 0:
|
||||
print(
|
||||
f"📊 스캔 완료 후 재시도 큐 상태: {retry_status_after['queue_length']}개 작업 대기 중"
|
||||
)
|
||||
|
||||
# 다음 URL로 진행
|
||||
current_progress["current_index"] = current_url_index + 1
|
||||
save_progress()
|
||||
|
||||
# 모든 URL 처리 완료 후 재시도 큐가 빌 때까지 대기
|
||||
if not is_shutdown_requested():
|
||||
print("\n🔄 모든 URL 처리 완료. 재시도 큐 처리 대기 중...")
|
||||
while True:
|
||||
if is_shutdown_requested():
|
||||
print("🛑 재시도 큐 대기 중 종료 요청으로 중단합니다.")
|
||||
return
|
||||
await asyncio.sleep(1)
|
||||
break
|
||||
|
||||
print(f"\n🎉 모든 스캔이 완료되었습니다! ({total_count}개 URL)")
|
||||
print("🎉 재시도 큐도 모두 처리되었습니다!")
|
||||
else:
|
||||
print("\n🛑 종료 요청으로 인해 스캔이 중단되었습니다.")
|
||||
retry_status = await get_retry_queue_status()
|
||||
if retry_status["queue_length"] == 0:
|
||||
break
|
||||
print(
|
||||
f"⏳ 재시도 큐에 {retry_status['queue_length']}개 작업 남음. 30초 후 다시 확인..."
|
||||
)
|
||||
# 대기 중에도 종료 요청 체크
|
||||
for _ in range(30):
|
||||
if is_shutdown_requested():
|
||||
print("🛑 재시도 큐 대기 중 종료 요청으로 중단합니다.")
|
||||
break
|
||||
await asyncio.sleep(1)
|
||||
|
||||
if not is_shutdown_requested():
|
||||
print(f"\n🎉 모든 스캔이 완료되었습니다! ({total_count}개 URL)")
|
||||
print("🎉 재시도 큐도 모두 처리되었습니다!")
|
||||
else:
|
||||
print("\n🛑 종료 요청으로 인해 스캔이 중단되었습니다.")
|
||||
else:
|
||||
print("\n🛑 종료 요청으로 인해 스캔이 중단되었습니다.")
|
||||
|
||||
finally:
|
||||
# 항상 리소스 정리
|
||||
print("🔄 브라우저 리소스를 정리합니다...")
|
||||
await cleanup_browser_resources()
|
||||
|
|
|
|||
|
|
@ -30,8 +30,10 @@ Instructions:
|
|||
3. Click the **SSO login button**.
|
||||
4. Check if the user is **already logged and immediately redirected back to the original site** without showing a login screen.
|
||||
- ✅ If so, treat the login as successful and return immediately.
|
||||
5. If login proceeds without interruptions, wait for redirection back to the original site and record the final URL.
|
||||
6. Close your browser window after the login is completed.
|
||||
5. If login proceeds without interruptions, complete the login and **immediately close the browser window**. Do not perform any further actions.
|
||||
6. Login is considered successful if:
|
||||
- You are redirected to a page that indicates successful login (e.g., a welcome page, dashboard, or account page).
|
||||
- If a page such as a sign-up page appears, consider it a successful login and terminate immediately.
|
||||
|
||||
Credentials to use for login:
|
||||
- Google → `{google_id}` / `{google_password}`
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ Instructions:
|
|||
a. If a **CAPTCHA**, complete it.
|
||||
b. If a **MFA prompt**, or a request for **ID/password entry** appears, do NOT proceed - Immediately stop and return the appropriate status.
|
||||
- If a **"Continue"**, **"Trust"**, **"Authorize"**, or **"Allow"** button is displayed, click it to grant consent.
|
||||
7. If login proceeds without interruptions, wait for redirection back to the original site and record the final URL.
|
||||
8. Close your browser window after the login is completed.
|
||||
7. If login proceeds without interruptions, complete the login and **immediately close the browser window**. Do not perform any further actions.
|
||||
8. Login is considered successful if:
|
||||
- You are redirected to a page that indicates successful login (e.g., a welcome page, dashboard, or account page).
|
||||
- If a page such as a sign-up page appears, consider it a successful login and terminate immediately.
|
||||
|
||||
Credentials to use for Apple login:
|
||||
- Email: {os.getenv("APPLE_EMAIL", "")}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,9 @@ Instructions:
|
|||
- Click "Create account", "Sign up", or "Complete registration" button
|
||||
- Only after completing ALL steps, record the final URL as successful login
|
||||
9. If all steps are completed successfully, close your browser window.
|
||||
10. Login is considered successful if:
|
||||
- You are redirected to a page that indicates successful login (e.g., a welcome page, dashboard, or account page).
|
||||
- If a page such as a sign-up page appears, consider it a successful login and terminate immediately.
|
||||
|
||||
Credentials to use for Facebook login (if needed):
|
||||
- Email/Phone: {os.getenv("FACEBOOK_EMAIL", "")}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,10 @@ Instructions:
|
|||
|
||||
8. Close your browser window after the login is completed.
|
||||
|
||||
9. Login is considered successful if:
|
||||
- You are redirected to a page that indicates successful login (e.g., a welcome page, dashboard, or account page).
|
||||
- If a page such as a sign-up page appears, consider it a successful login and terminate immediately.
|
||||
|
||||
Credentials to use for GitHub login:
|
||||
- Email: {os.getenv("GITHUB_EMAIL", "")}
|
||||
- Password: {os.getenv("GITHUB_PASSWORD", "")}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ Instructions:
|
|||
d. Click the "Sign in" or "Next" button.
|
||||
7. If login proceeds without interruptions, wait for redirection back to the original site and record the final URL.
|
||||
8. Close your browser window after the login is completed.
|
||||
9. Login is considered successful if:
|
||||
- You are redirected to a page that indicates successful login (e.g., a welcome page, dashboard, or account page).
|
||||
- If a page such as a sign-up page appears, consider it a successful login and terminate immediately.
|
||||
|
||||
Credentials to use for Google login:
|
||||
- Email: {os.getenv("GOOGLE_EMAIL", "")}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ prompt = f"""
|
|||
- Email: {os.getenv("MICROSOFT_EMAIL", "")}
|
||||
- Password: {os.getenv("MICROSOFT_PASSWORD", "")}
|
||||
9. 로그인 완료 후 브라우저 창을 닫으세요.
|
||||
10. Login is considered successful if:
|
||||
- You are redirected to a page that indicates successful login (e.g., a welcome page, dashboard, or account page).
|
||||
- If a page such as a sign-up page appears, consider it a successful login and terminate immediately.
|
||||
|
||||
제약 사항:
|
||||
- 검색 엔진을 사용하거나 URL을 추측하지 마세요.
|
||||
|
|
|
|||
|
|
@ -35,15 +35,33 @@ def load_progress():
|
|||
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
"""Ctrl+C 시그널 핸들러 - 강제 종료"""
|
||||
global shutdown_requested
|
||||
"""Ctrl+C 시그널 핸들러 - browser-use pause 기능과 호환"""
|
||||
global shutdown_requested, ctrl_c_count, last_ctrl_c_time
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
with shutdown_lock:
|
||||
if shutdown_requested:
|
||||
# 이미 종료 요청이 있었다면 즉시 강제 종료
|
||||
print("\n<EFBFBD> 강제 종료합니다!")
|
||||
# 연속된 Ctrl+C 감지 (2초 내에 두 번 누르면 강제 종료)
|
||||
if current_time - last_ctrl_c_time < 2.0:
|
||||
ctrl_c_count += 1
|
||||
else:
|
||||
ctrl_c_count = 1
|
||||
|
||||
last_ctrl_c_time = current_time
|
||||
|
||||
# 두 번째 Ctrl+C이거나 이미 종료 요청이 있었다면 강제 종료
|
||||
if ctrl_c_count >= 2 or shutdown_requested:
|
||||
print("\n⚡ 강제 종료합니다!")
|
||||
import asyncio
|
||||
try:
|
||||
loop = asyncio.get_running_loop()
|
||||
for task in asyncio.all_tasks(loop):
|
||||
task.cancel()
|
||||
except RuntimeError:
|
||||
pass
|
||||
os._exit(1)
|
||||
|
||||
# 첫 번째 Ctrl+C: 정상 종료 요청
|
||||
shutdown_requested = True
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
|
|
@ -62,15 +80,10 @@ def signal_handler(signum, frame):
|
|||
save_progress()
|
||||
print(f"💾 진행 상황이 {progress_file}에 저장되었습니다.")
|
||||
print("다음에 같은 명령어로 실행하면 이어서 진행할 수 있습니다.")
|
||||
print("\n🔄 정리 작업 중... (다시 Ctrl+C를 누르면 강제 종료)")
|
||||
print("<EFBFBD> 2초 내에 Ctrl+C를 다시 누르면 강제 종료됩니다.")
|
||||
|
||||
# 정리 작업을 위해 잠시 대기 후 종료
|
||||
def delayed_exit():
|
||||
time.sleep(2) # 2초 후 자동 종료
|
||||
print("\n✅ 정리 완료. 프로그램을 종료합니다.")
|
||||
os._exit(0)
|
||||
|
||||
threading.Thread(target=delayed_exit, daemon=True).start()
|
||||
# 정상적인 종료를 위해 KeyboardInterrupt 발생
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
|
||||
def is_shutdown_requested():
|
||||
|
|
@ -78,6 +91,29 @@ def is_shutdown_requested():
|
|||
with shutdown_lock:
|
||||
return shutdown_requested
|
||||
|
||||
def request_shutdown():
|
||||
"""외부에서 종료를 요청할 수 있는 함수"""
|
||||
global shutdown_requested
|
||||
with shutdown_lock:
|
||||
if not shutdown_requested:
|
||||
shutdown_requested = True
|
||||
print("\n🛑 종료가 요청되었습니다.")
|
||||
print(f"📊 현재 진행 상황:")
|
||||
print(f" - 전체: {current_progress['total']}개 URL")
|
||||
print(f" - 완료: {current_progress['current_index']}개 URL")
|
||||
print(f" - 현재 처리 중: {current_progress['current_url']}")
|
||||
if current_progress.get('start_line'):
|
||||
print(f" - domains.txt의 {current_progress['start_line'] + current_progress['current_index']}번째 줄")
|
||||
if current_progress["total"] > 0:
|
||||
print(f" - 진행률: {current_progress['current_index']}/{current_progress['total']} ({current_progress['current_index']/current_progress['total']*100:.1f}%)")
|
||||
|
||||
save_progress()
|
||||
print(f"💾 진행 상황이 {progress_file}에 저장되었습니다.")
|
||||
print("다음에 같은 명령어로 실행하면 이어서 진행할 수 있습니다.")
|
||||
|
||||
|
||||
def setup_signal_handler():
|
||||
"""시그널 핸들러 등록"""
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
"""시그널 핸들러 등록 - browser-use와의 호환성을 위해 비활성화"""
|
||||
# browser-use 라이브러리가 자체적으로 Ctrl+C 처리를 하므로
|
||||
# 우리의 signal handler는 등록하지 않음
|
||||
pass
|
||||
|
|
|
|||
116
src/lib/utils/progress_v2.py
Normal file
116
src/lib/utils/progress_v2.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""
|
||||
종료 처리를 위한 개선된 모듈
|
||||
browser-use의 pause 기능과 호환되도록 설계
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
import time
|
||||
import threading
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
# 진행 상황 추적을 위한 전역 변수
|
||||
current_progress = {"current_index": 0, "total": 0, "current_url": "", "start_line": 0}
|
||||
progress_file = Path("data/scan_progress.json")
|
||||
|
||||
# 종료 관리를 위한 전역 변수
|
||||
shutdown_requested = False
|
||||
shutdown_lock = threading.Lock()
|
||||
original_handler = None
|
||||
|
||||
|
||||
def save_progress():
|
||||
"""현재 진행 상황을 파일에 저장"""
|
||||
progress_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(progress_file, "w", encoding="utf-8") as f:
|
||||
json.dump(current_progress, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def load_progress():
|
||||
"""이전 진행 상황을 파일에서 불러오기"""
|
||||
if os.path.exists(progress_file):
|
||||
try:
|
||||
with open(progress_file, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except (json.JSONDecodeError, IOError):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def request_shutdown():
|
||||
"""종료 요청 함수 - 외부에서 호출 가능"""
|
||||
global shutdown_requested
|
||||
with shutdown_lock:
|
||||
if not shutdown_requested:
|
||||
shutdown_requested = True
|
||||
print("\n🛑 종료가 요청되었습니다. 현재 작업을 완료한 후 종료합니다...")
|
||||
save_progress()
|
||||
print(f"💾 진행 상황이 {progress_file}에 저장되었습니다.")
|
||||
|
||||
|
||||
def is_shutdown_requested():
|
||||
"""종료 요청 상태를 확인하는 함수"""
|
||||
with shutdown_lock:
|
||||
return shutdown_requested
|
||||
|
||||
|
||||
def cleanup_signal_handler():
|
||||
"""signal handler를 정리하고 원래 상태로 복원"""
|
||||
global original_handler
|
||||
if original_handler is not None:
|
||||
signal.signal(signal.SIGINT, original_handler)
|
||||
original_handler = None
|
||||
|
||||
|
||||
def setup_minimal_signal_handler():
|
||||
"""최소한의 signal handler만 설정 - browser-use와 충돌 방지"""
|
||||
global original_handler
|
||||
|
||||
# 원래 핸들러 저장
|
||||
original_handler = signal.signal(signal.SIGINT, signal.SIG_DFL)
|
||||
|
||||
def graceful_signal_handler(signum, frame):
|
||||
"""우아한 종료를 위한 최소한의 signal handler"""
|
||||
print("\n🛑 종료 신호를 받았습니다...")
|
||||
save_progress()
|
||||
print(f"💾 진행 상황이 {progress_file}에 저장되었습니다.")
|
||||
|
||||
# 원래 핸들러로 복원하고 신호를 다시 발생시킴
|
||||
signal.signal(signal.SIGINT, original_handler)
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
|
||||
signal.signal(signal.SIGINT, graceful_signal_handler)
|
||||
|
||||
|
||||
class GracefulShutdown:
|
||||
"""컨텍스트 매니저로 사용할 수 있는 우아한 종료 클래스"""
|
||||
|
||||
def __init__(self):
|
||||
self.original_handler = None
|
||||
|
||||
def __enter__(self):
|
||||
self.original_handler = signal.signal(signal.SIGINT, self._signal_handler)
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.original_handler is not None:
|
||||
signal.signal(signal.SIGINT, self.original_handler)
|
||||
|
||||
def _signal_handler(self, signum, frame):
|
||||
"""내부 signal handler"""
|
||||
request_shutdown()
|
||||
# 원래 핸들러 복원 후 신호 재전송
|
||||
signal.signal(signal.SIGINT, self.original_handler)
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
|
||||
|
||||
# 기존 함수들과의 호환성을 위한 별칭
|
||||
def setup_signal_handler():
|
||||
"""기존 코드와의 호환성을 위한 함수"""
|
||||
pass # browser-use의 signal handler를 방해하지 않음
|
||||
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
"""기존 코드와의 호환성을 위한 함수"""
|
||||
request_shutdown()
|
||||
34
src/main.py
34
src/main.py
|
|
@ -73,10 +73,17 @@ def main():
|
|||
if os.path.exists(log_file):
|
||||
with open(log_file, "r") as f:
|
||||
tmp_user_data_dir = f.read().strip()
|
||||
os.remove(tmp_user_data_dir)
|
||||
os.remove(log_file)
|
||||
print(f"🔧 강제로 종료되기 전에 사용한 {tmp_user_data_dir}를 삭제하였습니다.")
|
||||
|
||||
try:
|
||||
import shutil
|
||||
if os.path.exists(tmp_user_data_dir):
|
||||
shutil.rmtree(tmp_user_data_dir)
|
||||
print(f"🔧 이전 실행의 임시 사용자 데이터 디렉토리 {tmp_user_data_dir}를 삭제하였습니다.")
|
||||
except (PermissionError, FileNotFoundError, OSError) as e:
|
||||
print(f"⚠️ 임시 사용자 데이터 디렉토리 삭제 실패: {e}")
|
||||
try:
|
||||
os.remove(log_file)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
try:
|
||||
asyncio.run(
|
||||
|
|
@ -88,29 +95,26 @@ def main():
|
|||
)
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\n사용자에 의해 중단되었습니다. 현재까지의 작업을 저장합니다...")
|
||||
from lib.utils.progress import save_progress
|
||||
|
||||
save_progress()
|
||||
print(f"💾 진행 상황이 {progress_file}에 저장되었습니다.")
|
||||
print("다음에 같은 명령어로 실행하면 이어서 진행할 수 있습니다.")
|
||||
# terminate
|
||||
print("\n🛑 사용자에 의해 중단되었습니다.")
|
||||
# 진행 상황 저장
|
||||
from lib.utils.progress import save_progress, request_shutdown
|
||||
request_shutdown()
|
||||
print("✅ 정리 완료.")
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 예상치 못한 오류가 발생했습니다: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
sys.exit(1)
|
||||
finally:
|
||||
# 정상 종료 시 진행 상황 파일 삭제 (종료 요청이 아닌 경우에만)
|
||||
# 정상 종료 시에만 진행 상황 파일 삭제
|
||||
from lib.utils.progress import is_shutdown_requested
|
||||
if not is_shutdown_requested() and os.path.exists(progress_file):
|
||||
try:
|
||||
os.remove(progress_file)
|
||||
print("진행 상황 파일이 삭제되었습니다.")
|
||||
print("✅ 진행 상황 파일이 삭제되었습니다.")
|
||||
except OSError as e:
|
||||
print(f"오류: 진행 상황 파일을 삭제하지 못했습니다. {e}", file=sys.stderr)
|
||||
print(f"⚠️ 진행 상황 파일 삭제 실패: {e}", file=sys.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue