브라우저 리소스 정리 및 종료 처리 개선, 진행 상황 저장 기능 추가

This commit is contained in:
암냥 2025-07-11 18:37:31 +09:00
commit d6803ad20e
13 changed files with 551 additions and 172 deletions

119
run.py
View file

@ -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()
if process.returncode != 0:
print("Python 스크립트 실행 실패")
sys.exit(1)
except subprocess.CalledProcessError:
print("Python 스크립트 실행 실패")
returncode = process.wait()
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,36 +125,41 @@ 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()
# 라인 범위 검증
if args.start_line < 0 or args.end_line < 0:
print("라인 번호는 0 이상이어야 합니다.")
sys.exit(1)
if args.start_line > args.end_line:
print("시작 라인은 종료 라인보다 크거나 같아야 합니다.")
sys.exit(1)
# 도메인 파일 다운로드
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)
# 스크립트 실행
run_script(args.start_line, args.end_line, args.skh)
print("처리 완료.")
if __name__ == "__main__":
main()
main()

View 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("✅ 긴급 리소스 정리 완료.")

View file

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

View file

@ -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
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)
print(f"\n🎉 모든 스캔이 완료되었습니다! ({total_count}개 URL)")
print("🎉 재시도 큐도 모두 처리되었습니다!")
else:
print("\n🛑 종료 요청으로 인해 스캔이 중단되었습니다.")
if not is_shutdown_requested():
print(f"\n🎉 모든 스캔이 완료되었습니다! ({total_count}개 URL)")
print("🎉 재시도 큐도 모두 처리되었습니다!")
else:
print("\n🛑 종료 요청으로 인해 스캔이 중단되었습니다.")
else:
print("\n🛑 종료 요청으로 인해 스캔이 중단되었습니다.")
finally:
# 항상 리소스 정리
print("🔄 브라우저 리소스를 정리합니다...")
await cleanup_browser_resources()

View file

@ -30,9 +30,11 @@ 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}`
- Naver `{naver_id}` / `{naver_password}`

View file

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

View file

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

View file

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

View file

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

View file

@ -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을 추측하지 마세요.

View file

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

View 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()

View file

@ -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__":