diff --git a/all_code.txt b/all_code.txt
index 400faac..07d3234 100644
--- a/all_code.txt
+++ b/all_code.txt
@@ -1429,7 +1429,7 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list, con
async with asyncssh.connect(host, username=username, password=password, known_hosts=None) as conn:
async with conn.create_process(term_type='xterm-256color', term_size=(200, 24), encoding='utf-8') as process:
- async def read_until_quiet(timeout=1.0):
+ async def read_until_quiet(timeout=1.0, prompt_pattern: str = None):
output = ""
while True:
try:
@@ -1439,6 +1439,9 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list, con
if "--More--" in chunk or "More" in chunk:
process.stdin.write(" ")
await process.stdin.drain()
+ # 🌟 精準 Prompt 偵測
+ if prompt_pattern and re.search(prompt_pattern, output):
+ break
except asyncio.TimeoutError:
break
return output
@@ -1447,14 +1450,14 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list, con
if cmts_version == "unknown":
process.stdin.write("show version\n")
await process.stdin.drain()
- version_output = await read_until_quiet(timeout=1.5)
+ version_output = await read_until_quiet(timeout=1.5, prompt_pattern=r"(?:#|>)")
match = re.search(r"(?:infra|vcmts-cd-0)\s+([\w\.\-]+)", version_output)
if match:
cmts_version = match.group(1)
process.stdin.write("config\n")
await process.stdin.drain()
- await read_until_quiet(timeout=1.5)
+ await read_until_quiet(timeout=1.5, prompt_pattern=r"\(config\)#")
for path in batch:
# 🌟 優化 1:強迫讓出事件迴圈控制權,讓 FastAPI 去處理其他使用者的請求
@@ -1609,7 +1612,7 @@ async def fetch_raw_config(host: str, username: str, password: str, config_type:
async with asyncssh.connect(host, username=username, password=password, known_hosts=None) as conn:
async with conn.create_process(term_type='xterm-256color', term_size=(200, 24), encoding='utf-8') as process:
- async def read_until_quiet(timeout=2.0):
+ async def read_until_quiet(timeout=2.0, prompt_pattern: str = None):
output = ""
while True:
try:
@@ -1620,25 +1623,28 @@ async def fetch_raw_config(host: str, username: str, password: str, config_type:
if "--More--" in chunk or "More" in chunk:
process.stdin.write(" ")
await process.stdin.drain()
+ # 🌟 精準 Prompt 偵測
+ if prompt_pattern and re.search(prompt_pattern, output):
+ break
except asyncio.TimeoutError:
break
return output
# 🌟 新增這行:剛連線成功後,先清空終端機的登入歡迎詞 (MOTD) 與雜訊
- await read_until_quiet(timeout=1.0)
+ await read_until_quiet(timeout=1.0, prompt_pattern=r"(?:#|>)")
# 關鍵修改:根據 config_type 切換模式與指令,並加上 | nomore 關閉分頁
if config_type == "full":
# 1. 先進入 config 模式
process.stdin.write("config\n")
await process.stdin.drain()
- await read_until_quiet(timeout=1.5) # 等待提示字元變成 (config)#
+ await read_until_quiet(timeout=1.5, prompt_pattern=r"\(config\)#") # 等待提示字元變成 (config)#
# 2. 下達 full-configuration 指令 (🌟 加上 | nomore)
cmd = "show full-configuration | nomore"
process.stdin.write(f"{cmd}\n")
await process.stdin.drain()
- raw_output = await read_until_quiet(timeout=3.0)
+ raw_output = await read_until_quiet(timeout=3.0, prompt_pattern=r"\(config\)#")
# 3. 抓完後退回上一層 (保持良好習慣)
process.stdin.write("exit\n")
@@ -5127,8 +5133,8 @@ export function collapseAll(elementId) {
// 🌟 核心函數:生成完整設備配置樹狀圖 (Lazy 版)
// ==========================================
export function buildTree(node, path = '', isParentCommandGroup = false, mode = 'running') {
- let html = '';
- if (!node || typeof node !== 'object') return html;
+ const htmlParts = [];
+ if (!node || typeof node !== 'object') return htmlParts.join('');
for (const [key, value] of Object.entries(node)) {
const currentPath = path ? `${path}::${key}` : key;
@@ -5169,8 +5175,7 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
`;
- // 🌟 關鍵修改:加入 data-* 屬性,並在 ontoggle 時呼叫 lazyLoadFolder
- html += `
+ htmlParts.push(`
@@ -5211,9 +5216,8 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
- `;
+ `);
} else {
- // 葉節點邏輯保持不變
const isVirtualIndex = /^\[\d+\]$/.test(key);
const isNoCommand = (key === 'no');
const safeValue = (value === null ? '' : value).toString().replace(/"/g, """);
@@ -5258,7 +5262,7 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
`;
}
- html += `
+ htmlParts.push(`
- `;
+ `);
}
}
- return html;
+ return htmlParts.join('');
}
// ==========================================
@@ -5298,7 +5302,8 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isParentCommandGroup = false, mode = 'running') {
if (typeof data !== 'object' || data === null) return '';
- let html = ``;
+ const htmlParts = [];
+ htmlParts.push(`
`);
for (const key in data) {
const value = data[key];
@@ -5348,7 +5353,7 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa
`;
// 🌟 加入 lazyLoadFolder 觸發
- html += `
+ htmlParts.push(`
@@ -5366,9 +5371,8 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa
- `;
+ `);
} else {
- // 葉節點邏輯保持不變
const isVirtualIndex = /^\[\d+\]$/.test(key);
let displayName = isVirtualIndex ? `
項目 ${key}` : `
${key}`;
const safeValue = (value === null ? '' : value).toString().replace(/"/g, """);
@@ -5389,7 +5393,7 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa
currentIcon = `
`;
}
- html += `
+ htmlParts.push(`
${displayName}${colonHtml}
${valueHtml}
- `;
+ `);
}
}
- html += '
';
- return html;
+ htmlParts.push('');
+ return htmlParts.join('');
}
================================================================================
@@ -5655,7 +5659,7 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest):
async with asyncssh.connect(req.host, username=req.username, password=req.password, known_hosts=None) as conn:
async with conn.create_process(term_type='xterm-256color', term_size=(200, 24), encoding='utf-8') as process:
- async def read_until_quiet(timeout=1.0):
+ async def read_until_quiet(timeout=1.0, prompt_pattern: str = None):
output = ""
while True:
try:
@@ -5665,6 +5669,9 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest):
if "--More--" in chunk or "More" in chunk:
process.stdin.write(" ")
await process.stdin.drain()
+ # 🌟 精準 Prompt 偵測
+ if prompt_pattern and re.search(prompt_pattern, output):
+ break
except asyncio.TimeoutError:
break
return output
@@ -5672,14 +5679,14 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest):
# 2. 進入設定模式
process.stdin.write("config\n")
await process.stdin.drain()
- await read_until_quiet(timeout=1.5)
+ await read_until_quiet(timeout=1.5, prompt_pattern=r"\(config\)#")
yield json.dumps({"status": "progress", "message": "✅ 成功進入 Global Configuration 模式"}) + "\n"
# 3. 逐行寫入並進行 Fail-safe 檢查
for cmd in commands:
process.stdin.write(f"{cmd}\n")
await process.stdin.drain()
- out = await read_until_quiet(timeout=0.2)
+ out = await read_until_quiet(timeout=0.2, prompt_pattern=r"\(config.*\)#")
clean_out = re.sub(r'\x1b\[[0-9;]*[mGK]', '', out).strip()
@@ -5694,7 +5701,7 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest):
# 2. 對設備下達 abort 指令
process.stdin.write("abort\n")
await process.stdin.drain()
- await read_until_quiet(timeout=1.0) # 等待設備處理 abort 並退出 config 模式
+ await read_until_quiet(timeout=1.0, prompt_pattern=r"(?:#|>)") # 等待設備處理 abort 並退出 config 模式
# 3. 回報最終錯誤並關閉連線
yield json.dumps({
@@ -5702,7 +5709,6 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest):
"message": f"❌ 寫入中斷且已安全撤銷!失敗指令: {cmd}",
"output": clean_out
}) + "\n"
- # 🌟 移除手動 conn.close(),交由 async with 處理
return
yield json.dumps({
@@ -5718,8 +5724,7 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest):
yield json.dumps({"status": "progress", "message": "⏳ 正在寫入 Commit 保存設定..."}) + "\n"
process.stdin.write("commit\n")
await process.stdin.drain()
- commit_out = await read_until_quiet(timeout=6.0)
- clean_commit = re.sub(r'\x1b\[[0-9;]*[mGK]', '', commit_out).strip()
+ commit_out = await read_until_quiet(timeout=6.0, prompt_pattern=r"\(config\)#")
# 5. 退出並關閉連線
process.stdin.write("exit\n")
@@ -5753,6 +5758,9 @@ router = APIRouter()
async def websocket_terminal(websocket: WebSocket, host: str, username: str, password: str):
await websocket.accept()
conn = None
+ process = None
+ ws_task = None
+ ssh_task = None
try:
# 建立連線
conn = await asyncssh.connect(host, username=username, password=password, known_hosts=None)
@@ -5774,6 +5782,8 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
safe_text = data_bytes.decode('utf-8', errors='replace')
await websocket.send_text(safe_text)
+ except asyncio.CancelledError:
+ pass
except Exception as e:
print("\n❌ [WS Forward Error] 讀取設備畫面時發生錯誤:")
traceback.print_exc()
@@ -5791,6 +5801,9 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
process.stdin.write(data_bytes)
await process.stdin.drain()
except WebSocketDisconnect:
+ # 主動拋出,讓外層捕捉以進行資源回收
+ raise
+ except asyncio.CancelledError:
pass
except Exception as e:
print("\n❌ [SSH Forward Error] 寫入指令到設備時發生錯誤:")
@@ -5807,6 +5820,12 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
for task in pending:
task.cancel()
+ except WebSocketDisconnect:
+ print("[DEBUG] WebSocket 正常斷線,正在終止背景任務...")
+ if ws_task and not ws_task.done():
+ ws_task.cancel()
+ if ssh_task and not ssh_task.done():
+ ssh_task.cancel()
except Exception as e:
error_msg = f"\r\n\x1b[31mSSH Connection Error: {str(e)}\x1b[0m\r\n"
try:
@@ -5816,6 +5835,9 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
print("\n❌ [Connection Error] 建立連線或執行過程中發生錯誤:")
traceback.print_exc()
finally:
+ # 🌟 確保 process 與 conn 絕對被關閉,防止 Memory Leak
+ if process:
+ process.close()
if conn:
conn.close()
try:
@@ -5824,6 +5846,7 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
pass
+
================================================================================
FILE: routers/lock.py
================================================================================
@@ -6294,7 +6317,7 @@ async def execute_cmts_config(req: ConfigRequest):
async with conn.create_process() as process:
# 🌟 建立安全的非同步讀取函數 (讀到安靜為止,避免卡死)
- async def read_until_quiet(timeout=0.5):
+ async def read_until_quiet(timeout=0.5, prompt_pattern: str = None):
out_data = ""
while True:
try:
@@ -6303,6 +6326,9 @@ async def execute_cmts_config(req: ConfigRequest):
if not chunk:
break
out_data += str(chunk)
+ # 🌟 精準 Prompt 偵測
+ if prompt_pattern and re.search(prompt_pattern, out_data):
+ break
except asyncio.TimeoutError:
break
return out_data
@@ -6310,7 +6336,7 @@ async def execute_cmts_config(req: ConfigRequest):
# 1. 進入設定模式
process.stdin.write("config\n")
await process.stdin.drain()
- await read_until_quiet(0.5) # 清空歡迎訊息
+ await read_until_quiet(0.5, prompt_pattern=r"\(config\)#") # 清空歡迎訊息
# 2. 逐行送出指令並回報進度
for cmd in commands:
@@ -6320,7 +6346,7 @@ async def execute_cmts_config(req: ConfigRequest):
# ⏱️ 智慧等待:如果是 commit 指令,給予 5 秒讓設備存檔;其他指令等 0.5 秒
wait_time = 5.0 if cmd.strip() == "commit" else 0.5
- output = await read_until_quiet(wait_time)
+ output = await read_until_quiet(wait_time, prompt_pattern=r"\(config.*\)#")
# 🛡️ 防呆撤銷機制:偵測到錯誤立刻 abort
lower_output = output.lower()
@@ -6338,7 +6364,7 @@ async def execute_cmts_config(req: ConfigRequest):
# 3. 退出設定模式
process.stdin.write("exit\n")
await process.stdin.drain()
- final_output = await read_until_quiet(1.0)
+ final_output = await read_until_quiet(1.0, prompt_pattern=r"(?:#|>)")
yield json.dumps({
"status": "success",
diff --git a/cmts_scraper.py b/cmts_scraper.py
index 2dfd694..d059997 100644
--- a/cmts_scraper.py
+++ b/cmts_scraper.py
@@ -163,7 +163,7 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list, con
async with asyncssh.connect(host, username=username, password=password, known_hosts=None) as conn:
async with conn.create_process(term_type='xterm-256color', term_size=(200, 24), encoding='utf-8') as process:
- async def read_until_quiet(timeout=1.0):
+ async def read_until_quiet(timeout=1.0, prompt_pattern: str = None):
output = ""
while True:
try:
@@ -173,6 +173,9 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list, con
if "--More--" in chunk or "More" in chunk:
process.stdin.write(" ")
await process.stdin.drain()
+ # 🌟 精準 Prompt 偵測
+ if prompt_pattern and re.search(prompt_pattern, output):
+ break
except asyncio.TimeoutError:
break
return output
@@ -181,14 +184,14 @@ async def sync_cmts_leaves_async(host, username, password, leaf_paths: list, con
if cmts_version == "unknown":
process.stdin.write("show version\n")
await process.stdin.drain()
- version_output = await read_until_quiet(timeout=1.5)
+ version_output = await read_until_quiet(timeout=1.5, prompt_pattern=r"(?:#|>)")
match = re.search(r"(?:infra|vcmts-cd-0)\s+([\w\.\-]+)", version_output)
if match:
cmts_version = match.group(1)
process.stdin.write("config\n")
await process.stdin.drain()
- await read_until_quiet(timeout=1.5)
+ await read_until_quiet(timeout=1.5, prompt_pattern=r"\(config\)#")
for path in batch:
# 🌟 優化 1:強迫讓出事件迴圈控制權,讓 FastAPI 去處理其他使用者的請求
@@ -343,7 +346,7 @@ async def fetch_raw_config(host: str, username: str, password: str, config_type:
async with asyncssh.connect(host, username=username, password=password, known_hosts=None) as conn:
async with conn.create_process(term_type='xterm-256color', term_size=(200, 24), encoding='utf-8') as process:
- async def read_until_quiet(timeout=2.0):
+ async def read_until_quiet(timeout=2.0, prompt_pattern: str = None):
output = ""
while True:
try:
@@ -354,25 +357,28 @@ async def fetch_raw_config(host: str, username: str, password: str, config_type:
if "--More--" in chunk or "More" in chunk:
process.stdin.write(" ")
await process.stdin.drain()
+ # 🌟 精準 Prompt 偵測
+ if prompt_pattern and re.search(prompt_pattern, output):
+ break
except asyncio.TimeoutError:
break
return output
# 🌟 新增這行:剛連線成功後,先清空終端機的登入歡迎詞 (MOTD) 與雜訊
- await read_until_quiet(timeout=1.0)
+ await read_until_quiet(timeout=1.0, prompt_pattern=r"(?:#|>)")
# 關鍵修改:根據 config_type 切換模式與指令,並加上 | nomore 關閉分頁
if config_type == "full":
# 1. 先進入 config 模式
process.stdin.write("config\n")
await process.stdin.drain()
- await read_until_quiet(timeout=1.5) # 等待提示字元變成 (config)#
+ await read_until_quiet(timeout=1.5, prompt_pattern=r"\(config\)#") # 等待提示字元變成 (config)#
# 2. 下達 full-configuration 指令 (🌟 加上 | nomore)
cmd = "show full-configuration | nomore"
process.stdin.write(f"{cmd}\n")
await process.stdin.drain()
- raw_output = await read_until_quiet(timeout=3.0)
+ raw_output = await read_until_quiet(timeout=3.0, prompt_pattern=r"\(config\)#")
# 3. 抓完後退回上一層 (保持良好習慣)
process.stdin.write("exit\n")
diff --git a/routers/backup.py b/routers/backup.py
index b689b0b..a9b6ab8 100644
--- a/routers/backup.py
+++ b/routers/backup.py
@@ -241,7 +241,7 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest):
async with asyncssh.connect(req.host, username=req.username, password=req.password, known_hosts=None) as conn:
async with conn.create_process(term_type='xterm-256color', term_size=(200, 24), encoding='utf-8') as process:
- async def read_until_quiet(timeout=1.0):
+ async def read_until_quiet(timeout=1.0, prompt_pattern: str = None):
output = ""
while True:
try:
@@ -251,6 +251,9 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest):
if "--More--" in chunk or "More" in chunk:
process.stdin.write(" ")
await process.stdin.drain()
+ # 🌟 精準 Prompt 偵測
+ if prompt_pattern and re.search(prompt_pattern, output):
+ break
except asyncio.TimeoutError:
break
return output
@@ -258,14 +261,14 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest):
# 2. 進入設定模式
process.stdin.write("config\n")
await process.stdin.drain()
- await read_until_quiet(timeout=1.5)
+ await read_until_quiet(timeout=1.5, prompt_pattern=r"\(config\)#")
yield json.dumps({"status": "progress", "message": "✅ 成功進入 Global Configuration 模式"}) + "\n"
# 3. 逐行寫入並進行 Fail-safe 檢查
for cmd in commands:
process.stdin.write(f"{cmd}\n")
await process.stdin.drain()
- out = await read_until_quiet(timeout=0.2)
+ out = await read_until_quiet(timeout=0.2, prompt_pattern=r"\(config.*\)#")
clean_out = re.sub(r'\x1b\[[0-9;]*[mGK]', '', out).strip()
@@ -280,7 +283,7 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest):
# 2. 對設備下達 abort 指令
process.stdin.write("abort\n")
await process.stdin.drain()
- await read_until_quiet(timeout=1.0) # 等待設備處理 abort 並退出 config 模式
+ await read_until_quiet(timeout=1.0, prompt_pattern=r"(?:#|>)") # 等待設備處理 abort 並退出 config 模式
# 3. 回報最終錯誤並關閉連線
yield json.dumps({
@@ -288,7 +291,6 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest):
"message": f"❌ 寫入中斷且已安全撤銷!失敗指令: {cmd}",
"output": clean_out
}) + "\n"
- # 🌟 移除手動 conn.close(),交由 async with 處理
return
yield json.dumps({
@@ -304,8 +306,7 @@ async def execute_restore(backup_id: str, req: ExecuteRestoreRequest):
yield json.dumps({"status": "progress", "message": "⏳ 正在寫入 Commit 保存設定..."}) + "\n"
process.stdin.write("commit\n")
await process.stdin.drain()
- commit_out = await read_until_quiet(timeout=6.0)
- clean_commit = re.sub(r'\x1b\[[0-9;]*[mGK]', '', commit_out).strip()
+ commit_out = await read_until_quiet(timeout=6.0, prompt_pattern=r"\(config\)#")
# 5. 退出並關閉連線
process.stdin.write("exit\n")
diff --git a/routers/config.py b/routers/config.py
index daab193..e3f7acc 100644
--- a/routers/config.py
+++ b/routers/config.py
@@ -105,7 +105,7 @@ async def execute_cmts_config(req: ConfigRequest):
async with conn.create_process() as process:
# 🌟 建立安全的非同步讀取函數 (讀到安靜為止,避免卡死)
- async def read_until_quiet(timeout=0.5):
+ async def read_until_quiet(timeout=0.5, prompt_pattern: str = None):
out_data = ""
while True:
try:
@@ -114,6 +114,9 @@ async def execute_cmts_config(req: ConfigRequest):
if not chunk:
break
out_data += str(chunk)
+ # 🌟 精準 Prompt 偵測
+ if prompt_pattern and re.search(prompt_pattern, out_data):
+ break
except asyncio.TimeoutError:
break
return out_data
@@ -121,7 +124,7 @@ async def execute_cmts_config(req: ConfigRequest):
# 1. 進入設定模式
process.stdin.write("config\n")
await process.stdin.drain()
- await read_until_quiet(0.5) # 清空歡迎訊息
+ await read_until_quiet(0.5, prompt_pattern=r"\(config\)#") # 清空歡迎訊息
# 2. 逐行送出指令並回報進度
for cmd in commands:
@@ -131,7 +134,7 @@ async def execute_cmts_config(req: ConfigRequest):
# ⏱️ 智慧等待:如果是 commit 指令,給予 5 秒讓設備存檔;其他指令等 0.5 秒
wait_time = 5.0 if cmd.strip() == "commit" else 0.5
- output = await read_until_quiet(wait_time)
+ output = await read_until_quiet(wait_time, prompt_pattern=r"\(config.*\)#")
# 🛡️ 防呆撤銷機制:偵測到錯誤立刻 abort
lower_output = output.lower()
@@ -149,7 +152,7 @@ async def execute_cmts_config(req: ConfigRequest):
# 3. 退出設定模式
process.stdin.write("exit\n")
await process.stdin.drain()
- final_output = await read_until_quiet(1.0)
+ final_output = await read_until_quiet(1.0, prompt_pattern=r"(?:#|>)")
yield json.dumps({
"status": "success",
diff --git a/routers/terminal.py b/routers/terminal.py
index f25da43..1f31e6c 100644
--- a/routers/terminal.py
+++ b/routers/terminal.py
@@ -9,6 +9,9 @@ router = APIRouter()
async def websocket_terminal(websocket: WebSocket, host: str, username: str, password: str):
await websocket.accept()
conn = None
+ process = None
+ ws_task = None
+ ssh_task = None
try:
# 建立連線
conn = await asyncssh.connect(host, username=username, password=password, known_hosts=None)
@@ -30,6 +33,8 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
safe_text = data_bytes.decode('utf-8', errors='replace')
await websocket.send_text(safe_text)
+ except asyncio.CancelledError:
+ pass
except Exception as e:
print("\n❌ [WS Forward Error] 讀取設備畫面時發生錯誤:")
traceback.print_exc()
@@ -47,6 +52,9 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
process.stdin.write(data_bytes)
await process.stdin.drain()
except WebSocketDisconnect:
+ # 主動拋出,讓外層捕捉以進行資源回收
+ raise
+ except asyncio.CancelledError:
pass
except Exception as e:
print("\n❌ [SSH Forward Error] 寫入指令到設備時發生錯誤:")
@@ -63,6 +71,12 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
for task in pending:
task.cancel()
+ except WebSocketDisconnect:
+ print("[DEBUG] WebSocket 正常斷線,正在終止背景任務...")
+ if ws_task and not ws_task.done():
+ ws_task.cancel()
+ if ssh_task and not ssh_task.done():
+ ssh_task.cancel()
except Exception as e:
error_msg = f"\r\n\x1b[31mSSH Connection Error: {str(e)}\x1b[0m\r\n"
try:
@@ -72,9 +86,13 @@ async def websocket_terminal(websocket: WebSocket, host: str, username: str, pas
print("\n❌ [Connection Error] 建立連線或執行過程中發生錯誤:")
traceback.print_exc()
finally:
+ # 🌟 確保 process 與 conn 絕對被關閉,防止 Memory Leak
+ if process:
+ process.close()
if conn:
conn.close()
try:
await websocket.close()
except:
pass
+
diff --git a/static/tree-ui.js b/static/tree-ui.js
index 617f597..567004b 100644
--- a/static/tree-ui.js
+++ b/static/tree-ui.js
@@ -74,8 +74,8 @@ export function collapseAll(elementId) {
// 🌟 核心函數:生成完整設備配置樹狀圖 (Lazy 版)
// ==========================================
export function buildTree(node, path = '', isParentCommandGroup = false, mode = 'running') {
- let html = '';
- if (!node || typeof node !== 'object') return html;
+ const htmlParts = [];
+ if (!node || typeof node !== 'object') return htmlParts.join('');
for (const [key, value] of Object.entries(node)) {
const currentPath = path ? `${path}::${key}` : key;
@@ -116,8 +116,7 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
`;
- // 🌟 關鍵修改:加入 data-* 屬性,並在 ontoggle 時呼叫 lazyLoadFolder
- html += `
+ htmlParts.push(`
@@ -158,9 +157,8 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
- `;
+ `);
} else {
- // 葉節點邏輯保持不變
const isVirtualIndex = /^\[\d+\]$/.test(key);
const isNoCommand = (key === 'no');
const safeValue = (value === null ? '' : value).toString().replace(/"/g, """);
@@ -205,7 +203,7 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
`;
}
- html += `
+ htmlParts.push(`
- `;
+ `);
}
}
- return html;
+ return htmlParts.join('');
}
// ==========================================
@@ -245,7 +243,8 @@ export function buildTree(node, path = '', isParentCommandGroup = false, mode =
export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isParentCommandGroup = false, mode = 'running') {
if (typeof data !== 'object' || data === null) return '';
- let html = ``;
+ const htmlParts = [];
+ htmlParts.push(`
`);
for (const key in data) {
const value = data[key];
@@ -295,7 +294,7 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa
`;
// 🌟 加入 lazyLoadFolder 觸發
- html += `
+ htmlParts.push(`
@@ -313,9 +312,8 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa
- `;
+ `);
} else {
- // 葉節點邏輯保持不變
const isVirtualIndex = /^\[\d+\]$/.test(key);
let displayName = isVirtualIndex ? `
項目 ${key}` : `
${key}`;
const safeValue = (value === null ? '' : value).toString().replace(/"/g, """);
@@ -336,7 +334,7 @@ export function buildRealFilterTree(data, parentPath = '', hiddenKeys = [], isPa
currentIcon = `
`;
}
- html += `
+ htmlParts.push(`
${displayName}${colonHtml}
${valueHtml}
- `;
+ `);
}
}
- html += '
';
- return html;
+ htmlParts.push('');
+ return htmlParts.join('');
}
\ No newline at end of file