Update embedded app debugging workflow
This commit is contained in:
1 parent
d50be29246
commit
38807e5f97
13 files changed
+846
-122
No files matched your search
+70
-14
@@ -53,6 +53,9 @@
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
tab-size: 4;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
cursor: text;
|
||||
font: 13px/1.42 Consolas, "Cascadia Mono", "Microsoft YaHei UI", monospace;
|
||||
}
|
||||
.console {
|
||||
@@ -176,6 +179,8 @@
|
||||
let sendBusy = false;
|
||||
let promptMode = 'unknown';
|
||||
let pageClosing = false;
|
||||
let livenessConnection = null;
|
||||
let selectingLog = false;
|
||||
|
||||
commandPromptView.textContent = `${configuredPort} >`;
|
||||
|
||||
@@ -184,6 +189,29 @@
|
||||
return `${path}${separator}client=${encodeURIComponent(clientId)}`;
|
||||
}
|
||||
|
||||
function connectLiveness() {
|
||||
if (pageClosing || livenessConnection || typeof EventSource !== 'function') return;
|
||||
// 页面在后台时 setInterval 可能被节流;独立的原生持续连接才是页面存活依据。
|
||||
// 它只维护当前页面的租约,不打开串口、不发送命令,也不会重启已经退出的服务。
|
||||
livenessConnection = new EventSource(`${clientUrl('/api/client/live')}&token=${encodeURIComponent(token)}`);
|
||||
livenessConnection.onerror = () => {
|
||||
if (!pageClosing) refreshStatus();
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchLocal(path) {
|
||||
const controller = new AbortController();
|
||||
// 请求失败后必须放开 polling 标志;否则一个悬挂请求会让页面永久停止更新。
|
||||
const timeout = setTimeout(() => controller.abort(), 2500);
|
||||
try {
|
||||
const response = await fetch(path, { cache: 'no-store', signal: controller.signal });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
return await response.arrayBuffer();
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
// 仅清理终端显示控制字符,不修改设备输出中的普通调试内容。
|
||||
function removeTerminalControls(text) {
|
||||
text = text.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, '');
|
||||
@@ -250,42 +278,57 @@
|
||||
updateInputControls();
|
||||
}
|
||||
|
||||
function isLogSelectionActive() {
|
||||
// 拖选开始时选区可能仍为空,也不能替换文本节点;否则拖选会被轮询打断。
|
||||
if (selectingLog) return true;
|
||||
const selection = window.getSelection();
|
||||
if (!selection || selection.isCollapsed) return false;
|
||||
for (let index = 0; index < selection.rangeCount; index++) {
|
||||
if (selection.getRangeAt(index).intersectsNode(logView)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function refreshLog() {
|
||||
if (logPolling) return;
|
||||
if (pageClosing || logPolling) return;
|
||||
logPolling = true;
|
||||
const nearBottom = logView.scrollHeight - logView.scrollTop - logView.clientHeight < 100;
|
||||
const shouldFollowLatest = followView.checked && (!hasRenderedLog || nearBottom);
|
||||
try {
|
||||
const response = await fetch(`${clientUrl('/api/log')}&t=${Date.now()}`, { cache: 'no-store' });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const bytes = new Uint8Array(await response.arrayBuffer());
|
||||
const bytes = new Uint8Array(await fetchLocal(`${clientUrl('/api/log')}&t=${Date.now()}`));
|
||||
const fullText = removeTerminalControls(decoder.decode(bytes));
|
||||
updateSessionState(fullText);
|
||||
const visibleText = fullText.length > 500000
|
||||
? `[前面的日志已在页面中省略,完整数据仍保存在文件中]\n${fullText.slice(-500000)}`
|
||||
: fullText;
|
||||
logView.textContent = visibleText || `日志文件当前为空,正在等待 ${configuredPort} 数据…`;
|
||||
// 只暂停页面绘制,不停串口采集、文件落盘或页面存活连接。
|
||||
// 必须在请求完成后检查选区,防止等待响应期间用户刚开始选择文字。
|
||||
if (isLogSelectionActive()) return;
|
||||
const nextText = visibleText || `日志文件当前为空,正在等待 ${configuredPort} 数据…`;
|
||||
if (logView.textContent === nextText) return;
|
||||
const nearBottom = logView.scrollHeight - logView.scrollTop - logView.clientHeight < 100;
|
||||
const shouldFollowLatest = followView.checked && (!hasRenderedLog || nearBottom);
|
||||
logView.textContent = nextText;
|
||||
// 首次打开必须显示最新日志;等待浏览器完成布局后再滚动到底部,避免大日志仍停在开头。
|
||||
if (shouldFollowLatest) {
|
||||
requestAnimationFrame(() => {
|
||||
logView.scrollTop = logView.scrollHeight;
|
||||
if (!isLogSelectionActive()) logView.scrollTop = logView.scrollHeight;
|
||||
});
|
||||
}
|
||||
hasRenderedLog = true;
|
||||
} catch (error) {
|
||||
logView.textContent = `${logView.textContent}\n[网页] 日志读取失败:${error.message}`;
|
||||
if (!isLogSelectionActive()) {
|
||||
logView.textContent = `${logView.textContent}\n[网页] 日志读取失败:${error.message}`;
|
||||
}
|
||||
} finally {
|
||||
logPolling = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshStatus() {
|
||||
if (statusPolling) return;
|
||||
if (pageClosing || statusPolling) return;
|
||||
statusPolling = true;
|
||||
try {
|
||||
const response = await fetch(`${clientUrl('/api/status')}&t=${Date.now()}`, { cache: 'no-store' });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const status = await response.json();
|
||||
const bytes = await fetchLocal(`${clientUrl('/api/status')}&t=${Date.now()}`);
|
||||
const status = JSON.parse(decoder.decode(bytes));
|
||||
applyConnectionState(Boolean(status.isOpen));
|
||||
connectionView.textContent = status.isOpen ? `● ${status.port} 已连接` : `● ${status.port} 已断开`;
|
||||
connectionView.style.color = status.isOpen ? 'var(--ok)' : 'var(--danger)';
|
||||
@@ -343,6 +386,10 @@
|
||||
function disconnectClient() {
|
||||
if (pageClosing) return;
|
||||
pageClosing = true;
|
||||
if (livenessConnection) {
|
||||
livenessConnection.close();
|
||||
livenessConnection = null;
|
||||
}
|
||||
const disconnectUrl = `${clientUrl('/api/client/disconnect')}&token=${encodeURIComponent(token)}`;
|
||||
// 浏览器关闭标签页时普通异步请求可能被直接取消;sendBeacon 专门用于离页通知。
|
||||
// 某些内置浏览器未实现 sendBeacon,因此同时保留 keepalive fetch 作为兼容兜底。
|
||||
@@ -369,10 +416,16 @@
|
||||
});
|
||||
|
||||
sendButton.addEventListener('click', sendCommand);
|
||||
logView.addEventListener('pointerdown', (event) => {
|
||||
if (event.button === 0) selectingLog = true;
|
||||
});
|
||||
window.addEventListener('pointerup', () => { selectingLog = false; });
|
||||
window.addEventListener('pointercancel', () => { selectingLog = false; });
|
||||
window.addEventListener('blur', () => { selectingLog = false; });
|
||||
followView.addEventListener('change', () => {
|
||||
if (followView.checked) {
|
||||
requestAnimationFrame(() => {
|
||||
logView.scrollTop = logView.scrollHeight;
|
||||
if (!isLogSelectionActive()) logView.scrollTop = logView.scrollHeight;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -408,9 +461,12 @@
|
||||
window.addEventListener('beforeunload', disconnectClient);
|
||||
window.addEventListener('pageshow', () => {
|
||||
pageClosing = false;
|
||||
connectLiveness();
|
||||
refreshLog();
|
||||
refreshStatus();
|
||||
});
|
||||
|
||||
connectLiveness();
|
||||
refreshLog();
|
||||
refreshStatus();
|
||||
setInterval(refreshLog, 800);
|
||||
|
||||
Reference in new issue
Block a user