""" RAG 검색 챗봇 웹 애플리케이션 - 장치 관리 API 라우트 정의 (사용자 정의 실행, f-string 오류 수정됨) """ import logging import requests import uuid # 사용자 정의 실행을 위해 추가 import time # 사용자 정의 실행을 위해 추가 import shlex # 사용자 정의 실행을 위해 추가 import os # 경로 처리를 위해 추가 (필요시) from flask import request, jsonify import json # 로거 가져오기 logger = logging.getLogger(__name__) def register_device_routes(app, login_required, DEVICE_SERVER_URL): """Flask 애플리케이션에 장치 관리 관련 라우트 등록""" # 사용자 지정 장치 서버 URL 변수 custom_device_url = None # URL 설정 함수 def get_device_url(): # 사용자 지정 URL이 있으면 사용, 없으면 환경변수 값 사용 return custom_device_url or DEVICE_SERVER_URL @app.route('/api/device/connect', methods=['POST']) @login_required def connect_device_server(): """사용자 지정 장치 서버 URL 연결 API""" nonlocal custom_device_url # 상위 스코프의 변수 참조 try: # 요청에서 URL 가져오기 request_data = request.get_json() if not request_data or 'url' not in request_data: logger.error("URL이 제공되지 않았습니다.") return jsonify({ "success": False, "error": "URL이 제공되지 않았습니다." }), 400 # Bad Request new_url = request_data['url'].strip() if not new_url: logger.error("URL이 비어 있습니다.") return jsonify({ "success": False, "error": "URL이 비어 있습니다." }), 400 # Bad Request # URL 설정 logger.info(f"사용자 지정 장치 서버 URL 설정: {new_url}") custom_device_url = new_url # 설정된 URL로 상태 확인 시도 try: api_path = "/api/status" logger.info(f"장치 서버 상태 확인 요청: {custom_device_url}{api_path}") response = requests.get(f"{custom_device_url}{api_path}", timeout=5) if response.status_code == 200: try: data = response.json() logger.info(f"사용자 지정 장치 서버 연결 성공. 응답 데이터: {data}") return jsonify({ "success": True, "message": "장치 서버 연결에 성공했습니다.", "server_status": data.get("status", "정상") # 서버 응답 구조에 따라 키 조정 필요 }) except requests.exceptions.JSONDecodeError: logger.error("장치 서버 응답 JSON 파싱 실패") return jsonify({ "success": False, "error": "장치 서버로부터 유효하지 않은 JSON 응답을 받았습니다." }), 502 # Bad Gateway else: error_message = f"장치 서버 응답 오류: {response.status_code}" try: error_detail = response.json().get("error", response.text) error_message += f" - {error_detail}" except Exception: error_message += f" - {response.text}" logger.warning(error_message) custom_device_url = None # 연결 실패 시 URL 초기화 return jsonify({ "success": False, "error": error_message }), 502 # Bad Gateway except requests.exceptions.Timeout: logger.error(f"장치 서버 연결 타임아웃 ({custom_device_url})") custom_device_url = None # 연결 실패 시 URL 초기화 return jsonify({ "success": False, "error": "장치 서버 연결 타임아웃. 서버 응답이 너무 느립니다." }), 504 # Gateway Timeout except requests.exceptions.ConnectionError: logger.error(f"장치 관리 서버 연결 실패 ({custom_device_url})") custom_device_url = None # 연결 실패 시 URL 초기화 return jsonify({ "success": False, "error": "장치 관리 서버에 연결할 수 없습니다. 서버가 실행 중인지, URL이 정확한지 확인해주세요." }), 502 # Bad Gateway except Exception as e: logger.error(f"장치 서버 연결 중 예상치 못한 오류 발생: {e}", exc_info=True) custom_device_url = None # 연결 실패 시 URL 초기화 return jsonify({ "success": False, "error": f"장치 서버 연결 중 오류 발생: {str(e)}" }), 500 # Internal Server Error except Exception as e: logger.error(f"/api/device/connect 처리 중 내부 서버 오류: {e}", exc_info=True) return jsonify({ "success": False, "error": f"내부 서버 오류 발생: {str(e)}" }), 500 # Internal Server Error @app.route('/api/device/status', methods=['GET']) @login_required def device_status(): """장치 관리 서버의 상태를 확인하는 API 엔드포인트""" try: # 연결 타임아웃 설정 timeout = 5 # 5초로 타임아웃 설정 try: # 장치 서버 상태 확인 - 경로: /api/status current_device_url = get_device_url() api_path = "/api/status" logger.info(f"장치 서버 상태 확인 요청: {current_device_url}{api_path}") response = requests.get(f"{current_device_url}{api_path}", timeout=timeout) # 응답 상태 코드 및 내용 로깅 (디버깅 강화) logger.debug(f"장치 서버 응답 상태 코드: {response.status_code}") try: logger.debug(f"장치 서버 응답 내용: {response.text[:200]}...") # 너무 길면 잘라서 로깅 except Exception: logger.debug("장치 서버 응답 내용 로깅 실패 (텍스트 형식 아님?)") if response.status_code == 200: # 성공 시 응답 데이터 구조 확인 및 로깅 try: data = response.json() logger.info(f"장치 서버 상태 확인 성공. 응답 데이터: {data}") return jsonify({"success": True, "status": "connected", "data": data}) except requests.exceptions.JSONDecodeError: logger.error("장치 서버 응답 JSON 파싱 실패") return jsonify({ "success": False, "error": "장치 서버로부터 유효하지 않은 JSON 응답을 받았습니다." }), 502 # Bad Gateway (업스트림 서버 오류) else: # 실패 시 오류 메시지 포함 로깅 error_message = f"장치 서버 응답 오류: {response.status_code}" try: # 서버에서 오류 메시지를 json으로 보내는 경우 포함 error_detail = response.json().get("error", response.text) error_message += f" - {error_detail}" except Exception: error_message += f" - {response.text}" # JSON 파싱 실패 시 원본 텍스트 logger.warning(error_message) # 경고 레벨로 로깅 return jsonify({ "success": False, "error": error_message }), 502 # Bad Gateway except requests.exceptions.Timeout: logger.error(f"장치 서버 연결 타임아웃 ({get_device_url()})") return jsonify({ "success": False, "error": "장치 서버 연결 타임아웃. 서버 응답이 너무 느립니다." }), 504 # Gateway Timeout except requests.exceptions.ConnectionError: current_device_url = get_device_url() logger.error(f"장치 관리 서버 연결 실패 ({current_device_url})") return jsonify({ "success": False, "error": "장치 관리 서버에 연결할 수 없습니다. 서버가 실행 중인지, URL이 정확한지 확인해주세요." }), 502 # Bad Gateway (연결 실패도 업스트림 문제로 간주) except Exception as e: logger.error(f"장치 서버 연결 중 예상치 못한 오류 발생: {e}", exc_info=True) # 상세 스택 트레이스 로깅 return jsonify({ "success": False, "error": f"장치 서버 연결 중 오류 발생: {str(e)}" }), 500 # Internal Server Error (Flask 앱 내부 또는 requests 라이브러리 문제 가능성) except Exception as e: # 이 try-except 블록은 /api/device/status 라우트 자체의 내부 오류 처리 logger.error(f"/api/device/status 처리 중 내부 서버 오류: {e}", exc_info=True) return jsonify({ "success": False, "error": f"내부 서버 오류 발생: {str(e)}" }), 500 # Internal Server Error @app.route('/api/device/list', methods=['GET']) @login_required def device_list(): """장치 목록 조회 API""" logger.info("장치 목록 조회 요청") try: current_device_url = get_device_url() api_path = "/api/devices" # LocalPCAgent API 문서에 따라 확인 필요 logger.info(f"장치 목록 조회 요청: {current_device_url}{api_path}") response = requests.get(f"{current_device_url}{api_path}", timeout=5) logger.debug(f"장치 목록 응답 상태 코드: {response.status_code}") if response.status_code == 200: try: data = response.json() devices = data.get("devices", []) # LocalPCAgent 응답 형식에 맞게 키 조정 logger.info(f"장치 목록 조회 성공: {len(devices)}개 장치") return jsonify({ "success": True, "devices": devices }) except requests.exceptions.JSONDecodeError: logger.error("장치 목록 응답 JSON 파싱 실패") return jsonify({"success": False, "error": "장치 서버로부터 유효하지 않은 JSON 응답"}), 502 else: error_message = f"장치 목록 조회 실패: {response.status_code}" try: error_message += f" - {response.json().get('error', response.text)}" except Exception: error_message += f" - {response.text}" logger.warning(error_message) return jsonify({ "success": False, "error": error_message }), 502 except requests.exceptions.Timeout: logger.error("장치 목록 조회 시간 초과") return jsonify({ "success": False, "error": "장치 목록 조회 시간이 초과되었습니다." }), 504 except requests.exceptions.ConnectionError: logger.error("장치 관리 서버 연결 실패") return jsonify({ "success": False, "error": "장치 관리 서버에 연결할 수 없습니다. 서버가 실행 중인지 확인해주세요." }), 503 # Service Unavailable except Exception as e: logger.error(f"장치 목록 조회 중 오류 발생: {e}", exc_info=True) return jsonify({ "success": False, "error": f"장치 목록 조회 중 오류 발생: {str(e)}" }), 500 @app.route('/api/device/programs', methods=['GET']) @login_required def device_programs(): """실행 가능한 프로그램 목록 조회 API""" logger.info("프로그램 목록 조회 요청") try: current_device_url = get_device_url() api_path = "/api/programs" logger.info(f"프로그램 목록 조회 요청: {current_device_url}{api_path}") response = requests.get(f"{current_device_url}{api_path}", timeout=5) logger.debug(f"프로그램 목록 응답 상태 코드: {response.status_code}") if response.status_code == 200: try: data = response.json() programs = data.get("programs", []) logger.info(f"프로그램 목록 조회 성공: {len(programs)}개 프로그램") return jsonify({ "success": True, "programs": programs }) except requests.exceptions.JSONDecodeError: logger.error("프로그램 목록 응답 JSON 파싱 실패") return jsonify({"success": False, "error": "장치 서버로부터 유효하지 않은 JSON 응답"}), 502 else: error_message = f"프로그램 목록 조회 실패: {response.status_code}" try: error_message += f" - {response.json().get('error', response.text)}" except Exception: error_message += f" - {response.text}" logger.warning(error_message) return jsonify({ "success": False, "error": error_message }), 502 except requests.exceptions.Timeout: logger.error("프로그램 목록 조회 시간 초과") return jsonify({ "success": False, "error": "프로그램 목록 조회 시간이 초과되었습니다." }), 504 except requests.exceptions.ConnectionError: logger.error("장치 관리 서버 연결 실패") return jsonify({ "success": False, "error": "장치 관리 서버에 연결할 수 없습니다. 서버가 실행 중인지 확인해주세요." }), 503 except Exception as e: logger.error(f"프로그램 목록 조회 중 오류 발생: {e}", exc_info=True) return jsonify({ "success": False, "error": f"프로그램 목록 조회 중 오류 발생: {str(e)}" }), 500 @app.route('/api/device/programs//execute', methods=['POST']) @login_required def execute_program(program_id): """등록된 프로그램 실행 API""" logger.info(f"등록된 프로그램 실행 요청: {program_id}") try: current_device_url = get_device_url() api_path = f"/api/programs/{program_id}/execute" logger.info(f"프로그램 실행 요청: {current_device_url}{api_path}") response = requests.post( f"{current_device_url}{api_path}", json={}, # 필요시 여기에 파라미터 추가 가능 timeout=10 # 프로그램 실행에는 더 긴 시간 부여 ) logger.debug(f"프로그램 실행 응답 상태 코드: {response.status_code}") if response.status_code == 200: try: data = response.json() # LocalPCAgent 응답에서 필요한 정보 추출 (예: success, message, output 등) logger.info(f"프로그램 실행 응답: {data}") return jsonify(data) # 서버 응답 구조에 맞춰 반환 except requests.exceptions.JSONDecodeError: logger.error("프로그램 실행 응답 JSON 파싱 실패") return jsonify({ "success": False, "error": "프로그램 실행 서버로부터 유효하지 않은 JSON 응답" }), 502 else: error_message = f"프로그램 실행 요청 실패: {response.status_code}" try: error_message += f" - {response.json().get('error', response.text)}" except Exception: error_message += f" - {response.text}" logger.warning(error_message) return jsonify({ "success": False, "error": error_message }), 502 # 또는 response.status_code 를 그대로 반환하는 것도 고려 except requests.exceptions.Timeout: logger.error("프로그램 실행 요청 시간 초과") return jsonify({ "success": False, "error": "프로그램 실행 요청 시간이 초과되었습니다." }), 504 except requests.exceptions.ConnectionError: logger.error("장치 관리 서버 연결 실패") return jsonify({ "success": False, "error": "장치 관리 서버에 연결할 수 없습니다. 서버가 실행 중인지 확인해주세요." }), 503 except Exception as e: logger.error(f"프로그램 실행 중 오류 발생: {e}", exc_info=True) return jsonify({ "success": False, "error": f"프로그램 실행 중 오류 발생: {str(e)}" }), 500 # ================== 사용자 정의 프로그램 실행 엔드포인트 추가 ================== @app.route('/api/device/execute-custom', methods=['POST']) @login_required def execute_custom_program(): """사용자 정의 프로그램 실행 API - 임시 ID를 생성하여 등록 후 실행""" logger.info("사용자 정의 프로그램 임시 등록 및 실행 요청") # LocalPCAgent 서버 응답을 저장할 변수 execute_data = None final_response = None temp_id = None # 임시 ID 저장 변수 try: # 요청 데이터 확인 request_data = request.get_json() if not request_data or 'command' not in request_data: logger.error("명령어가 제공되지 않았습니다.") return jsonify({ "success": False, "error": "실행할 명령어를 제공해주세요." }), 400 # Bad Request command = request_data['command'].strip() if not command: logger.error("명령어가 비어 있습니다.") return jsonify({ "success": False, "error": "실행할 명령어를 입력해주세요." }), 400 # Bad Request # 현재 장치 서버 URL 가져오기 current_device_url = get_device_url() if not current_device_url: logger.error("장치 서버 URL이 설정되지 않았습니다.") return jsonify({"success": False, "error": "장치 서버 URL이 설정되지 않아 연결할 수 없습니다."}), 503 # --- 1. 임시 프로그램 등록 --- # 임시 프로그램 ID 생성 (시간 기반 uuid) temp_id = f"temp_program_{int(time.time())}_{uuid.uuid4().hex[:8]}" # 명령어 경로와 인수 분리 (shlex 사용) try: # 따옴표로 묶인 명령어 처리 (Windows 경로 등) parts = shlex.split(command) except Exception as parse_err: # shlex 파싱 오류 시 간단한 방식으로 분리 (예외 케이스 처리) logger.warning(f"shlex 파싱 오류 ({parse_err}), 단순 분리 시도: {command}") parts = command.split(maxsplit=1) # 실행파일과 나머지 인수로 분리 시도 if not parts: # 분리 결과가 없으면 오류 logger.error(f"명령어 파싱 실패: {command}") return jsonify({"success": False, "error": "명령어 형식을 인식할 수 없습니다."}), 400 path = parts[0] args = parts[1:] if len(parts) > 1 else [] # ================== 수정된 부분 시작 ================== # 경로에서 파일명만 추출 (백슬래시 문제 해결) # os.path.basename 사용 또는 문자열 처리 방식 사용 try: # 모든 백슬래시를 슬래시로 변경 후 마지막 부분 추출 filename = path.replace('\\', '/').split('/')[-1] if not filename: # 경로가 '/'나 '\\'로 끝나는 경우 대비 filename = path # 원본 경로를 사용하거나 다른 기본값 설정 except Exception: filename = "unknown" # 예외 발생 시 기본값 # ================== 수정된 부분 끝 ==================== # 프로그램 등록 API 호출 데이터 구성 logger.info(f"임시 프로그램 등록 시도: ID={temp_id}, 경로='{path}', 인수={args}") register_data = { "id": temp_id, # 수정된 filename 사용 "name": f"임시 프로그램 ({filename})", "path": path, "args": args, "ui_required": True, # UI가 필요한 것으로 가정 (필요시 수정) "description": f"임시 등록된 프로그램: {command}" } register_response = requests.post( f"{current_device_url}/api/programs", # LocalPCAgent의 프로그램 등록 엔드포인트 json=register_data, timeout=5 ) if not register_response.ok: # 등록 실패 시 오류 처리 logger.error(f"임시 프로그램 등록 실패: {register_response.status_code}") try: error_data = register_response.json() error_message = error_data.get('error', register_response.text) except: error_message = register_response.text return jsonify({ "success": False, "error": f"임시 프로그램 등록 실패({register_response.status_code}): {error_message}" }), 502 # Bad Gateway (Upstream 실패) # --- 2. 임시 프로그램 실행 --- logger.info(f"임시 프로그램 실행 시도: {temp_id}") execute_response = requests.post( f"{current_device_url}/api/programs/{temp_id}/execute", # LocalPCAgent의 프로그램 실행 엔드포인트 json={}, timeout=15 # 실행은 더 긴 시간 부여 ) # 실행 결과를 우선 저장 (삭제 후 반환하기 위해) if execute_response.ok: try: execute_data = execute_response.json() # 성공 응답을 기본 응답으로 설정 final_response = jsonify(execute_data) logger.info(f"임시 프로그램 실행 성공 (응답 코드 {execute_response.status_code}): {execute_data}") except Exception as parse_error: logger.error(f"임시 프로그램 실행 성공 응답 파싱 실패: {parse_error}") # 파싱 실패 시 오류 응답 설정 final_response = jsonify({ "success": False, "error": f"프로그램 실행 응답(성공:{execute_response.status_code}) 파싱 실패: {str(parse_error)}" }), 502 else: # 실행 실패 시 오류 처리 logger.error(f"임시 프로그램 실행 실패: {execute_response.status_code}") try: error_data = execute_response.json() error_message = error_data.get('error', execute_response.text) except: error_message = execute_response.text # 실행 실패 응답을 기본 응답으로 설정 final_response = jsonify({ "success": False, "error": f"프로그램 실행 실패({execute_response.status_code}): {error_message}" }), 502 # --- 3. 임시 프로그램 삭제 (try-finally 블록으로 이동) --- # 실행 성공/실패 여부와 관계없이 삭제 시도 return final_response # 저장된 최종 응답 반환 except requests.exceptions.Timeout: logger.error("사용자 정의 프로그램 요청 시간 초과 (등록 또는 실행 단계)") return jsonify({ "success": False, "error": "프로그램 요청 시간이 초과되었습니다 (LocalPCAgent 응답 없음)." }), 504 # Gateway Timeout except requests.exceptions.ConnectionError: logger.error("장치 관리 서버 연결 실패 (등록 또는 실행 단계)") return jsonify({ "success": False, "error": "장치 관리 서버에 연결할 수 없습니다. 서버가 실행 중인지 확인해주세요." }), 503 # Service Unavailable except Exception as e: logger.error(f"사용자 정의 프로그램 실행 중 Flask 측 오류 발생: {e}", exc_info=True) return jsonify({ "success": False, "error": f"프로그램 실행 중 내부 오류 발생: {str(e)}" }), 500 # Internal Server Error finally: # --- 3. 임시 프로그램 삭제 (finally 블록에서 실행) --- if temp_id and get_device_url(): # 임시 ID가 생성되었고 URL이 유효할 때만 삭제 시도 current_device_url = get_device_url() # URL 다시 가져오기 (필요시) logger.info(f"임시 프로그램 삭제 시도: {temp_id}") try: delete_response = requests.delete( f"{current_device_url}/api/programs/{temp_id}", # LocalPCAgent의 프로그램 삭제 엔드포인트 timeout=3 ) if delete_response.ok: logger.info(f"임시 프로그램 삭제 성공: {temp_id}") else: logger.warning(f"임시 프로그램 삭제 실패 ({delete_response.status_code}): {temp_id} - {delete_response.text[:100]}") except Exception as delete_error: logger.warning(f"임시 프로그램 삭제 중 오류 발생 (무시됨): {delete_error}") elif not temp_id: logger.debug("임시 ID가 생성되지 않아 삭제 건너<0xEB>뜀.") elif not get_device_url(): logger.warning("장치 서버 URL이 없어 임시 프로그램 삭제 건너<0xEB>뜀.") @app.route('/api/device/scan-ports', methods=['GET']) @login_required def scan_device_ports(): """로컬PC에 연결된 장치(COM 포트 및 USB 장치) 목록 조회 API""" logger.info("장치 포트 스캔 요청") try: # 프로그램 ID (장치 스캔 스크립트 ID - scan_ports) program_id = "scan_ports" # 현재 장치 서버 URL 가져오기 current_device_url = get_device_url() if not current_device_url: logger.error("장치 서버 URL이 설정되지 않았습니다.") return jsonify({ "success": False, "error": "장치 서버 URL이 설정되지 않아 연결할 수 없습니다." }), 503 # Service Unavailable # 장치 스캔 프로그램 실행 (프로그램 ID: scan_ports) api_path = f"/api/programs/{program_id}/execute" logger.info(f"장치 포트 스캔 프로그램 실행 요청: {current_device_url}{api_path}") # 요청 전송: 장치 스캔 스크립트 실행 response = requests.post( f"{current_device_url}{api_path}", json={}, # 필요한 파라미터가 있으면 여기에 추가 timeout=20 # 장치 스캔은 시간이 오래 걸릴 수 있으므로 타임아웃 증가 ) logger.debug(f"장치 포트 스캔 응답 상태 코드: {response.status_code}") if response.status_code == 200: try: data = response.json() # LocalPCAgent 응답에서 필요한 정보 추출 if data.get("success", False): # 스캔 결과 출력 가져오기 output = data.get("output", "") try: # 출력이 JSON 형식인지 확인 및 파싱 scan_results = json.loads(output) logger.info("장치 포트 스캔 결과 파싱 성공") # 결과 반환 return jsonify({ "success": True, "timestamp": scan_results.get("timestamp", ""), "system_info": scan_results.get("system_info", {}), "devices": scan_results.get("devices", {}) }) except json.JSONDecodeError as json_err: logger.error(f"장치 포트 스캔 결과 JSON 파싱 실패: {json_err}") return jsonify({ "success": False, "error": "장치 포트 스캔 결과를 JSON으로 파싱할 수 없습니다.", "raw_output": output[:1000] # 긴 출력은 잘라서 반환 }), 500 else: # 프로그램 실행은 성공했지만 스캔 자체가 실패한 경우 error_message = data.get("error", "알 수 없는 오류") logger.error(f"장치 포트 스캔 프로그램 오류: {error_message}") return jsonify({ "success": False, "error": f"장치 포트 스캔 프로그램 오류: {error_message}" }), 500 except Exception as parse_err: logger.error(f"장치 포트 스캔 응답 처리 중 오류: {parse_err}") return jsonify({ "success": False, "error": f"장치 포트 스캔 응답 처리 중 오류: {str(parse_err)}" }), 500 else: # API 요청 자체가 실패한 경우 error_message = f"장치 포트 스캔 API 요청 실패 (상태 코드: {response.status_code})" try: error_data = response.json() if 'error' in error_data: error_message += f": {error_data['error']}" except: if response.text: error_message += f": {response.text[:200]}" logger.error(error_message) return jsonify({ "success": False, "error": error_message }), response.status_code except requests.exceptions.Timeout: error_message = "장치 포트 스캔 요청 시간 초과" logger.error(error_message) return jsonify({ "success": False, "error": error_message }), 504 # Gateway Timeout except requests.exceptions.ConnectionError: error_message = f"장치 서버 연결 실패 ({get_device_url()})" logger.error(error_message) return jsonify({ "success": False, "error": "장치 서버에 연결할 수 없습니다. 서버가 실행 중인지 확인해주세요." }), 502 # Bad Gateway except Exception as e: logger.error(f"장치 포트 스캔 요청 처리 중 예외 발생: {e}", exc_info=True) return jsonify({ "success": False, "error": f"장치 포트 스캔 처리 중 오류: {str(e)}" }), 500 # Internal Server Error # register_device_routes 함수의 끝