mirror of
https://github.com/j93es/browser-use-oauth.git
synced 2026-06-04 02:21:52 +09:00
Merge pull request #38 from j93es/feat/some
feat: Browser use v0.5.3 업데이트 및 프로세스 안정성 개선
This commit is contained in:
commit
9858d7acd2
18 changed files with 911 additions and 710 deletions
|
|
@ -6,7 +6,7 @@ readme = "README.md"
|
|||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"black>=25.1.0",
|
||||
"browser-use[memory]==0.3.3",
|
||||
"browser-use[memory]==0.5.3",
|
||||
"chardet>=5.2.0",
|
||||
"isort>=6.0.1",
|
||||
"lmnr[all]>=0.6.10",
|
||||
|
|
|
|||
97
run.py
97
run.py
|
|
@ -1,5 +1,6 @@
|
|||
import argparse
|
||||
import os
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
|
@ -13,42 +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")
|
||||
|
||||
subprocess.run(command, check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
print("Python 스크립트 실행 실패")
|
||||
process = subprocess.Popen(command)
|
||||
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(
|
||||
|
|
@ -59,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()
|
||||
|
||||
|
|
@ -82,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)
|
||||
|
||||
# 스크립트 실행
|
||||
|
|
@ -90,5 +160,6 @@ def main():
|
|||
|
||||
print("처리 완료.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
from lib.browser_use.agents import *
|
||||
from lib.browser_use.clean_resources import *
|
||||
from lib.browser_use.func import *
|
||||
from lib.browser_use.init_profile import *
|
||||
from lib.browser_use.model import *
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, Optional
|
||||
|
|
@ -8,7 +9,6 @@ from typing import Any, Dict, Optional
|
|||
from browser_use import Agent, BrowserSession, Controller
|
||||
from patchright.async_api import async_playwright as async_patchright
|
||||
|
||||
from lib.browser_use.clean_resources import clean_resources
|
||||
from lib.browser_use.init_profile import GetProfile
|
||||
from lib.browser_use.sensitive_data import GetSensitiveData
|
||||
from lib.llm import CreateChatGoogle, get_prompt
|
||||
|
|
@ -147,15 +147,15 @@ async def _run_agent_with_retry(agent_config):
|
|||
|
||||
while try_cnt < 3:
|
||||
try:
|
||||
Profile = await GetProfile(headless=headless)
|
||||
session = BrowserSession(
|
||||
playwright=(await async_patchright().start()),
|
||||
browser_profile=await GetProfile(headless=headless),
|
||||
browser_profile=Profile[0],
|
||||
)
|
||||
|
||||
agent = Agent(browser_session=session, **agent_config["agent_params"])
|
||||
|
||||
response = await agent.run()
|
||||
await clean_resources(agent, session)
|
||||
|
||||
if any(
|
||||
keyword in str(response)
|
||||
|
|
@ -180,11 +180,14 @@ async def _run_agent_with_retry(agent_config):
|
|||
await add_to_retry_queue(task)
|
||||
return None
|
||||
|
||||
# remove profile
|
||||
if Profile[1]:
|
||||
shutil.rmtree(Profile[1], ignore_errors=True)
|
||||
print(f"🗑️ 임시 프로필 디렉토리 삭제 완료: {Profile[1]}")
|
||||
|
||||
return response
|
||||
|
||||
except Exception as e:
|
||||
await clean_resources(agent, session)
|
||||
|
||||
# 일반 에러 처리
|
||||
try_cnt += 1
|
||||
if try_cnt >= 3:
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
from pathlib import Path
|
||||
|
||||
|
||||
async def clean_resources(agent=None, session=None):
|
||||
"""리소스를 정리하는 함수"""
|
||||
storage_state_temp_path = Path("./data/storage_state_temp.json").resolve()
|
||||
if storage_state_temp_path.exists():
|
||||
try:
|
||||
# remove file
|
||||
print(f"🗑️ 임시 스토리지 상태 파일 삭제 중: {storage_state_temp_path}")
|
||||
# unlink removes the file
|
||||
storage_state_temp_path.unlink()
|
||||
print("🗑️ 임시 스토리지 상태 파일 삭제 완료.")
|
||||
except Exception as e:
|
||||
print(f"⚠️ 임시 스토리지 상태 파일 삭제 실패: {e}")
|
||||
|
||||
if agent:
|
||||
try:
|
||||
await agent.close()
|
||||
except Exception as e:
|
||||
print(f"⚠️ 에이전트 리소스 정리 실패: {e}")
|
||||
if session:
|
||||
try:
|
||||
await session.close()
|
||||
except Exception as e:
|
||||
print(f"⚠️ 세션 리소스 정리 실패: {e}")
|
||||
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,34 +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()
|
||||
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)
|
||||
|
||||
# 새 임시 디렉토리 경로 로깅
|
||||
with open(log_file, "w") as f:
|
||||
f.write(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},
|
||||
# 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']
|
||||
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
|
||||
return [profile, tmp_user_data_dir] if tmp_user_data_dir else [profile]
|
||||
|
|
|
|||
|
|
@ -8,9 +8,11 @@ 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,
|
||||
|
|
@ -75,6 +77,7 @@ async def main_loop(
|
|||
filepath: str, start_line: int, end_line: int, skip_html_check: bool = False
|
||||
):
|
||||
"""지정된 URL 목록에 대해 스캔을 실행하는 메인 루프"""
|
||||
try:
|
||||
# 재시도 큐 처리기 시작
|
||||
await start_retry_queue_processor()
|
||||
|
||||
|
|
@ -106,6 +109,11 @@ async def main_loop(
|
|||
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
|
||||
|
|
@ -124,9 +132,18 @@ async def main_loop(
|
|||
|
||||
if i > 0:
|
||||
print("⏳ API 쿼터 보호를 위해 30초 대기 중...")
|
||||
await asyncio.sleep(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()
|
||||
|
|
@ -140,15 +157,35 @@ async def main_loop(
|
|||
save_progress()
|
||||
|
||||
# 모든 URL 처리 완료 후 재시도 큐가 빌 때까지 대기
|
||||
if not is_shutdown_requested():
|
||||
print("\n🔄 모든 URL 처리 완료. 재시도 큐 처리 대기 중...")
|
||||
while True:
|
||||
if is_shutdown_requested():
|
||||
print("🛑 재시도 큐 대기 중 종료 요청으로 중단합니다.")
|
||||
break
|
||||
|
||||
retry_status = await get_retry_queue_status()
|
||||
if retry_status["queue_length"] == 0:
|
||||
break
|
||||
print(
|
||||
f"⏳ 재시도 큐에 {retry_status['queue_length']}개 작업 남음. 30초 후 다시 확인..."
|
||||
)
|
||||
await asyncio.sleep(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,7 +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.
|
||||
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}`
|
||||
|
|
@ -38,7 +41,6 @@ Credentials to use for login:
|
|||
- GitHub → `{github_id}` / `{github_password}`
|
||||
- facebook → `{facebook_id}` / `{facebook_password}`
|
||||
- Microsoft → `{microsoft_id}` / `{microsoft_password}`
|
||||
If credentials for a provider are not provided, skip the login attempt for that provider.
|
||||
|
||||
Constraints:
|
||||
- Do NOT use search engines or guess URLs.
|
||||
|
|
|
|||
|
|
@ -26,7 +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.
|
||||
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", "")}
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ Instructions:
|
|||
- Password: "TestPassword123!"
|
||||
- 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", "")}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,12 @@ Instructions:
|
|||
|
||||
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 GitHub login:
|
||||
- Email: {os.getenv("GITHUB_EMAIL", "")}
|
||||
- Password: {os.getenv("GITHUB_PASSWORD", "")}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ Instructions:
|
|||
c. Wait for the password input field, then enter the password: {os.getenv("GOOGLE_PASSWORD", "")}
|
||||
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", "")}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,10 @@ prompt = f"""
|
|||
8. 로그인 되어있지 않으면 아래의 EMAIL과 PASSWORD를 사용하여 로그인하세요:
|
||||
- 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을 추측하지 마세요.
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
import json
|
||||
import os
|
||||
import os, sys
|
||||
import signal
|
||||
import time
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
# 진행 상황 추적을 위한 전역 변수
|
||||
current_progress = {"current_index": 0, "total": 0, "current_url": "", "start_line": 0}
|
||||
progress_file = Path("data/scan_progress.json")
|
||||
|
||||
# Ctrl+C 처리를 위한 전역 변수
|
||||
ctrl_c_count = 0
|
||||
last_ctrl_c_time = 0
|
||||
shutdown_requested = False
|
||||
shutdown_lock = threading.Lock()
|
||||
|
||||
|
||||
def save_progress():
|
||||
"""현재 진행 상황을 파일에 저장"""
|
||||
|
|
@ -27,26 +35,85 @@ def load_progress():
|
|||
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
"""Ctrl+C 시그널 핸들러"""
|
||||
"""Ctrl+C 시그널 핸들러 - browser-use pause 기능과 호환"""
|
||||
global shutdown_requested, ctrl_c_count, last_ctrl_c_time
|
||||
|
||||
current_time = time.time()
|
||||
|
||||
with shutdown_lock:
|
||||
# 연속된 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)
|
||||
print("🛑 스캔이 중단되었습니다!")
|
||||
print(f"📊 진행 상황:")
|
||||
print("🛑 종료 신호를 받았습니다!")
|
||||
print(f"📊 현재 진행 상황:")
|
||||
print(f" - 전체: {current_progress['total']}개 URL")
|
||||
print(f" - 완료: {current_progress['current_index']}개 URL")
|
||||
print(f" - 현재 처리 중: {current_progress['current_url']}")
|
||||
print(
|
||||
f" - domains.txt의 {current_progress['start_line'] + current_progress['current_index']}번째 줄"
|
||||
)
|
||||
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}%)"
|
||||
)
|
||||
print(f" - 진행률: {current_progress['current_index']}/{current_progress['total']} ({current_progress['current_index']/current_progress['total']*100:.1f}%)")
|
||||
print("=" * 60)
|
||||
|
||||
# 진행 상황 저장
|
||||
save_progress()
|
||||
print(f"💾 진행 상황이 {progress_file}에 저장되었습니다.")
|
||||
exit(0)
|
||||
print("다음에 같은 명령어로 실행하면 이어서 진행할 수 있습니다.")
|
||||
print("<EFBFBD> 2초 내에 Ctrl+C를 다시 누르면 강제 종료됩니다.")
|
||||
|
||||
# 정상적인 종료를 위해 KeyboardInterrupt 발생
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
|
||||
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()
|
||||
41
src/main.py
41
src/main.py
|
|
@ -26,6 +26,8 @@ def setup_environment():
|
|||
Laminar.initialize(project_api_key=os.getenv("LMNR_PROJECT_API_KEY"))
|
||||
except ImportError:
|
||||
print("⚠️ Laminar 라이브러리가 설치되지 않았습니다. 관련 기능이 비활성화됩니다.")
|
||||
else:
|
||||
print("⚠️ LMNR_PROJECT_API_KEY 환경 변수가 설정되지 않았습니다. Laminar 기능이 비활성화됩니다.")
|
||||
|
||||
|
||||
def parse_arguments():
|
||||
|
|
@ -64,6 +66,25 @@ def main():
|
|||
setup_signal_handler()
|
||||
args = parse_arguments()
|
||||
|
||||
# read and remove user data path
|
||||
log_file = os.path.join("./data", "userdata.dump")
|
||||
if not os.path.exists("./data"):
|
||||
os.makedirs("./data")
|
||||
if os.path.exists(log_file):
|
||||
with open(log_file, "r") as f:
|
||||
tmp_user_data_dir = f.read().strip()
|
||||
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(
|
||||
main_loop(
|
||||
|
|
@ -74,16 +95,26 @@ def main():
|
|||
)
|
||||
)
|
||||
except KeyboardInterrupt:
|
||||
print("\n프로그램이 사용자에 의해 중단되었습니다.")
|
||||
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:
|
||||
# 정상 종료 시 진행 상황 파일 삭제
|
||||
if os.path.exists(progress_file):
|
||||
# 정상 종료 시에만 진행 상황 파일 삭제
|
||||
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