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
@@ -29,6 +29,7 @@ namespace EmbeddedAppPcDebug
|
||||
private readonly object _logLock = new object();
|
||||
private readonly object _stateLock = new object();
|
||||
private readonly Dictionary<string, DateTime> _webClients = new Dictionary<string, DateTime>(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, HttpListenerResponse> _liveWebClients = new Dictionary<string, HttpListenerResponse>(StringComparer.Ordinal);
|
||||
|
||||
private HttpListener _listener;
|
||||
private FileStream _rawStream;
|
||||
@@ -122,6 +123,8 @@ namespace EmbeddedAppPcDebug
|
||||
_rawPath));
|
||||
|
||||
_listener = new HttpListener();
|
||||
// 必须收到页面连接断开的写异常,不能吞掉后把已关闭的页面永久计为在线。
|
||||
_listener.IgnoreWriteExceptions = false;
|
||||
_listener.Prefixes.Add(_listenPrefix);
|
||||
_listener.Start();
|
||||
|
||||
@@ -269,6 +272,7 @@ namespace EmbeddedAppPcDebug
|
||||
if (string.IsNullOrWhiteSpace(clientId) || clientId.Length > 128) return;
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_stopping) return;
|
||||
_webClients[clientId] = DateTime.UtcNow;
|
||||
_hadWebClient = true;
|
||||
_noClientSinceUtc = DateTime.MinValue;
|
||||
@@ -276,14 +280,76 @@ namespace EmbeddedAppPcDebug
|
||||
}
|
||||
|
||||
private void RemoveWebClient(string clientId)
|
||||
{
|
||||
RemoveWebClient(clientId, null, "网页主动离页");
|
||||
}
|
||||
|
||||
private void RemoveWebClient(string clientId, HttpListenerResponse expectedResponse, string reason)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(clientId)) return;
|
||||
HttpListenerResponse liveResponse;
|
||||
bool removed;
|
||||
lock (_stateLock)
|
||||
{
|
||||
_webClients.Remove(clientId);
|
||||
_liveWebClients.TryGetValue(clientId, out liveResponse);
|
||||
// EventSource 重连可能先建立新连接,旧连接的 finally 不能删除新连接。
|
||||
if (expectedResponse != null && !object.ReferenceEquals(liveResponse, expectedResponse)) return;
|
||||
_liveWebClients.Remove(clientId);
|
||||
removed = _webClients.Remove(clientId);
|
||||
if (_hadWebClient && _webClients.Count == 0 && _noClientSinceUtc == DateTime.MinValue)
|
||||
_noClientSinceUtc = DateTime.UtcNow;
|
||||
}
|
||||
if (liveResponse != null) { try { liveResponse.Abort(); } catch { } }
|
||||
if (removed && !_stopping) WriteSystemLog(reason + ";客户端 " + clientId);
|
||||
}
|
||||
|
||||
private void SendClientLiveness(HttpListenerContext context)
|
||||
{
|
||||
string clientId = context.Request.QueryString["client"];
|
||||
if (string.IsNullOrWhiteSpace(clientId) || clientId.Length > 128)
|
||||
throw new ArgumentException("页面客户端标识无效。");
|
||||
HttpListenerResponse response = context.Response;
|
||||
HttpListenerResponse previousResponse;
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_stopping) throw new IOException("串口网页服务正在结束。");
|
||||
_liveWebClients.TryGetValue(clientId, out previousResponse);
|
||||
_liveWebClients[clientId] = response;
|
||||
_webClients[clientId] = DateTime.UtcNow;
|
||||
_hadWebClient = true;
|
||||
_noClientSinceUtc = DateTime.MinValue;
|
||||
}
|
||||
if (previousResponse != null) { try { previousResponse.Abort(); } catch { } }
|
||||
|
||||
try
|
||||
{
|
||||
response.StatusCode = 200;
|
||||
response.ContentType = "text/event-stream; charset=utf-8";
|
||||
response.Headers["Cache-Control"] = "no-store";
|
||||
response.SendChunked = true;
|
||||
byte[] heartbeat = Encoding.UTF8.GetBytes("retry: 500\n: page connection alive\n\n");
|
||||
// 原生 EventSource 持续连接不依赖页面 setInterval。即使页面定时器被节流,
|
||||
// 仍由服务端每秒探测一次连接;页面关闭/崩溃时,写失败后释放该页面租约。
|
||||
while (!_stopping)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
HttpListenerResponse current;
|
||||
if (!_liveWebClients.TryGetValue(clientId, out current) || !object.ReferenceEquals(current, response)) break;
|
||||
}
|
||||
response.OutputStream.Write(heartbeat, 0, heartbeat.Length);
|
||||
response.OutputStream.Flush();
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
catch (HttpListenerException) { /* 断开的网页连接是正常生命周期事件,不是串口故障。 */ }
|
||||
catch (IOException) { }
|
||||
catch (ObjectDisposedException) { }
|
||||
finally
|
||||
{
|
||||
RemoveWebClient(clientId, response, "网页持续连接已断开(未收到主动离页通知)");
|
||||
try { response.Abort(); } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
private void WebClientMonitorLoop()
|
||||
@@ -297,10 +363,14 @@ namespace EmbeddedAppPcDebug
|
||||
List<string> expiredClients = new List<string>();
|
||||
foreach (KeyValuePair<string, DateTime> item in _webClients)
|
||||
{
|
||||
if ((now - item.Value).TotalSeconds >= WebClientTimeoutSeconds)
|
||||
if (!_liveWebClients.ContainsKey(item.Key) && (now - item.Value).TotalSeconds >= WebClientTimeoutSeconds)
|
||||
expiredClients.Add(item.Key);
|
||||
}
|
||||
foreach (string clientId in expiredClients) _webClients.Remove(clientId);
|
||||
foreach (string clientId in expiredClients)
|
||||
{
|
||||
_webClients.Remove(clientId);
|
||||
WriteSystemLog("网页轮询心跳超时(不能据此断定页面已关闭);客户端 " + clientId);
|
||||
}
|
||||
|
||||
if (_webClients.Count > 0)
|
||||
{
|
||||
@@ -310,13 +380,14 @@ namespace EmbeddedAppPcDebug
|
||||
{
|
||||
if (_noClientSinceUtc == DateTime.MinValue) _noClientSinceUtc = now;
|
||||
shouldStop = (now - _noClientSinceUtc).TotalMilliseconds >= LastClientGraceMilliseconds;
|
||||
// 判定和置位必须在同一把锁内,防止刷新页面刚接管就被旧判定停止。
|
||||
if (shouldStop) _stopping = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldStop)
|
||||
{
|
||||
WriteSystemLog("最后一个串口网页已关闭,正在停止日志采集并释放串口。");
|
||||
_stopping = true;
|
||||
WriteSystemLog("所有串口网页连接已结束,宽限期内无页面接管;正在停止日志采集并释放串口。");
|
||||
try { if (_listener != null) _listener.Stop(); } catch { }
|
||||
break;
|
||||
}
|
||||
@@ -377,6 +448,13 @@ namespace EmbeddedAppPcDebug
|
||||
return;
|
||||
}
|
||||
|
||||
if (method == "GET" && path == "/api/client/live")
|
||||
{
|
||||
ValidateDisconnectRequest(context);
|
||||
SendClientLiveness(context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (method == "GET" && path == "/api/log")
|
||||
{
|
||||
TouchWebClient(context.Request.QueryString["client"]);
|
||||
@@ -442,7 +520,7 @@ namespace EmbeddedAppPcDebug
|
||||
|
||||
private void ValidateDisconnectRequest(HttpListenerContext context)
|
||||
{
|
||||
// sendBeacon 无法自定义请求头,因此仅离页通知接口允许在查询参数中携带会话令牌。
|
||||
// sendBeacon / EventSource 无法自定义请求头,仅页面生命周期接口允许查询令牌。
|
||||
string token = context.Request.Headers["X-Serial-Token"];
|
||||
if (string.IsNullOrWhiteSpace(token)) token = context.Request.QueryString["token"];
|
||||
if (!string.Equals(token, _sessionToken, StringComparison.Ordinal))
|
||||
@@ -503,11 +581,13 @@ namespace EmbeddedAppPcDebug
|
||||
string lastError;
|
||||
DateTime lastReceive;
|
||||
int activeWebClients;
|
||||
int liveWebClients;
|
||||
lock (_stateLock)
|
||||
{
|
||||
lastError = _lastError;
|
||||
lastReceive = _lastReceiveUtc;
|
||||
activeWebClients = _webClients.Count;
|
||||
liveWebClients = _liveWebClients.Count;
|
||||
}
|
||||
|
||||
return "{" +
|
||||
@@ -516,6 +596,7 @@ namespace EmbeddedAppPcDebug
|
||||
"\"receivedBytes\":" + Interlocked.Read(ref _receivedBytes).ToString(CultureInfo.InvariantCulture) + "," +
|
||||
"\"lastReceiveUtc\":\"" + (lastReceive == DateTime.MinValue ? string.Empty : lastReceive.ToString("o", CultureInfo.InvariantCulture)) + "\"," +
|
||||
"\"activeWebClients\":" + activeWebClients.ToString(CultureInfo.InvariantCulture) + "," +
|
||||
"\"liveWebClients\":" + liveWebClients.ToString(CultureInfo.InvariantCulture) + "," +
|
||||
"\"lastError\":\"" + JsonEscape(lastError) + "\"" +
|
||||
"}";
|
||||
}
|
||||
@@ -581,6 +662,14 @@ namespace EmbeddedAppPcDebug
|
||||
public void Stop()
|
||||
{
|
||||
_stopping = true;
|
||||
List<HttpListenerResponse> liveResponses;
|
||||
lock (_stateLock)
|
||||
{
|
||||
liveResponses = new List<HttpListenerResponse>(_liveWebClients.Values);
|
||||
_liveWebClients.Clear();
|
||||
_webClients.Clear();
|
||||
}
|
||||
foreach (HttpListenerResponse response in liveResponses) { try { response.Abort(); } catch { } }
|
||||
try { if (_listener != null) _listener.Stop(); } catch { }
|
||||
try { if (_serialPort.IsOpen) _serialPort.Close(); } catch { }
|
||||
|
||||
|
||||
Reference in new issue
Block a user