mirror of
https://github.com/j93es/browser-use-oauth.git
synced 2026-06-04 05: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"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"black>=25.1.0",
|
"black>=25.1.0",
|
||||||
"browser-use[memory]==0.3.3",
|
"browser-use[memory]==0.5.3",
|
||||||
"chardet>=5.2.0",
|
"chardet>=5.2.0",
|
||||||
"isort>=6.0.1",
|
"isort>=6.0.1",
|
||||||
"lmnr[all]>=0.6.10",
|
"lmnr[all]>=0.6.10",
|
||||||
|
|
|
||||||
97
run.py
97
run.py
|
|
@ -1,5 +1,6 @@
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
@ -13,42 +14,107 @@ PYTHON_SCRIPT = "./src/main.py"
|
||||||
DOMAIN_FILE = "./data/domains.txt"
|
DOMAIN_FILE = "./data/domains.txt"
|
||||||
# ─────────────
|
# ─────────────
|
||||||
|
|
||||||
|
|
||||||
def download_domains():
|
def download_domains():
|
||||||
"""도메인 파일 다운로드"""
|
"""도메인 파일 다운로드"""
|
||||||
try:
|
try:
|
||||||
print("도메인 파일 다운로드 중...")
|
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()
|
response.raise_for_status()
|
||||||
|
|
||||||
# 디렉토리가 없으면 생성
|
# 디렉토리가 없으면 생성
|
||||||
os.makedirs(os.path.dirname("./data"), exist_ok=True)
|
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)
|
f.write(response.text)
|
||||||
print("도메인 파일 다운로드 완료")
|
print("도메인 파일 다운로드 완료")
|
||||||
except requests.RequestException as e:
|
except requests.RequestException as e:
|
||||||
print(f"도메인 파일 다운로드 실패: {e}")
|
print(f"도메인 파일 다운로드 실패: {e}")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def run_script(start_line, end_line, skh_option):
|
def run_script(start_line, end_line, skh_option):
|
||||||
"""Python 스크립트 실행"""
|
"""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}...")
|
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:
|
try:
|
||||||
command = [
|
command = [
|
||||||
"uv", "run", PYTHON_SCRIPT,
|
"uv",
|
||||||
"-f", DOMAIN_FILE,
|
"run",
|
||||||
"-s", str(start_line),
|
PYTHON_SCRIPT,
|
||||||
"-e", str(end_line),
|
"-f",
|
||||||
|
DOMAIN_FILE,
|
||||||
|
"-s",
|
||||||
|
str(start_line),
|
||||||
|
"-e",
|
||||||
|
str(end_line),
|
||||||
]
|
]
|
||||||
if skh_option:
|
if skh_option:
|
||||||
command.append("--skip-html-check")
|
command.append("--skip-html-check")
|
||||||
|
|
||||||
subprocess.run(command, check=True)
|
process = subprocess.Popen(command)
|
||||||
except subprocess.CalledProcessError:
|
returncode = process.wait()
|
||||||
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)
|
sys.exit(1)
|
||||||
|
finally:
|
||||||
|
# 시그널 핸들러 복원
|
||||||
|
signal.signal(signal.SIGINT, original_sigint)
|
||||||
|
signal.signal(signal.SIGTERM, original_sigterm)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
parser = argparse.ArgumentParser(
|
parser = argparse.ArgumentParser(
|
||||||
|
|
@ -59,13 +125,15 @@ def main():
|
||||||
uv run run.py 10000 11000 # 10000~11000 라인 처리
|
uv run run.py 10000 11000 # 10000~11000 라인 처리
|
||||||
uv run run.py 10000 11000 --skh # SKH 옵션 활성화
|
uv run run.py 10000 11000 --skh # SKH 옵션 활성화
|
||||||
uv run run.py 10000 11000 --no-download # 다운로드 생략
|
uv run run.py 10000 11000 --no-download # 다운로드 생략
|
||||||
"""
|
""",
|
||||||
)
|
)
|
||||||
|
|
||||||
parser.add_argument("start_line", type=int, help="시작 라인 번호")
|
parser.add_argument("start_line", type=int, help="시작 라인 번호")
|
||||||
parser.add_argument("end_line", type=int, help="종료 라인 번호")
|
parser.add_argument("end_line", type=int, help="종료 라인 번호")
|
||||||
parser.add_argument("--skh", action="store_true", help="SKH 옵션 활성화")
|
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()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
@ -82,7 +150,9 @@ def main():
|
||||||
if not args.no_download:
|
if not args.no_download:
|
||||||
download_domains()
|
download_domains()
|
||||||
elif not os.path.exists(DOMAIN_FILE):
|
elif not os.path.exists(DOMAIN_FILE):
|
||||||
print(f"도메인 파일({DOMAIN_FILE})이 존재하지 않습니다. --no-download 옵션을 제거하거나 파일을 준비해주세요.")
|
print(
|
||||||
|
f"도메인 파일({DOMAIN_FILE})이 존재하지 않습니다. --no-download 옵션을 제거하거나 파일을 준비해주세요."
|
||||||
|
)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# 스크립트 실행
|
# 스크립트 실행
|
||||||
|
|
@ -90,5 +160,6 @@ def main():
|
||||||
|
|
||||||
print("처리 완료.")
|
print("처리 완료.")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
from lib.browser_use.agents import *
|
from lib.browser_use.agents import *
|
||||||
from lib.browser_use.clean_resources import *
|
|
||||||
from lib.browser_use.func import *
|
from lib.browser_use.func import *
|
||||||
from lib.browser_use.init_profile import *
|
from lib.browser_use.init_profile import *
|
||||||
from lib.browser_use.model import *
|
from lib.browser_use.model import *
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import shutil
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
@ -8,7 +9,6 @@ from typing import Any, Dict, Optional
|
||||||
from browser_use import Agent, BrowserSession, Controller
|
from browser_use import Agent, BrowserSession, Controller
|
||||||
from patchright.async_api import async_playwright as async_patchright
|
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.init_profile import GetProfile
|
||||||
from lib.browser_use.sensitive_data import GetSensitiveData
|
from lib.browser_use.sensitive_data import GetSensitiveData
|
||||||
from lib.llm import CreateChatGoogle, get_prompt
|
from lib.llm import CreateChatGoogle, get_prompt
|
||||||
|
|
@ -147,15 +147,15 @@ async def _run_agent_with_retry(agent_config):
|
||||||
|
|
||||||
while try_cnt < 3:
|
while try_cnt < 3:
|
||||||
try:
|
try:
|
||||||
|
Profile = await GetProfile(headless=headless)
|
||||||
session = BrowserSession(
|
session = BrowserSession(
|
||||||
playwright=(await async_patchright().start()),
|
playwright=(await async_patchright().start()),
|
||||||
browser_profile=await GetProfile(headless=headless),
|
browser_profile=Profile[0],
|
||||||
)
|
)
|
||||||
|
|
||||||
agent = Agent(browser_session=session, **agent_config["agent_params"])
|
agent = Agent(browser_session=session, **agent_config["agent_params"])
|
||||||
|
|
||||||
response = await agent.run()
|
response = await agent.run()
|
||||||
await clean_resources(agent, session)
|
|
||||||
|
|
||||||
if any(
|
if any(
|
||||||
keyword in str(response)
|
keyword in str(response)
|
||||||
|
|
@ -180,11 +180,14 @@ async def _run_agent_with_retry(agent_config):
|
||||||
await add_to_retry_queue(task)
|
await add_to_retry_queue(task)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
# remove profile
|
||||||
|
if Profile[1]:
|
||||||
|
shutil.rmtree(Profile[1], ignore_errors=True)
|
||||||
|
print(f"🗑️ 임시 프로필 디렉토리 삭제 완료: {Profile[1]}")
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
await clean_resources(agent, session)
|
|
||||||
|
|
||||||
# 일반 에러 처리
|
# 일반 에러 처리
|
||||||
try_cnt += 1
|
try_cnt += 1
|
||||||
if try_cnt >= 3:
|
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):
|
async def GetProfile(headless=False):
|
||||||
|
"""브라우저 프로필을 생성하고 임시 사용자 데이터 디렉토리를 관리합니다."""
|
||||||
user_data_dir = None
|
user_data_dir = None
|
||||||
|
tmp_user_data_dir = None
|
||||||
|
|
||||||
if USER_DATA_DIR and os.path.isdir(USER_DATA_DIR):
|
if USER_DATA_DIR and os.path.isdir(USER_DATA_DIR):
|
||||||
try:
|
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):
|
if os.path.exists(tmp_user_data_dir):
|
||||||
shutil.rmtree(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
|
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:
|
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(
|
profile = BrowserProfile(
|
||||||
# Security settings
|
# Security settings
|
||||||
disable_security=True,
|
disable_security=True,
|
||||||
#stealth=True,
|
|
||||||
# Display settings
|
# Display settings
|
||||||
headless=headless,
|
headless=headless,
|
||||||
device_scale_factor=1,
|
|
||||||
window_size={"width": 1600, "height": 900},
|
|
||||||
# Data persistence
|
# Data persistence
|
||||||
user_data_dir=user_data_dir,
|
user_data_dir=user_data_dir,
|
||||||
#storage_state=storage_state,
|
|
||||||
# Network settings
|
# Network settings
|
||||||
proxy={"server": proxy_url} if proxy_url else None,
|
proxy={"server": proxy_url} if proxy_url else None,
|
||||||
# Additional arguments
|
# Additional arguments
|
||||||
args=get_browser_args(),
|
ignore_default_args=[
|
||||||
ignore_default_args=['--enable-automation']
|
'--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,
|
start_retry_queue_processor,
|
||||||
test_oauth_login,
|
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 import is_html_url, notify_backend, read_lines_between
|
||||||
from lib.utils.progress import (
|
from lib.utils.progress import (
|
||||||
current_progress,
|
current_progress,
|
||||||
|
is_shutdown_requested,
|
||||||
load_progress,
|
load_progress,
|
||||||
progress_file,
|
progress_file,
|
||||||
save_progress,
|
save_progress,
|
||||||
|
|
@ -75,80 +77,115 @@ async def main_loop(
|
||||||
filepath: str, start_line: int, end_line: int, skip_html_check: bool = False
|
filepath: str, start_line: int, end_line: int, skip_html_check: bool = False
|
||||||
):
|
):
|
||||||
"""지정된 URL 목록에 대해 스캔을 실행하는 메인 루프"""
|
"""지정된 URL 목록에 대해 스캔을 실행하는 메인 루프"""
|
||||||
# 재시도 큐 처리기 시작
|
try:
|
||||||
await start_retry_queue_processor()
|
# 재시도 큐 처리기 시작
|
||||||
|
await start_retry_queue_processor()
|
||||||
|
|
||||||
target_list = read_lines_between(
|
target_list = read_lines_between(
|
||||||
filepath=filepath, start_line=start_line, end_line=end_line
|
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):
|
|
||||||
# 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}번째 줄"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 재시도 큐 상태 확인 및 출력
|
# 전체 목록 길이를 저장 (재개 시에도 유지되어야 함)
|
||||||
retry_status = await get_retry_queue_status()
|
total_count = len(target_list)
|
||||||
if retry_status["queue_length"] > 0:
|
current_progress["total"] = total_count
|
||||||
print(f"⏳ 재시도 큐에 {retry_status['queue_length']}개 작업 대기 중")
|
current_progress["start_line"] = start_line
|
||||||
|
current_progress["current_index"] = 0
|
||||||
|
|
||||||
if i > 0:
|
prev_progress = load_progress()
|
||||||
print("⏳ API 쿼터 보호를 위해 30초 대기 중...")
|
if prev_progress and prev_progress.get("start_line") == start_line:
|
||||||
await asyncio.sleep(30)
|
print("📋 이전 진행 상황을 발견했습니다:")
|
||||||
|
|
||||||
await scan_one_url(url, skip_html_check=skip_html_check)
|
|
||||||
|
|
||||||
# 스캔 완료 후 재시도 큐 상태 확인
|
|
||||||
retry_status_after = await get_retry_queue_status()
|
|
||||||
if retry_status_after["queue_length"] > 0:
|
|
||||||
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')}")
|
||||||
|
|
||||||
|
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}번째 줄"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 다음 URL로 진행
|
# 재시도 큐 상태 확인 및 출력
|
||||||
current_progress["current_index"] = current_url_index + 1
|
retry_status = await get_retry_queue_status()
|
||||||
save_progress()
|
if retry_status["queue_length"] > 0:
|
||||||
|
print(f"⏳ 재시도 큐에 {retry_status['queue_length']}개 작업 대기 중")
|
||||||
|
|
||||||
# 모든 URL 처리 완료 후 재시도 큐가 빌 때까지 대기
|
if i > 0:
|
||||||
print("\n🔄 모든 URL 처리 완료. 재시도 큐 처리 대기 중...")
|
print("⏳ API 쿼터 보호를 위해 30초 대기 중...")
|
||||||
while True:
|
# 대기 중에도 종료 요청 체크
|
||||||
retry_status = await get_retry_queue_status()
|
for _ in range(30):
|
||||||
if retry_status["queue_length"] == 0:
|
if is_shutdown_requested():
|
||||||
break
|
print("🛑 대기 중 종료 요청으로 스캔을 중단합니다.")
|
||||||
print(
|
return
|
||||||
f"⏳ 재시도 큐에 {retry_status['queue_length']}개 작업 남음. 30초 후 다시 확인..."
|
await asyncio.sleep(1)
|
||||||
)
|
|
||||||
await asyncio.sleep(30)
|
|
||||||
|
|
||||||
print(f"\n🎉 모든 스캔이 완료되었습니다! ({total_count}개 URL)")
|
try:
|
||||||
print("🎉 재시도 큐도 모두 처리되었습니다!")
|
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("🛑 재시도 큐 대기 중 종료 요청으로 중단합니다.")
|
||||||
|
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)
|
||||||
|
|
||||||
|
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**.
|
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.
|
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.
|
- ✅ 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:
|
Credentials to use for login:
|
||||||
- Google → `{google_id}` / `{google_password}`
|
- Google → `{google_id}` / `{google_password}`
|
||||||
|
|
@ -38,7 +41,6 @@ Credentials to use for login:
|
||||||
- GitHub → `{github_id}` / `{github_password}`
|
- GitHub → `{github_id}` / `{github_password}`
|
||||||
- facebook → `{facebook_id}` / `{facebook_password}`
|
- facebook → `{facebook_id}` / `{facebook_password}`
|
||||||
- Microsoft → `{microsoft_id}` / `{microsoft_password}`
|
- Microsoft → `{microsoft_id}` / `{microsoft_password}`
|
||||||
If credentials for a provider are not provided, skip the login attempt for that provider.
|
|
||||||
|
|
||||||
Constraints:
|
Constraints:
|
||||||
- Do NOT use search engines or guess URLs.
|
- Do NOT use search engines or guess URLs.
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,10 @@ Instructions:
|
||||||
a. If a **CAPTCHA**, complete it.
|
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.
|
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.
|
- 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:
|
Credentials to use for Apple login:
|
||||||
- Email: {os.getenv("APPLE_EMAIL", "")}
|
- Email: {os.getenv("APPLE_EMAIL", "")}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@ Instructions:
|
||||||
- Password: "TestPassword123!"
|
- Password: "TestPassword123!"
|
||||||
- Click "Create account", "Sign up", or "Complete registration" button
|
- Click "Create account", "Sign up", or "Complete registration" button
|
||||||
- Only after completing ALL steps, record the final URL as successful login
|
- 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):
|
Credentials to use for Facebook login (if needed):
|
||||||
- Email/Phone: {os.getenv("FACEBOOK_EMAIL", "")}
|
- 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.
|
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:
|
Credentials to use for GitHub login:
|
||||||
- Email: {os.getenv("GITHUB_EMAIL", "")}
|
- Email: {os.getenv("GITHUB_EMAIL", "")}
|
||||||
- Password: {os.getenv("GITHUB_PASSWORD", "")}
|
- 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", "")}
|
c. Wait for the password input field, then enter the password: {os.getenv("GOOGLE_PASSWORD", "")}
|
||||||
d. Click the "Sign in" or "Next" button.
|
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.
|
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:
|
Credentials to use for Google login:
|
||||||
- Email: {os.getenv("GOOGLE_EMAIL", "")}
|
- Email: {os.getenv("GOOGLE_EMAIL", "")}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,10 @@ prompt = f"""
|
||||||
8. 로그인 되어있지 않으면 아래의 EMAIL과 PASSWORD를 사용하여 로그인하세요:
|
8. 로그인 되어있지 않으면 아래의 EMAIL과 PASSWORD를 사용하여 로그인하세요:
|
||||||
- Email: {os.getenv("MICROSOFT_EMAIL", "")}
|
- Email: {os.getenv("MICROSOFT_EMAIL", "")}
|
||||||
- Password: {os.getenv("MICROSOFT_PASSWORD", "")}
|
- 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을 추측하지 마세요.
|
- 검색 엔진을 사용하거나 URL을 추측하지 마세요.
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os, sys
|
||||||
import signal
|
import signal
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
# 진행 상황 추적을 위한 전역 변수
|
# 진행 상황 추적을 위한 전역 변수
|
||||||
current_progress = {"current_index": 0, "total": 0, "current_url": "", "start_line": 0}
|
current_progress = {"current_index": 0, "total": 0, "current_url": "", "start_line": 0}
|
||||||
progress_file = Path("data/scan_progress.json")
|
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():
|
def save_progress():
|
||||||
"""현재 진행 상황을 파일에 저장"""
|
"""현재 진행 상황을 파일에 저장"""
|
||||||
|
|
@ -27,26 +35,85 @@ def load_progress():
|
||||||
|
|
||||||
|
|
||||||
def signal_handler(signum, frame):
|
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("\n" + "=" * 60)
|
||||||
print("🛑 스캔이 중단되었습니다!")
|
print("🛑 종료 신호를 받았습니다!")
|
||||||
print(f"📊 진행 상황:")
|
print(f"📊 현재 진행 상황:")
|
||||||
print(f" - 전체: {current_progress['total']}개 URL")
|
print(f" - 전체: {current_progress['total']}개 URL")
|
||||||
print(f" - 완료: {current_progress['current_index']}개 URL")
|
print(f" - 완료: {current_progress['current_index']}개 URL")
|
||||||
print(f" - 현재 처리 중: {current_progress['current_url']}")
|
print(f" - 현재 처리 중: {current_progress['current_url']}")
|
||||||
print(
|
if current_progress.get('start_line'):
|
||||||
f" - domains.txt의 {current_progress['start_line'] + current_progress['current_index']}번째 줄"
|
print(f" - domains.txt의 {current_progress['start_line'] + current_progress['current_index']}번째 줄")
|
||||||
)
|
|
||||||
if current_progress["total"] > 0:
|
if current_progress["total"] > 0:
|
||||||
print(
|
print(f" - 진행률: {current_progress['current_index']}/{current_progress['total']} ({current_progress['current_index']/current_progress['total']*100:.1f}%)")
|
||||||
f" - 진행률: {current_progress['current_index']}/{current_progress['total']} ({current_progress['current_index']/current_progress['total']*100:.1f}%)"
|
|
||||||
)
|
|
||||||
print("=" * 60)
|
print("=" * 60)
|
||||||
|
|
||||||
|
# 진행 상황 저장
|
||||||
save_progress()
|
save_progress()
|
||||||
print(f"💾 진행 상황이 {progress_file}에 저장되었습니다.")
|
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():
|
def setup_signal_handler():
|
||||||
"""시그널 핸들러 등록"""
|
"""시그널 핸들러 등록 - browser-use와의 호환성을 위해 비활성화"""
|
||||||
signal.signal(signal.SIGINT, signal_handler)
|
# 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"))
|
Laminar.initialize(project_api_key=os.getenv("LMNR_PROJECT_API_KEY"))
|
||||||
except ImportError:
|
except ImportError:
|
||||||
print("⚠️ Laminar 라이브러리가 설치되지 않았습니다. 관련 기능이 비활성화됩니다.")
|
print("⚠️ Laminar 라이브러리가 설치되지 않았습니다. 관련 기능이 비활성화됩니다.")
|
||||||
|
else:
|
||||||
|
print("⚠️ LMNR_PROJECT_API_KEY 환경 변수가 설정되지 않았습니다. Laminar 기능이 비활성화됩니다.")
|
||||||
|
|
||||||
|
|
||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
|
|
@ -64,6 +66,25 @@ def main():
|
||||||
setup_signal_handler()
|
setup_signal_handler()
|
||||||
args = parse_arguments()
|
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:
|
try:
|
||||||
asyncio.run(
|
asyncio.run(
|
||||||
main_loop(
|
main_loop(
|
||||||
|
|
@ -74,16 +95,26 @@ def main():
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
except KeyboardInterrupt:
|
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)
|
sys.exit(1)
|
||||||
finally:
|
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:
|
try:
|
||||||
os.remove(progress_file)
|
os.remove(progress_file)
|
||||||
print("진행 상황 파일이 삭제되었습니다.")
|
print("✅ 진행 상황 파일이 삭제되었습니다.")
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
print(f"오류: 진행 상황 파일을 삭제하지 못했습니다. {e}", file=sys.stderr)
|
print(f"⚠️ 진행 상황 파일 삭제 실패: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue