Spaces:
Sleeping
Sleeping
File size: 21,280 Bytes
b7ede89 1c12943 d7c9976 1c12943 b7ede89 4f0db0e 79e1e8d 2c15d1f b7ede89 99ef753 79e1e8d 99ef753 2c15d1f 4f0db0e b7ede89 d7c9976 b7ede89 4f0db0e b7ede89 d7c9976 b7ede89 d7c9976 4f0db0e b7ede89 4f0db0e b7ede89 d7c9976 2c15d1f d7c9976 b7ede89 d7c9976 b7ede89 d7c9976 b7ede89 d7c9976 4f0db0e b7ede89 4f0db0e b7ede89 d7c9976 79e1e8d d7c9976 b7ede89 d7c9976 b7ede89 d7c9976 b7ede89 d7c9976 4f0db0e b7ede89 4f0db0e 0f13aa5 b7ede89 6c97837 b7ede89 4f0db0e b7ede89 c074b9a b7ede89 c074b9a b7ede89 0f13aa5 b7ede89 6c97837 d7c9976 b7ede89 d7c9976 b7ede89 d7c9976 b7ede89 6c97837 d7c9976 4f0db0e b7ede89 4f0db0e b7ede89 6c97837 b7ede89 6c97837 0f13aa5 b7ede89 6c97837 b7ede89 d7c9976 b7ede89 6c97837 d7c9976 b7ede89 d7c9976 b7ede89 d7c9976 b7ede89 6c97837 b7ede89 d7c9976 b7ede89 6c97837 b7ede89 d7c9976 b7ede89 d7c9976 6c97837 b7ede89 6c97837 b7ede89 6c97837 b7ede89 79e1e8d 6c97837 b7ede89 6c97837 b7ede89 18aeb7a 6c97837 b7ede89 d7c9976 6c97837 b7ede89 6c97837 b7ede89 d7c9976 6c97837 b7ede89 d7c9976 b7ede89 1cbdaf4 b7ede89 6c97837 b7ede89 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 |
# requirements.txt 에 다음 내용을 추가하거나 업데이트해야 합니다:
# gradio>=4.0
# requests
import gradio as gr
import requests
from requests.exceptions import RequestException
import json
import time
# --- 전역 변수 및 세션 설정 ---
SERVER_URL = ""
REQUEST_TIMEOUT = 15
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=5, pool_maxsize=10,
max_retries=requests.adapters.Retry(
total=3, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET", "POST"], connect=3, read=3, redirect=3
)
)
session.mount('http://', adapter)
session.mount('https://', adapter)
# --- 백엔드 통신 함수 ---
def connect_server(ngrok_url):
"""입력된 Ngrok URL로 서버 연결 시도"""
global SERVER_URL
print(f"[DEBUG] 서버 연결 시도: {ngrok_url}")
try:
if not ngrok_url:
return "오류: 서버 URL을 입력해주세요."
# URL 끝 슬래시 제거
if ngrok_url.endswith('/'):
ngrok_url = ngrok_url[:-1]
# '/health' 또는 '/api/status' 엔드포인트로 상태 확인
health_url = f"{ngrok_url}/health"
status_url = f"{ngrok_url}/api/status"
test_url = ""
response = None
try:
test_url = health_url
print(f"[DEBUG] {test_url} 확인 중...")
response = session.get(test_url, timeout=5) # 상태 확인은 짧은 타임아웃
if response.status_code != 200:
raise RequestException("Health check failed or returned non-200")
except RequestException as e_health:
print(f"[DEBUG] {test_url} 실패 ({e_health}), {status_url} 로 재시도...")
try:
test_url = status_url
response = session.get(test_url, timeout=5) # 상태 확인은 짧은 타임아웃
if response.status_code != 200:
raise RequestException("Status check failed or returned non-200")
except RequestException as e_status:
print(f"[ERROR] 서버 연결 최종 실패: {e_status}")
return f"서버 연결 실패: {test_url} 확인 중 오류 발생 ({e_status})"
# 연결 성공 시
SERVER_URL = ngrok_url
print(f"[INFO] 서버 연결 성공: {SERVER_URL}")
return f"서버 연결 성공! ({test_url} 응답코드: {response.status_code})"
except Exception as e: # 포괄적인 예외 처리
print(f"[ERROR] 서버 연결 중 예외 발생: {e}")
return f"서버 연결 중 예상치 못한 오류 발생: {str(e)}"
def check_status():
"""서버의 /api/status 엔드포인트 상태 확인"""
global SERVER_URL
if not SERVER_URL:
return gr.update(value="오류: 서버에 먼저 연결하세요.")
print(f"[DEBUG] 서버 상태 확인 요청: {SERVER_URL}/api/status")
try:
response = session.get(f"{SERVER_URL}/api/status", timeout=REQUEST_TIMEOUT)
if response.status_code == 200:
try:
status_data = json.dumps(response.json(), indent=2, ensure_ascii=False)
print(f"[INFO] 상태 확인 성공")
return gr.update(value=status_data)
except json.JSONDecodeError:
print(f"[ERROR] 상태 응답 JSON 파싱 실패")
return gr.update(value="오류: 서버 응답 형식이 잘못되었습니다 (JSON 아님).")
else:
print(f"[ERROR] 상태 확인 실패: HTTP {response.status_code}")
return gr.update(value=f"상태 확인 실패: HTTP {response.status_code}\n{response.text}")
except RequestException as e:
print(f"[ERROR] 상태 확인 중 네트워크 오류: {e}")
return gr.update(value=f"상태 확인 중 오류 발생: {str(e)}")
def echo_test(message):
"""서버의 /api/send (echo 액션) 테스트"""
global SERVER_URL
if not SERVER_URL:
return gr.update(value="오류: 서버에 먼저 연결하세요.")
if not message:
return gr.update(value="오류: 에코 테스트 메시지를 입력하세요.")
print(f"[DEBUG] 에코 테스트 요청: {SERVER_URL}/api/send, 메시지: {message}")
try:
payload = {"action": "echo", "data": {"message": message}, "timestamp": int(time.time() * 1000)}
response = session.post(f"{SERVER_URL}/api/send", json=payload, headers={"Content-Type": "application/json"}, timeout=REQUEST_TIMEOUT)
if response.status_code == 200:
try:
echo_data = json.dumps(response.json(), indent=2, ensure_ascii=False)
print(f"[INFO] 에코 테스트 성공")
return gr.update(value=echo_data)
except json.JSONDecodeError:
print(f"[ERROR] 에코 응답 JSON 파싱 실패")
return gr.update(value="오류: 서버 응답 형식이 잘못되었습니다 (JSON 아님).")
else:
print(f"[ERROR] 에코 테스트 실패: HTTP {response.status_code}")
return gr.update(value=f"에코 테스트 실패: HTTP {response.status_code}\n{response.text}")
except RequestException as e:
print(f"[ERROR] 에코 테스트 중 네트워크 오류: {e}")
return gr.update(value=f"에코 테스트 중 오류 발생: {str(e)}")
def get_programs():
"""서버의 /api/programs 에서 프로그램 목록 조회"""
global SERVER_URL
print(f"[DEBUG] 프로그램 목록 조회 함수 시작")
program_options = []
default_return = ("<div style='color: gray;'>프로그램 목록이 없습니다.</div>", gr.update(choices=[]), gr.update(choices=[]))
# 테스트 모드 (서버 URL 없을 때)
if not SERVER_URL:
print(f"[DEBUG] 테스트 모드: 프로그램 목록 생성")
test_programs = [
{"id": "test1", "name": "테스트 메모장", "description": "텍스트 편집기", "path": "C:\\Windows\\notepad.exe"},
{"id": "test2", "name": "테스트 계산기", "description": "기본 계산기", "path": "C:\\Windows\\System32\\calc.exe"}
]
html_output = "<div style='padding: 10px; background-color: #fffaeb; border: 1px solid #ffecb3; border-radius: 5px;'>"
html_output += "<h4 style='margin-top:0; color: #856404;'>[테스트 모드]</h4>"
html_output += "<p style='font-size: 0.9em;'>서버에 연결되지 않아 테스트 데이터가 표시됩니다.</p>"
html_output += "<table style='width: 100%; border-collapse: collapse; margin-top: 10px;'><thead><tr style='background-color: #fff3cd;'>"
html_output += "<th style='padding: 8px; text-align: left;'>이름</th><th style='padding: 8px; text-align: left;'>설명</th></tr></thead><tbody>"
for p in test_programs:
program_options.append((f"{p['name']} ({p['description']})", p['id']))
html_output += f"<tr style='border-bottom: 1px solid #eee;'><td style='padding: 8px;'>{p['name']}</td><td style='padding: 8px;'>{p['description']}</td></tr>"
html_output += "</tbody></table></div>"
print(f"[DEBUG] 테스트 옵션: {program_options}")
return html_output, gr.update(choices=program_options), gr.update(choices=program_options)
# 실제 API 호출 (서버 URL 있을 때)
try:
api_url = f"{SERVER_URL}/api/programs"
print(f"[DEBUG] API 호출: {api_url}")
headers = {"User-Agent": "GradioClient/1.0", "Accept": "application/json"}
response = session.get(api_url, headers=headers, timeout=REQUEST_TIMEOUT)
print(f"[DEBUG] API 응답 코드: {response.status_code}")
if response.status_code == 200:
try:
result_json = response.json()
programs = result_json.get("programs", [])
if not programs: # 프로그램 목록이 비어있을 경우
print("[INFO] 서버에서 반환된 프로그램 목록이 비어있습니다.")
return "<div style='color: gray; padding: 10px;'>서버에 등록된 프로그램이 없습니다.</div>", gr.update(choices=[]), gr.update(choices=[])
# 프로그램 목록 테이블 생성
html_output = "<div style='max-height: 300px; overflow-y: auto; border: 1px solid #eee; padding: 10px; border-radius: 5px;'>"
html_output += "<table style='width: 100%; border-collapse: collapse;'><thead><tr style='background-color: #f8f9fa;'>"
html_output += "<th style='padding: 8px; text-align: left;'>이름</th><th style='padding: 8px; text-align: left;'>설명</th><th style='padding: 8px; text-align: left;'>경로</th></tr></thead><tbody>"
for program in programs:
program_id = program.get("id", "")
program_name = program.get("name", "N/A")
program_desc = program.get("description", "")
program_path = program.get("path", "")
# ID가 유효한 경우에만 옵션 추가
if program_id:
program_options.append((f"{program_name} ({program_desc})", program_id))
html_output += f"<tr style='border-bottom: 1px solid #eee;'><td style='padding: 8px;'>{program_name}</td><td style='padding: 8px;'>{program_desc}</td><td style='padding: 8px;'>{program_path}</td></tr>"
else:
print(f"[WARN] ID가 없는 프로그램 데이터 발견: {program}")
html_output += f"<tr style='border-bottom: 1px solid #eee; background-color: #fff3cd;'><td style='padding: 8px; color: orange;' colspan='3'>[주의] ID가 없는 프로그램: {program_name}</td></tr>"
html_output += "</tbody></table></div>"
html_output += f"<div style='margin-top: 10px; font-size: 0.9em; color: #666;'>총 {len(program_options)}개 프로그램 (ID 유효 기준)</div>"
print(f"[INFO] 프로그램 목록 조회 성공, 옵션 개수: {len(program_options)}")
return html_output, gr.update(choices=program_options), gr.update(choices=program_options)
except json.JSONDecodeError:
print(f"[ERROR] 프로그램 목록 응답 JSON 파싱 실패")
error_html = "<div style='color: red;'>오류: 서버 응답 형식이 잘못되었습니다 (JSON 아님).</div>"
return error_html, gr.update(choices=[]), gr.update(choices=[])
else:
print(f"[ERROR] 프로그램 목록 조회 실패: HTTP {response.status_code}")
error_html = f"<div style='color: red;'>프로그램 목록 조회 실패: HTTP {response.status_code}<br>{response.text}</div>"
return error_html, gr.update(choices=[]), gr.update(choices=[])
except RequestException as e:
print(f"[ERROR] 프로그램 목록 조회 중 네트워크 오류: {e}")
error_html = f"<div style='color: red;'>프로그램 목록 조회 중 오류 발생: {str(e)}</div>"
return error_html, gr.update(choices=[]), gr.update(choices=[])
except Exception as e: # 포괄적 예외 처리
print(f"[ERROR] 프로그램 목록 조회 중 예상치 못한 오류: {e}")
error_html = f"<div style='color: red;'>프로그램 목록 조회 중 예상치 못한 오류: {str(e)}</div>"
return error_html, gr.update(choices=[]), gr.update(choices=[])
def execute_program(program_id):
"""선택된 프로그램 ID를 사용하여 서버에 실행 요청"""
global SERVER_URL
# 입력 값 타입 로깅 및 검증
print(f"[DEBUG] 프로그램 실행 시도: ID='{program_id}', Type={type(program_id)}")
if not isinstance(program_id, str):
error_msg = f"오류: 프로그램 ID 형식이 잘못되었습니다 (받은 타입: {type(program_id)}). 프로그램을 다시 선택해주세요."
print(f"[ERROR] {error_msg}")
return f"<div style='color: red;' class='error-box'>{error_msg}</div>"
if not program_id:
error_msg = "오류: 실행할 프로그램을 선택해주세요."
print(f"[WARN] {error_msg}")
return f"<div style='color: orange;' class='warning-box'>{error_msg}</div>"
# 테스트 ID 처리
if program_id.startswith("test"):
print(f"[DEBUG] 테스트 프로그램 '{program_id}' 실행 시뮬레이션")
return f"<div style='color: green;' class='success-box'>테스트 프로그램 '{program_id}' 실행 완료 (시뮬레이션)</div>"
if not SERVER_URL:
error_msg = "오류: 서버에 먼저 연결해야 합니다."
print(f"[WARN] {error_msg}")
return f"<div style='color: orange;' class='warning-box'>{error_msg}</div>"
# 실제 API 호출
try:
api_url = f"{SERVER_URL}/api/programs/{program_id}/execute"
print(f"[DEBUG] 서버 실행 요청: {api_url}")
headers = {"User-Agent": "GradioClient/1.0", "Accept": "application/json", "Content-Type": "application/json"}
# POST 요청의 본문에 ID 포함 (서버 API 구현에 따라 필요 없을 수도 있음)
payload = {"id": program_id}
response = session.post(api_url, headers=headers, json=payload, timeout=REQUEST_TIMEOUT)
print(f"[DEBUG] 실행 API 응답 코드: {response.status_code}")
if response.status_code == 200:
try:
result = response.json()
msg = result.get('message', '프로그램이 성공적으로 실행되었습니다.')
print(f"[INFO] 프로그램 실행 성공: {msg}")
return f"<div style='color: green;' class='success-box'>실행 성공: {msg}</div>"
except json.JSONDecodeError:
print(f"[ERROR] 실행 응답 JSON 파싱 실패")
return f"<div style='color: red;' class='error-box'>오류: 실행 응답 형식이 잘못되었습니다 (JSON 아님).</div>"
else:
error_text = response.text # 오류 메시지 포함
print(f"[ERROR] 프로그램 실행 실패: HTTP {response.status_code}, 응답: {error_text}")
return f"<div style='color: red;' class='error-box'>실행 실패: HTTP {response.status_code}<br><pre>{error_text}</pre></div>"
except RequestException as e:
error_msg = f"실행 중 네트워크 오류 발생: {str(e)}"
print(f"[ERROR] {error_msg}")
return f"<div style='color: red;' class='error-box'>{error_msg}</div>"
except Exception as e: # 포괄적 예외 처리
error_msg = f"실행 중 예상치 못한 오류 발생: {str(e)}"
print(f"[ERROR] {error_msg}")
return f"<div style='color: red;' class='error-box'>{error_msg}</div>"
# --- Gradio UI 구성 (최신 버전 스타일) ---
with gr.Blocks(title="LocalPCAgent 제어 인터페이스 v4", theme=gr.themes.Soft()) as demo:
# 사용자 정의 CSS (선택 사항)
gr.HTML("""
<style>
.error-box { padding: 10px; border: 1px solid #dc3545; border-radius: 5px; background-color: #f8d7da; }
.success-box { padding: 10px; border: 1px solid #28a745; border-radius: 5px; background-color: #d4edda; }
.warning-box { padding: 10px; border: 1px solid #ffc107; border-radius: 5px; background-color: #fff3cd; }
table { font-size: 0.9em; }
th, td { padding: 6px 8px; border-bottom: 1px solid #eee; }
th { background-color: #f8f9fa; font-weight: 500; }
tbody tr:hover { background-color: #f1f1f1; }
</style>
""")
gr.Markdown("# LocalPCAgent 제어 인터페이스")
gr.Markdown("원격 PC의 상태 확인 및 프로그램 실행을 위한 웹 UI입니다.")
# 섹션 1: 서버 연결
with gr.Accordion("1. 서버 연결 설정", open=True): # Accordion 사용
with gr.Row():
ngrok_url_input = gr.Textbox(
label="Ngrok URL 입력",
placeholder="https://xxxx-xx-xxx-xxx.ngrok-free.app 형식",
scale=3 # 너비 비율
)
connect_btn = gr.Button("서버 연결", variant="secondary", scale=1)
connection_status = gr.Textbox(label="연결 상태", interactive=False)
# 섹션 2: 기능 탭
with gr.Tabs():
# 탭 1: 기본 기능
with gr.Tab("기본 기능"):
gr.Markdown("서버의 기본 상태를 확인하고 통신 테스트를 수행합니다.")
with gr.Row(equal_height=True): # Row 안의 요소들 높이 맞춤
with gr.Column(scale=1):
gr.Markdown("#### 서버 상태")
check_status_btn = gr.Button("상태 확인", variant="secondary")
gr.Markdown("#### 에코 테스트")
echo_message = gr.Textbox(label="보낼 메시지", value="Hello from Gradio!", lines=1)
echo_btn = gr.Button("에코 테스트 전송", variant="secondary")
with gr.Column(scale=2):
status_result = gr.Textbox(label="상태 결과", interactive=False, lines=5, max_lines=10)
echo_result = gr.Textbox(label="에코 결과", interactive=False, lines=5, max_lines=10)
# 탭 2: 프로그램 실행
with gr.Tab("프로그램 실행"):
gr.Markdown("서버에 등록된 프로그램을 조회하고 실행합니다.")
with gr.Column():
gr.Markdown("#### 단계 1: 프로그램 목록 조회")
with gr.Row(variant='compact'): # 컴포넌트 간 간격 좁힘
get_programs_btn = gr.Button("목록 새로고침", variant="secondary")
gr.Markdown(f"<small> (네트워크 상태에 따라 최대 {REQUEST_TIMEOUT}초 소요될 수 있습니다)</small>", visible=True) # 안내 문구
# HTML 결과 표시 영역
programs_result = gr.HTML(label="등록된 프로그램 목록")
# 참고용/숨겨진 드롭다운 (UI에는 보이지 않음)
program_dropdown_display = gr.Dropdown(label="내부용", visible=False)
with gr.Column():
gr.Markdown("#### 단계 2: 프로그램 선택 및 실행")
# 실제 사용자가 선택할 드롭다운
program_dropdown_exec = gr.Dropdown(
label="실행할 프로그램 선택",
choices=[], # 초기값은 비어있음
interactive=True,
info="위 '목록 새로고침' 버튼을 눌러 목록을 가져온 후 선택하세요." # Dropdown 아래 안내 문구
)
execute_program_btn = gr.Button("선택한 프로그램 실행", variant="primary") # 주요 실행 버튼 강조
# 실행 결과 표시 영역
execute_result = gr.HTML(label="실행 결과")
# --- 이벤트 핸들러 연결 ---
connect_btn.click(fn=connect_server, inputs=ngrok_url_input, outputs=connection_status)
check_status_btn.click(fn=check_status, inputs=None, outputs=status_result)
echo_btn.click(fn=echo_test, inputs=echo_message, outputs=echo_result)
# 프로그램 목록 새로고침 버튼 클릭
get_programs_btn.click(
fn=get_programs,
inputs=None,
# outputs 순서: HTML 결과, 숨겨진 드롭다운, 실행용 드롭다운
outputs=[programs_result, program_dropdown_display, program_dropdown_exec]
)
# 실행할 프로그램 드롭다운 선택 변경 시 (선택 사항: 결과 영역에 즉시 피드백)
# program_dropdown_exec.change(
# fn=lambda choice: f"<div style='color: blue; padding: 5px;'>'{choice}' 프로그램이 선택되었습니다. 실행 버튼을 클릭하세요.</div>" if choice else "",
# inputs=program_dropdown_exec,
# outputs=execute_result
# )
# 프로그램 실행 버튼 클릭
execute_program_btn.click(
fn=execute_program,
inputs=[program_dropdown_exec], # 선택된 드롭다운 값을 입력으로 사용
outputs=[execute_result] # 실행 결과를 HTML 영역에 표시
)
# --- 앱 실행 ---
if __name__ == "__main__":
print("==============================================")
print(" LocalPCAgent 웹 인터페이스 (Gradio 4.x) 시작 ")
print("==============================================")
print("* 서버 URL을 입력하고 '서버 연결'을 먼저 누르세요.")
print("* 문제가 발생하면 터미널 로그를 확인하세요.")
print("* Hugging Face Spaces 배포 시 requirements.txt 에")
print(" 'gradio>=4.0' 와 'requests' 를 포함해야 합니다.")
print("----------------------------------------------")
# share=True 옵션은 외부 접속 링크 생성 (로컬 테스트 시 불필요하면 제거)
demo.launch(debug=True, show_error=True)
print("==============================================")
print(" 웹 인터페이스 종료됨 ")
print("==============================================") |