main_server.py가 중복 실행되면서 좀비 프로세스가 수행되는 오류 해결, process_tracker.py가 감시하면서 할당되지 않은 pid가 존재하면 좀비프로세스로 판단하여 kill

This commit is contained in:
2026-02-11 07:48:06 +09:00
parent 7f2f575ec8
commit 4fd0aa91bc
8 changed files with 689 additions and 371 deletions

View File

@@ -28,6 +28,19 @@ class KISClient:
self.access_token = None
self.token_expired = None
self.last_req_time = 0
# 토큰 파일 경로 (영구 저장용)
self.token_file = os.path.join(Config.DATA_DIR, "kis_token.json")
self.load_token() # 초기화 시 토큰 로드 시도
def _safe_int(self, val):
"""안전한 int 변환"""
try:
if not val:
return 0
return int(str(val).strip())
except:
return 0
def _throttle(self):
"""API 요청 속도 제한 (초당 2회 이하로 제한)"""
@@ -41,6 +54,38 @@ class KISClient:
self.last_req_time = time.time()
def load_token(self):
"""파일에서 토큰 로드"""
if os.path.exists(self.token_file):
try:
with open(self.token_file, "r", encoding="utf-8") as f:
data = json.load(f)
# 만료 시간 체크
expire_str = data.get("expired_at")
if expire_str:
expire_dt = datetime.strptime(expire_str, "%Y-%m-%d %H:%M:%S")
if datetime.now() < expire_dt:
self.access_token = data.get("access_token")
self.token_expired = expire_dt
print(f"📂 [KIS] Saved Token Loaded (Expires: {expire_str})")
except Exception as e:
print(f"⚠️ Failed to load token file: {e}")
def save_token(self):
"""토큰 파일 저장"""
if not self.access_token or not self.token_expired:
return
try:
data = {
"access_token": self.access_token,
"expired_at": self.token_expired.strftime("%Y-%m-%d %H:%M:%S")
}
with open(self.token_file, "w", encoding="utf-8") as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"⚠️ Failed to save token file: {e}")
def _get_headers(self, tr_id=None):
"""공통 헤더 생성"""
headers = {
@@ -54,10 +99,16 @@ class KISClient:
return headers
def ensure_token(self):
"""접근 토큰 발급 (OAuth 2.0)"""
# 토큰 유효성 체크 로직은 생략 (실제 운영 시 만료 시간 체크 필요)
if self.access_token:
def ensure_token(self, force=False):
"""접근 토큰 발급 (OAuth 2.0) 및 유효성 관리"""
# 토큰이 있고, 만료 시간이 아직 안 지났으면 재사용
if not force and self.access_token and self.token_expired:
if datetime.now() < self.token_expired:
return
# 앱키 확인
if not self.app_key or not self.app_secret:
print("❌ [KIS] App Key or Secret is missing!")
return
url = f"{self.base_url}/oauth2/tokenP"
@@ -68,16 +119,41 @@ class KISClient:
}
try:
print("🔑 [KIS] 토큰 발급 요청...")
print(f"🔑 [KIS] 토큰 발급 요청: {url}")
res = requests.post(url, json=payload)
res.raise_for_status()
data = res.json()
self.access_token = data.get('access_token')
print("✅ [KIS] 토큰 발급 성공")
# 만료 시간 설정
expires_in = int(data.get('expires_in', 86400))
self.token_expired = datetime.now() + timedelta(seconds=expires_in - 60)
# 파일 저장
self.save_token()
print(f"✅ [KIS] 토큰 발급 성공 (만료: {self.token_expired.strftime('%Y-%m-%d %H:%M:%S')})")
except Exception as e:
print(f"❌ [KIS] 토큰 발급 실패: {e}")
# 1분 제한 에러 핸들링 (EGW00133)
retry = False
if isinstance(e, requests.exceptions.RequestException) and e.response is not None:
print(f"📄 [KIS Token Error Body]: {e.response.text}")
err_text = e.response.text
print(f"📄 [KIS Error]: {err_text}")
if "EGW00133" in err_text:
print("⏳ [KIS] Rate Limit Hit (1 min). Waiting 65s...")
time.sleep(65) # 1분 대기
retry = True
if retry:
# 재귀 호출 (한 번만)
self.ensure_token()
return
print(f"❌ [KIS] 토큰 발급 실패: {e}")
self.access_token = None
raise e
def get_hash_key(self, datas):
"""주문 시 필요한 Hash Key 생성 (Koreainvestment header 특화)"""
@@ -94,16 +170,62 @@ class KISClient:
print(f"❌ Hash Key 생성 실패: {e}")
return None
def get_balance(self):
"""주식 잔고 조회"""
def _request_api(self, method, endpoint, tr_id, params=None, data=None, use_hash=False):
"""API 요청 공통 핸들러 (토큰 만료 시 자동 갱신)"""
self._throttle()
self.ensure_token()
# 국내주식 잔고조회 TR ID: VTTC8434R (모의), TTTC8434R (실전)
tr_id = "VTTC8434R" if self.is_virtual else "TTTC8434R"
url = f"{self.base_url}/uapi/domestic-stock/v1/trading/inquire-balance"
url = f"{self.base_url}/{endpoint}"
headers = self._get_headers(tr_id)
headers = self._get_headers(tr_id=tr_id)
if use_hash and data:
hash_key = self.get_hash_key(data)
if hash_key:
headers["hashkey"] = hash_key
try:
if method == "GET":
res = requests.get(url, headers=headers, params=params)
else:
res = requests.post(url, headers=headers, json=data)
# 토큰 만료 체크 (500 에러 or msg_cd 확인)
is_token_error = False
try:
# KIS는 토큰 만료 시 500을 주거나 200/403 등과 함께 msg_cd로 알려줌
if res.status_code == 500 or res.status_code == 401 or res.status_code == 403:
err_data = res.json()
# EGW00121: 유효하지 않은 토큰, EGW00123: 만료된 토큰
if err_data.get('msg_cd') in ['EGW00121', 'EGW00123']:
is_token_error = True
except:
pass
if is_token_error:
print("🔄 [KIS] Token expired (caught). Refreshing...")
self.ensure_token(force=True)
headers = self._get_headers(tr_id)
if use_hash and data and "hashkey" in headers:
pass # Hash 재활용
if method == "GET":
res = requests.get(url, headers=headers, params=params)
else:
res = requests.post(url, headers=headers, json=data)
res.raise_for_status()
return res.json()
except Exception as e:
print(f"❌ [KIS] API Request Failed: {url} | {e}")
if isinstance(e, requests.exceptions.RequestException) and e.response is not None:
print(f"📄 [KIS Error Body]: {e.response.text}")
raise e
def get_balance(self):
"""주식 잔고 조회"""
tr_id = "VTTC8434R" if self.is_virtual else "TTTC8434R"
endpoint = "uapi/domestic-stock/v1/trading/inquire-balance"
# 쿼리 파라미터
params = {
@@ -121,9 +243,7 @@ class KISClient:
}
try:
res = requests.get(url, headers=headers, params=params)
res.raise_for_status()
data = res.json()
data = self._request_api("GET", endpoint, tr_id, params=params)
# 응답 정리
if data['rt_cd'] != '0':
@@ -149,10 +269,6 @@ class KISClient:
"deposit": int(summary['dnca_tot_amt'])
}
except Exception as e:
print(f"❌ [KIS] 잔고 조회 실패: {e}")
# If it's a requests error, verify if there's a response body
if isinstance(e, requests.exceptions.RequestException) and e.response is not None:
print(f"📄 [KIS Error Body]: {e.response.text}")
return {"error": str(e)}
def order(self, ticker, qty, buy_sell, price=0):
@@ -311,20 +427,14 @@ class KISClient:
"""지수 현재가 조회 (업종/지수)
ticker: 0001 (KOSPI), 1001 (KOSDAQ), etc.
"""
self._throttle()
self.ensure_token()
url = f"{self.base_url}/uapi/domestic-stock/v1/quotations/inquire-index-price"
headers = self._get_headers(tr_id="FHKUP03500100")
endpoint = "uapi/domestic-stock/v1/quotations/inquire-index-price"
params = {
"FID_COND_MRKT_DIV_CODE": "U", # U: 업종/지수
"FID_INPUT_ISCD": ticker
}
try:
res = requests.get(url, headers=headers, params=params)
res.raise_for_status()
data = res.json()
data = self._request_api("GET", endpoint, "FHKUP03500100", params=params)
if data['rt_cd'] != '0':
return None
return {
@@ -340,10 +450,7 @@ class KISClient:
def get_daily_index_price(self, ticker, period="D"):
"""지수 일별 시세 조회 (Market Stress Index용)"""
self._throttle()
self.ensure_token()
url = f"{self.base_url}/uapi/domestic-stock/v1/quotations/inquire-daily-indexchartprice"
headers = self._get_headers(tr_id="FHKUP03500200")
endpoint = "uapi/domestic-stock/v1/quotations/inquire-daily-indexchartprice"
# 날짜 계산 (최근 100일)
end_dt = datetime.now().strftime("%Y%m%d")
@@ -358,11 +465,8 @@ class KISClient:
"FID_ORG_ADJ_PRC": "0" # 수정주가 반영 여부
}
try:
res = requests.get(url, headers=headers, params=params)
res.raise_for_status()
data = res.json()
data = self._request_api("GET", endpoint, "FHKUP03500200", params=params)
if data['rt_cd'] != '0':
return []
@@ -373,3 +477,38 @@ class KISClient:
except Exception as e:
print(f"❌ 지수 일별 시세 조회 실패({ticker}): {e}")
return []
def get_investor_trend(self, ticker):
"""종목별 투자자(외인/기관) 매매동향 조회"""
self._throttle()
self.ensure_token()
url = f"{self.base_url}/uapi/domestic-stock/v1/quotations/inquire-investor"
headers = self._get_headers(tr_id="FHKST01010900")
params = {
"FID_COND_MRKT_DIV_CODE": "J",
"FID_INPUT_ISCD": ticker
}
try:
res = requests.get(url, headers=headers, params=params)
res.raise_for_status()
data = res.json()
if data['rt_cd'] != '0':
return None
# output 리스트: [ {stck_bsop_date: 날짜, frgn_ntby_qty: 외인순매수, orgn_ntby_qty: 기관순매수, ...}, ... ]
trends = []
for item in data['output'][:5]: # 최근 5일치만
trends.append({
"date": item['stck_bsop_date'],
"foreigner": self._safe_int(item.get('frgn_ntby_qty')), # 외인 순매수량
"institutional": self._safe_int(item.get('orgn_ntby_qty')), # 기관 순매수량
"price_change": float(item['prdy_vrss']) # 전일대비 등락금액
})
# 최근일이 0번 인덱스임
return trends
except Exception as e:
print(f"❌ 투자자 동향 조회 실패({ticker}): {e}")
return None

View File

@@ -18,12 +18,14 @@ def run_telegram_bot_standalone():
from modules.services.telegram_bot.server import TelegramBotServer
from modules.utils.ipc import BotIPC
from modules.config import Config
from modules.utils.process_tracker import ProcessTracker
token = os.getenv("TELEGRAM_BOT_TOKEN")
if not token:
print("❌ [Telegram] TELEGRAM_BOT_TOKEN not found in .env")
sys.exit(1)
ProcessTracker.register("Telegram Bot Standalone")
print(f"🤖 [Telegram Bot Process] Starting... (PID: {os.getpid()})")
print(f"🔗 [Telegram Bot] Standalone Process Mode (IPC Enabled)")

View File

@@ -11,7 +11,7 @@ import logging
import subprocess
import sys
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes, TypeHandler
from telegram.ext import Application, CommandHandler, ContextTypes
from dotenv import load_dotenv
# 로깅 설정
@@ -39,7 +39,6 @@ class TelegramBotServer:
def refresh_bot_instance(self):
"""IPC에서 최신 봇 인스턴스 데이터 읽기"""
# [수정] 모듈 경로 변경
from modules.utils.ipc import BotIPC
ipc = BotIPC()
self.bot_instance = ipc.get_bot_instance_data()
@@ -47,8 +46,9 @@ class TelegramBotServer:
async def start_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""/start 명령어 핸들러"""
print(f"📨 [Telegram] /start received from user {update.effective_user.id}")
await update.message.reply_text(
"🤖 **AI Trading Bot Command Center**\n"
"🤖 <b>AI Trading Bot Command Center</b>\n"
"명령어 목록:\n"
"/status - 현재 봇 및 시장 상태 조회\n"
"/portfolio - 현재 보유 종목 및 평가액\n"
@@ -57,11 +57,11 @@ class TelegramBotServer:
"/macro - 거시경제 지표 및 시장 위험도\n"
"/system - PC 리소스(CPU/GPU) 상태\n"
"/ai - AI 모델 학습 상태 조회\n\n"
"**[관리 명령어]**\n"
"<b>[관리 명령어]</b>\n"
"/restart - 봇 재시작\n"
"/exec <command> - 원격 명령어 실행\n"
"/exec <code>명령어</code> - 원격 명령어 실행\n"
"/stop - 봇 종료",
parse_mode="Markdown"
parse_mode="HTML"
)
async def status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -74,13 +74,13 @@ class TelegramBotServer:
now = datetime.now()
is_market_open = (9 <= now.hour < 15) or (now.hour == 15 and now.minute < 30)
status_msg = "**System Status: ONLINE**\n"
status_msg += f"🕒 **Market:** {'OPEN 🟢' if is_market_open else 'CLOSED 🔴'}\n"
status_msg = "<b>System Status: ONLINE</b>\n"
status_msg += f"🕒 <b>Market:</b> {'OPEN 🟢' if is_market_open else 'CLOSED 🔴'}\n"
macro_warn = self.bot_instance.is_macro_warning_sent
status_msg += f"🌍 **Macro Filter:** {'DANGER 🚨 (Trading Halted)' if macro_warn else 'SAFE 🟢'}\n"
status_msg += f"🌍 <b>Macro Filter:</b> {'DANGER 🚨 (Trading Halted)' if macro_warn else 'SAFE 🟢'}\n"
await update.message.reply_text(status_msg, parse_mode="Markdown")
await update.message.reply_text(status_msg, parse_mode="HTML")
async def portfolio_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""/portfolio: 잔고 조회"""
@@ -96,19 +96,19 @@ class TelegramBotServer:
await update.message.reply_text(f"❌ 잔고 조회 실패: {balance['error']}")
return
msg = f"💰 **Total Asset:** `{int(balance['total_eval']):,} KRW`\n" \
f"💵 **Deposit:** `{int(balance['deposit']):,} KRW`\n\n"
msg = f"💰 <b>Total Asset:</b> <code>{int(balance['total_eval']):,} KRW</code>\n" \
f"💵 <b>Deposit:</b> <code>{int(balance['deposit']):,} KRW</code>\n\n"
if balance['holdings']:
msg += "**[Holdings]**\n"
msg += "<b>[Holdings]</b>\n"
for stock in balance['holdings']:
icon = "🔴" if stock['yield'] > 0 else "🔵"
msg += f"{icon} **{stock['name']}** `{stock['yield']}%`\n" \
msg += f"{icon} <b>{stock['name']}</b> <code>{stock['yield']}%</code>\n" \
f" (수량: {stock['qty']} / 평가손익: {stock['profit_loss']:,})\n"
else:
msg += "보유 중인 종목이 없습니다."
await update.message.reply_text(msg, parse_mode="Markdown")
await update.message.reply_text(msg, parse_mode="HTML")
except Exception as e:
await update.message.reply_text(f"❌ Error: {str(e)}")
@@ -116,41 +116,41 @@ class TelegramBotServer:
async def watchlist_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""/watchlist: 감시 대상 종목"""
if not self.refresh_bot_instance():
await update.message.reply_text("⚠️ 봇 인스턴스가 연결되지 않았습니다.")
return
target_dict = self.bot_instance.load_watchlist()
discovered = list(self.bot_instance.discovered_stocks)
msg = f"👀 **Watchlist: {len(target_dict)} items**\n"
msg = f"👀 <b>Watchlist: {len(target_dict)} items</b>\n"
for code, name in target_dict.items():
themes = self.bot_instance.theme_manager.get_themes(code)
theme_str = f" ({', '.join(themes)})" if themes else ""
msg += f"- {name}{theme_str}\n"
if discovered:
msg += f"\n**Discovered Today ({len(discovered)}):**\n"
msg += f"\n<b>Discovered Today ({len(discovered)}):</b>\n"
for code in discovered:
msg += f"- {code}\n"
await update.message.reply_text(msg)
await update.message.reply_text(msg, parse_mode="HTML")
async def update_watchlist_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""/update_watchlist: Watchlist 즉시 업데이트"""
await update.message.reply_text("🔄 Watchlist를 업데이트하고 있습니다... (30초 소요)")
try:
# [수정] IPC 모드에서도 직접 수행하기 위해 새로운 인스턴스 생성
from modules.services.kis import KISClient
from watchlist_manager import WatchlistManager
# 독립적인 KIS 클라이언트 생성
from modules.config import Config
temp_kis = KISClient()
mgr = WatchlistManager(temp_kis, watchlist_file=Config.WATCHLIST_FILE)
# 업데이트 수행 (파일 쓰기)
summary = mgr.update_watchlist_daily()
await update.message.reply_text(summary, parse_mode="Markdown")
# HTML 특수문자 이스케이프
summary = summary.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
await update.message.reply_text(summary)
except Exception as e:
await update.message.reply_text(f"❌ 업데이트 실패: {e}")
@@ -164,32 +164,29 @@ class TelegramBotServer:
await update.message.reply_text("⏳ 거시경제 데이터를 불러옵니다...")
try:
# [수정] IPC 데이터를 직접 사용하여 출력 (FakeKIS의 _macro_indices 활용)
# FakeKIS는 bot_instance.kis에 할당되어 있음
indices = getattr(self.bot_instance.kis, '_macro_indices', {})
if not indices:
await update.message.reply_text("⚠️ 데이터가 아직 수집되지 않았습니다. 잠시 후 다시 시도하세요.")
return
# 리스크 점수 계산 (간이)
status = "SAFE"
msi = indices.get('MSI', 0)
if msi >= 50: status = "DANGER"
elif msi >= 30: status = "CAUTION"
color = "🟢" if status == "SAFE" else "🔴" if status == "DANGER" else "🟡"
msg = f"{color} **Market Risk: {status}**\n\n"
msg = f"{color} <b>Market Risk: {status}</b>\n\n"
if 'MSI' in indices:
msg += f"🌡️ **Stress Index:** `{indices['MSI']}`\n"
msg += f"🌡️ <b>Stress Index:</b> <code>{indices['MSI']}</code>\n"
for k, v in indices.items():
if k != "MSI":
icon = "🔺" if v.get('change', 0) > 0 else "🔻"
msg += f"{icon} **{k}**: {v.get('price', 0)} ({v.get('change', 0)}%)\n"
msg += f"{icon} <b>{k}</b>: {v.get('price', 0)} ({v.get('change', 0)}%)\n"
await update.message.reply_text(msg, parse_mode="Markdown")
await update.message.reply_text(msg, parse_mode="HTML")
except Exception as e:
await update.message.reply_text(f"❌ Error: {e}")
@@ -220,97 +217,101 @@ class TelegramBotServer:
top_3 = top_processes[:3]
gpu_status = self.bot_instance.ollama_monitor.get_gpu_status()
gpu_msg = f"N/A"
gpu_msg = "N/A"
if gpu_status and gpu_status.get('name') != 'N/A':
gpu_name = gpu_status.get('name', 'GPU')
gpu_msg = f"{gpu_name}\n Temp: {gpu_status.get('temp', 0)}°C / VRAM: {gpu_status.get('vram_used', 0)}GB / {gpu_status.get('vram_total', 0)}GB"
msg = "🖥️ **PC System Status**\n" \
f"🧠 **CPU:** `{cpu}%`\n" \
f"💾 **RAM:** `{ram}%`\n" \
f"🎮 **GPU:** {gpu_msg}\n\n"
msg = "🖥️ <b>PC System Status</b>\n" \
f"🧠 <b>CPU:</b> <code>{cpu}%</code>\n" \
f"💾 <b>RAM:</b> <code>{ram}%</code>\n" \
f"🎮 <b>GPU:</b> {gpu_msg}\n\n"
if top_3:
msg += "⚙️ **Top CPU Processes:**\n"
msg += "⚙️ <b>Top CPU Processes:</b>\n"
for i, proc in enumerate(top_3, 1):
proc_name = proc.get('name', 'Unknown')
proc_cpu = proc.get('cpu_percent', 0)
msg += f" {i}. `{proc_name}` - {proc_cpu:.1f}%\n"
msg += f" {i}. <code>{proc_name}</code> - {proc_cpu:.1f}%\n"
await update.message.reply_text(msg, parse_mode="Markdown")
await update.message.reply_text(msg, parse_mode="HTML")
async def ai_status_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""/ai: AI 모델 학습 상태 조회"""
if not self.refresh_bot_instance():
await update.message.reply_text("⚠️ 메인 봇이 실행 중이 아닙니다.")
return
gpu = self.bot_instance.ollama_monitor.get_gpu_status()
msg = "🧠 **AI Model Status**\n"
msg += f"**LLM Engine:** Ollama (Llama 3.1)\n"
msg = "🧠 <b>AI Model Status</b>\n"
msg += "<b>LLM Engine:</b> Ollama (Llama 3.1)\n"
gpu_name = gpu.get('name', 'NVIDIA RTX 5070 Ti')
msg += f"**Device:** {gpu_name}\n"
msg += f"<b>Device:</b> {gpu_name}\n"
if gpu:
msg += f"**GPU Load:** `{gpu.get('load', 0)}%`\n"
msg += f"**VRAM Usage:** `{gpu.get('vram_used', 0)}GB` / {gpu.get('vram_total', 0)}GB"
msg += f"<b>GPU Load:</b> <code>{gpu.get('load', 0)}%</code>\n"
msg += f"<b>VRAM Usage:</b> <code>{gpu.get('vram_used', 0)}GB</code> / {gpu.get('vram_total', 0)}GB"
await update.message.reply_text(msg, parse_mode="Markdown")
await update.message.reply_text(msg, parse_mode="HTML")
async def restart_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""/restart: 텔레그램 봇 모듈만 재시작"""
await update.message.reply_text("🔄 **텔레그램 인터페이스를 재시작합니다...**")
await update.message.reply_text("🔄 <b>텔레그램 인터페이스를 재시작합니다...</b>", parse_mode="HTML")
# 재시작 플래그 설정 (runner.py에서 감지하여 재시작)
self.should_restart = True
self.application.stop_running()
async def stop_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""/stop: 봇 종료"""
await update.message.reply_text("🛑 **텔레그램 봇을 종료합니다.**")
await update.message.reply_text("🛑 <b>텔레그램 봇을 종료합니다.</b>", parse_mode="HTML")
# 종료 플래그 설정 (runner.py에서 루프 탈출)
self.should_restart = False
self.application.stop_running()
async def exec_command(self, update: Update, context: ContextTypes.DEFAULT_TYPE):
"""/exec: 원격 명령어 실행"""
# ultimate_handler에서는 context.args가 비어있으므로 직접 파싱
"""/exec: 원격 명령어 실행 (Non-blocking)"""
text = update.message.text.strip()
parts = text.split(maxsplit=1) # "/exec" 와 나머지 명령어로 분리
parts = text.split(maxsplit=1)
if len(parts) < 2:
await update.message.reply_text("❌ 사용법: /exec <command>")
await update.message.reply_text("❌ 사용법: /exec 명령어")
return
command = parts[1] # "/exec" 이후의 모든 텍스트
await update.message.reply_text(f"⚙️ 실행 중: `{command}`", parse_mode="Markdown")
command = parts[1]
await update.message.reply_text(f"⚙️ 실행 중: <code>{command}</code>", parse_mode="HTML")
try:
# 보안: 위험한 명령어 차단
dangerous_keywords = ['rm', 'del', 'format', 'shutdown', 'reboot', 'ipconfig']
dangerous_keywords = ['rm', 'del', 'format', 'shutdown', 'reboot']
if any(keyword in command.lower() for keyword in dangerous_keywords):
await update.message.reply_text("⛔ 위험한 명령어는 실행할 수 없습니다.")
return
# Windows에서는 PowerShell을 명시적으로 사용
import platform
if platform.system() == 'Windows':
exec_command = ['powershell', '-Command', command]
is_windows = platform.system() == 'Windows'
if is_windows:
exec_cmd = ['powershell', '-Command', command]
else:
exec_command = command
exec_cmd = command
# 명령어 실행 (타임아웃 30초)
result = subprocess.run(
exec_command,
shell=False if platform.system() == 'Windows' else True,
capture_output=True,
text=True,
encoding='utf-8',
errors='replace', # 인코딩 오류 무시
timeout=30,
cwd=os.getcwd()
)
def run_subprocess():
return subprocess.run(
exec_cmd,
shell=not is_windows,
capture_output=True,
text=True,
encoding='utf-8',
errors='replace',
timeout=30,
cwd=os.getcwd()
)
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, run_subprocess)
# stdout와 stderr 모두 확인
output = result.stdout.strip() if result.stdout else ""
error_output = result.stderr.strip() if result.stderr else ""
@@ -323,97 +324,53 @@ class TelegramBotServer:
else:
combined = "명령어 실행 완료 (출력 없음)"
# 출력이 너무 길면 잘라내기
if len(combined) > 3000:
combined = combined[:3000] + "\n... (출력이 너무 깁니다)"
combined = combined[:3000] + "\n... (Truncated)"
await update.message.reply_text(f"```\n{combined}\n```", parse_mode="Markdown")
# HTML 특수문자 이스케이프
combined = combined.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
await update.message.reply_text(f"<pre>{combined}</pre>", parse_mode="HTML")
except subprocess.TimeoutExpired:
await update.message.reply_text("⏱️ 명령어 실행 시간 초과 (30초)")
except asyncio.TimeoutError:
await update.message.reply_text("⏱️ 명령어 실행 시간 초과 (30초)")
except Exception as e:
print(f"❌ [Telegram /exec] Error: {e}")
import traceback
traceback.print_exc()
await update.message.reply_text(f"❌ 실행 오류: {str(e)}")
await update.message.reply_text(f"❌ 실행 오류: {e}")
def run(self):
"""봇 실행 (비동기 polling)"""
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
"""에러 핸들러"""
import traceback
"""봇 실행 (Handler 등록 및 Polling)"""
handlers = [
("start", self.start_command),
("status", self.status_command),
("portfolio", self.portfolio_command),
("watchlist", self.watchlist_command),
("update_watchlist", self.update_watchlist_command),
("macro", self.macro_command),
("system", self.system_command),
("ai", self.ai_status_command),
("restart", self.restart_command),
("stop", self.stop_command),
("exec", self.exec_command)
]
for cmd, func in handlers:
self.application.add_handler(CommandHandler(cmd, func))
# Conflict 에러는 무시 (다른 봇 인스턴스 실행 중)
async def error_handler(update: object, context: ContextTypes.DEFAULT_TYPE) -> None:
if "Conflict" in str(context.error):
print(f"⚠️ [Telegram] 다른 봇 인스턴스가 실행 중입니다. 이 인스턴스를 종료합니다.")
print(f"⚠️ [Telegram] Conflict detected. Stopping...")
if self.application.running:
await self.application.stop()
return
tb_list = traceback.format_exception(None, context.error, context.error.__traceback__)
tb_string = ''.join(tb_list)
print(f"❌ [Telegram Error] {tb_string}")
if isinstance(update, Update) and update.effective_message:
try:
await update.effective_message.reply_text(f"⚠️ 오류 발생: {context.error}")
except:
pass
print(f"❌ [Telegram Error] {context.error}")
async def ultimate_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
if not update.message or not update.message.text:
return
text = update.message.text.strip()
print(f"📨 [Telegram] Command Received: {text}")
try:
if text.startswith("/start"):
await self.start_command(update, context)
elif text.startswith("/status"):
await self.status_command(update, context)
elif text.startswith("/portfolio"):
await self.portfolio_command(update, context)
elif text.startswith("/watchlist"):
await self.watchlist_command(update, context)
elif text.startswith("/update_watchlist"):
await self.update_watchlist_command(update, context)
elif text.startswith("/macro"):
await self.macro_command(update, context)
elif text.startswith("/system"):
await self.system_command(update, context)
elif text.startswith("/ai"):
await self.ai_status_command(update, context)
elif text.startswith("/restart"):
await self.restart_command(update, context)
elif text.startswith("/stop"):
await self.stop_command(update, context)
elif text.startswith("/exec"):
await self.exec_command(update, context)
except Exception as e:
print(f"❌ Handle Error: {e}")
await update.message.reply_text(f"⚠️ Error: {e}")
# 에러 핸들러 등록
self.application.add_error_handler(error_handler)
self.application.add_handler(TypeHandler(Update, ultimate_handler))
# [최적화] Polling 설정 개선
print("🤖 [Telegram] Command Server Started (Optimized Polling Mode).")
print("🤖 [Telegram] Command Server Started (Standard Polling Mode).")
try:
self.application.run_polling(
allowed_updates=Update.ALL_TYPES,
stop_signals=None,
poll_interval=1.0, # 1초마다 폴링 (기본값 0.0)
timeout=10, # 타임아웃 10초
drop_pending_updates=True # 대기 중인 업데이트 무시
drop_pending_updates=True
)
except Exception as e:
if "Conflict" in str(e):
print(f"⚠️ [Telegram] 다른 봇 인스턴스가 실행 중입니다.")
print(f"⚠️ [Telegram] 기존 봇을 종료하고 다시 시도하세요.")
else:
print(f"❌ [Telegram] 봇 실행 오류: {e}")
import traceback
traceback.print_exc()
print(f"❌ [Telegram] Polling Error: {e}")