update http && process logs

pull/524/head
yuyu5333 2025-11-13 07:01:05 +00:00
parent 3a03cedd00
commit 7c947e59c1
2 changed files with 138 additions and 96 deletions

View File

@ -197,9 +197,59 @@ function startProcessPolling() {
}, 5000); }, 5000);
} }
// 带超时和重试的fetch请求函数
function fetchWithTimeoutAndRetry(url, options = {}, timeout = 10000, retries = 3) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
// 添加缓存控制头
const fetchOptions = {
...options,
headers: {
...options.headers,
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
},
signal: controller.signal
};
return fetch(url, fetchOptions)
.then(response => {
clearTimeout(timeoutId);
// 检查响应是否成功
if (!response.ok) {
throw new Error(`HTTP错误! 状态码: ${response.status}`);
}
return response;
})
.catch(error => {
clearTimeout(timeoutId);
// 如果是AbortError,说明请求超时
if (error.name === 'AbortError') {
throw new Error('请求超时');
}
// 如果还有重试次数,重新发起请求
if (retries > 0) {
console.warn(`请求失败,${retries}次重试剩余,${timeout/2}ms后重试...`);
return new Promise(resolve => {
setTimeout(() => {
resolve(fetchWithTimeoutAndRetry(url, options, timeout, retries - 1));
}, timeout / 2);
});
}
// 重试次数用完,抛出错误
throw error;
});
}
// 检查进程状态变化(特别是错误状态) // 检查进程状态变化(特别是错误状态)
function checkProcessStatusChanges() { function checkProcessStatusChanges() {
fetch('/processes') fetchWithTimeoutAndRetry('/processes')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
// 遍历每个进程,检查状态变化 // 遍历每个进程,检查状态变化
@ -223,6 +273,8 @@ function checkProcessStatusChanges() {
}) })
.catch(error => { .catch(error => {
console.error('检查进程状态时出错:', error); console.error('检查进程状态时出错:', error);
// 显示连接错误通知
showNotification('连接服务器失败,请刷新页面重试', 'error');
}); });
} }
@ -243,7 +295,7 @@ function checkAndOpenSwanlab(processId) {
// 如果没有URL或URL不完整,尝试从后端获取最新的进程信息 // 如果没有URL或URL不完整,尝试从后端获取最新的进程信息
if (!currentUrl || currentUrl.trim() === '') { if (!currentUrl || currentUrl.trim() === '') {
fetch('/processes') fetchWithTimeoutAndRetry('/processes')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const process = data.find(p => p.id === processId); const process = data.find(p => p.id === processId);
@ -414,7 +466,7 @@ function updateProcessItem(processItem, process) {
// 加载进程列表 // 加载进程列表
function loadProcesses() { function loadProcesses() {
fetch('/processes') fetchWithTimeoutAndRetry('/processes')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const processList = document.getElementById('process-list'); const processList = document.getElementById('process-list');
@ -709,7 +761,7 @@ function loadLogContent(processId, logsContainer) {
const oldContent = logsContainer.textContent; const oldContent = logsContainer.textContent;
const isScrolledToBottom = logsContainer.scrollHeight - logsContainer.scrollTop <= logsContainer.clientHeight + 10; const isScrolledToBottom = logsContainer.scrollHeight - logsContainer.scrollTop <= logsContainer.clientHeight + 10;
fetch(`/logs/${processId}`) fetchWithTimeoutAndRetry(`/logs/${processId}`)
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error('获取日志失败'); throw new Error('获取日志失败');
@ -838,16 +890,16 @@ function deleteProcess(processId) {
'确定要删除这个训练进程吗?此操作不可恢复。', '确定要删除这个训练进程吗?此操作不可恢复。',
() => { () => {
// 确认删除 // 确认删除
fetch(`/delete/${processId}`, { fetchWithTimeoutAndRetry(`/delete/${processId}`, {
method: 'POST' method: 'POST'
}) })
.then(response => { .then(response => {
// 检查响应状态 // 检查响应状态
if (!response.ok) { if (!response.ok) {
throw new Error('删除请求失败'); throw new Error('删除请求失败');
} }
return response.json().catch(() => ({})); // 即使没有JSON响应也继续 return response.json().catch(() => ({})); // 即使没有JSON响应也继续
}) })
.then(() => { .then(() => {
// 从UI中移除进程项 // 从UI中移除进程项
const processItem = document.querySelector(`[data-process-id="${processId}"]`); const processItem = document.querySelector(`[data-process-id="${processId}"]`);
@ -939,9 +991,9 @@ function stopProcess(processId) {
'确定要停止这个训练进程吗?', '确定要停止这个训练进程吗?',
() => { () => {
// 确认停止 // 确认停止
fetch(`/stop/${processId}`, { fetchWithTimeoutAndRetry(`/stop/${processId}`, {
method: 'POST' method: 'POST'
}) })
.then(() => { .then(() => {
// 立即给用户反馈,设置为手动停止状态 // 立即给用户反馈,设置为手动停止状态
const processItem = document.querySelector(`[data-process-id="${processId}"]`); const processItem = document.querySelector(`[data-process-id="${processId}"]`);
@ -972,7 +1024,7 @@ function stopProcess(processId) {
showNotification('训练进程已停止', 'info'); showNotification('训练进程已停止', 'info');
// 为了确保状态准确,仍然获取完整列表并更新单个进程 // 为了确保状态准确,仍然获取完整列表并更新单个进程
fetch('/processes') fetchWithTimeoutAndRetry('/processes')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
// 从完整列表中找到特定进程 // 从完整列表中找到特定进程
@ -1033,10 +1085,11 @@ document.getElementById('train-form').addEventListener('submit', function(e) {
// 延迟1秒后发送请求,确保两个通知之间有间隔 // 延迟1秒后发送请求,确保两个通知之间有间隔
setTimeout(() => { setTimeout(() => {
fetch('/train', { fetchWithTimeoutAndRetry('/train', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
}, },
body: JSON.stringify(data) body: JSON.stringify(data)
}) })
@ -1072,7 +1125,7 @@ document.getElementById('train-form').addEventListener('submit', function(e) {
// 加载日志文件列表 // 加载日志文件列表
function loadLogFiles() { function loadLogFiles() {
fetch('/logfiles') fetchWithTimeoutAndRetry('/logfiles')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
const logfilesList = document.getElementById('logfiles-list'); const logfilesList = document.getElementById('logfiles-list');
@ -1270,8 +1323,11 @@ function deleteLogFile(filename, button) {
button.disabled = true; button.disabled = true;
// 发送删除请求到服务器 // 发送删除请求到服务器
fetch(`/delete-logfile/${encodeURIComponent(filename)}`, { fetchWithTimeoutAndRetry(`/delete-logfile/${encodeURIComponent(filename)}`, {
method: 'DELETE' method: 'DELETE',
headers: {
'Cache-Control': 'no-cache'
}
}) })
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
@ -1345,7 +1401,7 @@ function viewLogFile(filename, button) {
// 读取完整日志内容 // 读取完整日志内容
logContainer.textContent = '加载中...'; logContainer.textContent = '加载中...';
fetch(`/logfile-content/${encodeURIComponent(filename)}`) fetchWithTimeoutAndRetry(`/logfile-content/${encodeURIComponent(filename)}`)
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error('获取日志失败'); throw new Error('获取日志失败');

View File

@ -280,82 +280,68 @@ def logs(process_id):
return '日志文件不存在或已被删除' return '日志文件不存在或已被删除'
try: try:
# 使用二进制模式读取,然后尝试解码以处理不同编码的日志文件 # 使用高效且健壮的方法读取文件的最后200行
def read_log_file_robust(file_path): def read_last_n_lines(file_path, n=200):
# 尝试多种编码方式读取文件 # 使用二进制模式读取文件,避免编码问题
with open(file_path, 'rb') as f:
# 获取文件大小
f.seek(0, os.SEEK_END)
file_size = f.tell()
# 如果文件很小,直接读取整个文件
if file_size < 1024 * 1024: # 小于1MB的文件直接读取
f.seek(0)
content = f.read()
return process_content(content)
# 对于大文件,使用缓冲读取末尾部分
# 估计需要读取的字节数(假设每行平均100字节)
buffer_size = n * 200 # 为了保险,读取更多字节
# 定位到适当的位置
position = max(0, file_size - buffer_size)
f.seek(position)
# 读取缓冲区内容
buffer = f.read(file_size - position)
# 处理缓冲区内容
lines = process_content(buffer)
# 确保我们获取到完整的行
# 如果缓冲区不是从文件开头开始,第一个行可能不完整
if position > 0:
# 跳过第一个可能不完整的行
if len(lines) > 1:
lines = lines[1:]
else:
# 如果只有一行且不在文件开头,可能需要读取更多
# 这里简单处理,直接读取整个文件(罕见情况)
f.seek(0)
content = f.read()
lines = process_content(content)
# 返回最后n行
return lines[-n:] if len(lines) > n else lines
def process_content(content):
# 尝试多种编码方式解码内容
encodings = ['utf-8', 'latin-1', 'gbk', 'gb2312'] encodings = ['utf-8', 'latin-1', 'gbk', 'gb2312']
for encoding in encodings: for encoding in encodings:
try: try:
with open(file_path, 'r', encoding=encoding) as f: text = content.decode(encoding)
return f.read(), encoding # 使用True参数保留换行符,确保行分隔符正确
return text.splitlines(True)
except UnicodeDecodeError: except UnicodeDecodeError:
continue continue
# 如果所有编码都失败,使用二进制模式读取并替换不可解码的字符 # 如果所有编码都失败,使用错误替换模式
with open(file_path, 'rb') as f: text = content.decode('utf-8', errors='replace')
content = f.read() return text.splitlines(True)
return content.decode('utf-8', errors='replace'), 'binary_decoded'
# 使用高效的方法读取文件的最后200行,确保以完整行为单位 # 读取最后200行
last_200_lines = [] last_200_lines = read_last_n_lines(log_file, 200)
# 先尝试使用二进制模式读取文件末尾的部分
with open(log_file, 'rb') as f:
# 尝试直接定位到文件末尾,然后向前读取
f.seek(0, os.SEEK_END)
file_size = f.tell()
# 计算需要读取的块数
position = file_size
blocks = []
block_size = 8192 # 8KB blocks
# 确保我们有足够的数据来处理完整行
found_complete_lines = False
while position > 0 and not found_complete_lines:
# 后退一个块的位置
position -= block_size
if position < 0:
position = 0
# 移动到计算的位置
f.seek(position)
# 读取这个块
block = f.read(block_size)
blocks.append(block)
# 如果已经收集了足够的数据,尝试解码并检查行数
combined_binary = b''.join(blocks)
# 尝试解码,使用errors='replace'处理无法解码的字符
try:
combined_text = combined_binary.decode('utf-8', errors='replace')
except:
combined_text = combined_binary.decode('latin-1')
lines = combined_text.splitlines(True) # 使用True保留换行符
# 确保我们不返回不完整的第一行
if len(lines) > 0:
# 如果有足够的行,确保我们从一个完整行开始
if len(lines) > 1:
# 跳过可能不完整的第一行
last_200_lines = lines[1:]
else:
last_200_lines = lines
# 如果我们有足够的行,停止读取
if len(last_200_lines) >= 200:
# 获取最后200行
last_200_lines = last_200_lines[-200:]
found_complete_lines = True
# 如果文件内容不足200行,或者上面的方法没有收集到足够的行
if len(last_200_lines) < 200:
# 重新读取整个文件(对于小文件)
content, encoding = read_log_file_robust(log_file)
all_lines = content.splitlines(True) # 使用True保留换行符
last_200_lines = all_lines[-200:] if len(all_lines) > 200 else all_lines
# 确保返回的内容顺序正确,并且不包含空行
return ''.join(last_200_lines) return ''.join(last_200_lines)
except Exception as e: except Exception as e:
return f'读取日志失败: {str(e)}' return f'读取日志失败: {str(e)}'
@ -507,7 +493,7 @@ def delete(process_id):
return jsonify({'success': True}) return jsonify({'success': True})
return jsonify({'success': False}) return jsonify({'success': False})
def find_available_port(start_port=5000, max_attempts=100): def find_available_port(start_port=12581, max_attempts=100):
"""查找可用的端口号 """查找可用的端口号
Args: Args:
@ -623,8 +609,8 @@ if __name__ == '__main__':
with open(PID_FILE, 'w') as f: with open(PID_FILE, 'w') as f:
f.write(str(os.getpid())) f.write(str(os.getpid()))
# 尝试使用默认端口5000,如果被占用则自动寻找可用端口 # 尝试使用默认端口12581,如果被占用则自动寻找可用端口
port = find_available_port(5000) port = find_available_port(12581)
if port is not None: if port is not None:
print(f"启动Flask服务器在 http://0.0.0.0:{port}") print(f"启动Flask服务器在 http://0.0.0.0:{port}")
print(f"使用nohup启动可保持服务持续运行: nohup python -u scripts/train_web_ui.py &") print(f"使用nohup启动可保持服务持续运行: nohup python -u scripts/train_web_ui.py &")