632 lines
26 KiB
C#
632 lines
26 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
using System.Globalization;
|
||
using System.IO;
|
||
using System.IO.Ports;
|
||
using System.Net;
|
||
using System.Text;
|
||
using System.Threading;
|
||
|
||
namespace EmbeddedAppPcDebug
|
||
{
|
||
/// <summary>
|
||
/// 在单一进程内独占串口,同时提供只允许本机访问的日志与命令接口。
|
||
/// </summary>
|
||
public sealed class SerialWebConsoleHost : IDisposable
|
||
{
|
||
private const int WebClientTimeoutSeconds = 3;
|
||
private const int LastClientGraceMilliseconds = 2000;
|
||
private readonly SerialPort _serialPort;
|
||
private readonly Encoding _encoding;
|
||
private readonly Decoder _decoder;
|
||
private readonly string _logPath;
|
||
private readonly string _rawPath;
|
||
private readonly string _htmlTemplate;
|
||
private readonly string _listenPrefix;
|
||
private readonly string _sessionToken;
|
||
private readonly int _readBufferSize;
|
||
private readonly object _writeLock = new object();
|
||
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 HttpListener _listener;
|
||
private FileStream _rawStream;
|
||
private StreamWriter _textWriter;
|
||
private Thread _receiveThread;
|
||
private Thread _httpThread;
|
||
private Thread _clientMonitorThread;
|
||
private volatile bool _stopping;
|
||
private volatile bool _started;
|
||
private bool _hadWebClient;
|
||
private DateTime _noClientSinceUtc = DateTime.MinValue;
|
||
private string _lastError = string.Empty;
|
||
private string _pendingLine = string.Empty;
|
||
private long _receivedBytes;
|
||
private DateTime _lastReceiveUtc = DateTime.MinValue;
|
||
|
||
public SerialWebConsoleHost(
|
||
string portName,
|
||
int baudRate,
|
||
int dataBits,
|
||
string parity,
|
||
string stopBits,
|
||
string handshake,
|
||
string encodingName,
|
||
int readTimeoutMilliseconds,
|
||
int readBufferSize,
|
||
bool dtrEnable,
|
||
bool rtsEnable,
|
||
string logPath,
|
||
string rawPath,
|
||
int webPort,
|
||
string htmlPath)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(portName)) throw new ArgumentException("串口名称不能为空。", "portName");
|
||
if (string.IsNullOrWhiteSpace(logPath)) throw new ArgumentException("文本日志路径不能为空。", "logPath");
|
||
if (string.IsNullOrWhiteSpace(rawPath)) throw new ArgumentException("原始日志路径不能为空。", "rawPath");
|
||
if (string.IsNullOrWhiteSpace(htmlPath) || !File.Exists(htmlPath)) throw new FileNotFoundException("找不到串口控制台页面。", htmlPath);
|
||
|
||
_encoding = Encoding.GetEncoding(encodingName);
|
||
_decoder = _encoding.GetDecoder();
|
||
_logPath = Path.GetFullPath(logPath);
|
||
_rawPath = Path.GetFullPath(rawPath);
|
||
_htmlTemplate = File.ReadAllText(htmlPath, new UTF8Encoding(false));
|
||
_listenPrefix = "http://127.0.0.1:" + webPort.ToString(CultureInfo.InvariantCulture) + "/";
|
||
_sessionToken = Guid.NewGuid().ToString("N");
|
||
_readBufferSize = readBufferSize;
|
||
|
||
_serialPort = new SerialPort();
|
||
_serialPort.PortName = portName;
|
||
_serialPort.BaudRate = baudRate;
|
||
_serialPort.DataBits = dataBits;
|
||
_serialPort.Parity = (Parity)Enum.Parse(typeof(Parity), parity, true);
|
||
_serialPort.StopBits = (StopBits)Enum.Parse(typeof(StopBits), stopBits, true);
|
||
_serialPort.Handshake = (Handshake)Enum.Parse(typeof(Handshake), handshake, true);
|
||
_serialPort.DtrEnable = dtrEnable;
|
||
_serialPort.RtsEnable = rtsEnable;
|
||
_serialPort.ReadTimeout = readTimeoutMilliseconds;
|
||
_serialPort.ReadBufferSize = Math.Max(4096, readBufferSize);
|
||
}
|
||
|
||
public bool IsStopping { get { return _stopping; } }
|
||
public string Url { get { return _listenPrefix; } }
|
||
|
||
public void Start()
|
||
{
|
||
if (_started) throw new InvalidOperationException("串口网页服务已经启动。");
|
||
|
||
string logDirectory = Path.GetDirectoryName(_logPath);
|
||
string rawDirectory = Path.GetDirectoryName(_rawPath);
|
||
if (!string.IsNullOrEmpty(logDirectory)) Directory.CreateDirectory(logDirectory);
|
||
if (!string.IsNullOrEmpty(rawDirectory)) Directory.CreateDirectory(rawDirectory);
|
||
|
||
try
|
||
{
|
||
FileStream textStream = new FileStream(_logPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
|
||
_textWriter = new StreamWriter(textStream, new UTF8Encoding(false));
|
||
_textWriter.AutoFlush = true;
|
||
_rawStream = new FileStream(_rawPath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite);
|
||
|
||
_serialPort.Open();
|
||
WriteSystemLog(string.Format(
|
||
CultureInfo.InvariantCulture,
|
||
"已打开 {0},{1} {2}{3}{4},流控 {5};网页控制台:{6};原始数据:{7}",
|
||
_serialPort.PortName,
|
||
_serialPort.BaudRate,
|
||
_serialPort.DataBits,
|
||
_serialPort.Parity.ToString().Substring(0, 1),
|
||
_serialPort.StopBits == StopBits.One ? "1" : (_serialPort.StopBits == StopBits.Two ? "2" : "1.5"),
|
||
_serialPort.Handshake,
|
||
_listenPrefix,
|
||
_rawPath));
|
||
|
||
_listener = new HttpListener();
|
||
_listener.Prefixes.Add(_listenPrefix);
|
||
_listener.Start();
|
||
|
||
_stopping = false;
|
||
_started = true;
|
||
_receiveThread = StartBackgroundThread(ReceiveLoop, "SerialWebConsole-RX");
|
||
_httpThread = StartBackgroundThread(HttpLoop, "SerialWebConsole-HTTP");
|
||
_clientMonitorThread = StartBackgroundThread(WebClientMonitorLoop, "SerialWebConsole-WebClient");
|
||
}
|
||
catch
|
||
{
|
||
Stop();
|
||
throw;
|
||
}
|
||
}
|
||
|
||
private static Thread StartBackgroundThread(ThreadStart action, string name)
|
||
{
|
||
Thread thread = new Thread(action);
|
||
thread.IsBackground = true;
|
||
thread.Name = name;
|
||
thread.Start();
|
||
return thread;
|
||
}
|
||
|
||
private void ReceiveLoop()
|
||
{
|
||
byte[] byteBuffer = new byte[_readBufferSize];
|
||
char[] charBuffer = new char[_encoding.GetMaxCharCount(_readBufferSize)];
|
||
|
||
while (!_stopping)
|
||
{
|
||
try
|
||
{
|
||
int count = _serialPort.Read(byteBuffer, 0, byteBuffer.Length);
|
||
if (count <= 0) continue;
|
||
|
||
lock (_logLock)
|
||
{
|
||
_rawStream.Write(byteBuffer, 0, count);
|
||
_rawStream.Flush();
|
||
}
|
||
|
||
int charCount = _decoder.GetChars(byteBuffer, 0, count, charBuffer, 0, false);
|
||
if (charCount > 0)
|
||
{
|
||
string text = new string(charBuffer, 0, charCount);
|
||
Console.Write(text);
|
||
WriteReceivedText(text);
|
||
}
|
||
|
||
Interlocked.Add(ref _receivedBytes, count);
|
||
lock (_stateLock)
|
||
{
|
||
_lastReceiveUtc = DateTime.UtcNow;
|
||
}
|
||
}
|
||
catch (TimeoutException)
|
||
{
|
||
// 读取超时用于让线程定期检查退出状态,不属于串口错误。
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
if (!_stopping)
|
||
{
|
||
SetFatalError("串口读取失败:" + ex.Message);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private void WriteReceivedText(string text)
|
||
{
|
||
lock (_logLock)
|
||
{
|
||
_pendingLine += text;
|
||
while (true)
|
||
{
|
||
int newlineIndex = _pendingLine.IndexOf('\n');
|
||
if (newlineIndex < 0) break;
|
||
string line = _pendingLine.Substring(0, newlineIndex).TrimEnd('\r');
|
||
_pendingLine = _pendingLine.Substring(newlineIndex + 1);
|
||
_textWriter.WriteLine("[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) + "] " + line);
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
private void WriteCommand(string text, string ending)
|
||
{
|
||
string terminator;
|
||
switch ((ending ?? string.Empty).ToUpperInvariant())
|
||
{
|
||
case "NONE": terminator = string.Empty; break;
|
||
case "CR": terminator = "\r"; break;
|
||
case "LF": terminator = "\n"; break;
|
||
case "CRLF": terminator = "\r\n"; break;
|
||
default: throw new ArgumentException("换行方式必须是 None、CR、LF 或 CRLF。", "ending");
|
||
}
|
||
string payload = (text ?? string.Empty) + terminator;
|
||
WriteBytes(_encoding.GetBytes(payload));
|
||
WriteTransmitLog(payload, "命令");
|
||
}
|
||
|
||
private void WriteTransmitLog(string text, string source)
|
||
{
|
||
// 将不可见的行尾转成明确标记,空命令回车会显示为 <CR>。
|
||
StringBuilder visible = new StringBuilder();
|
||
foreach (char c in text ?? string.Empty)
|
||
{
|
||
switch (c)
|
||
{
|
||
case '\r': visible.Append("<CR>"); break;
|
||
case '\n': visible.Append("<LF>"); break;
|
||
case '\t': visible.Append("<TAB>"); break;
|
||
default:
|
||
if (c < 32) visible.Append("<0x" + ((int)c).ToString("X2", CultureInfo.InvariantCulture) + ">");
|
||
else visible.Append(c);
|
||
break;
|
||
}
|
||
}
|
||
if (visible.Length == 0) visible.Append("<EMPTY>");
|
||
|
||
string line = "[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) + "] [TX:" + source + "] " + visible;
|
||
Console.WriteLine(line);
|
||
lock (_logLock)
|
||
{
|
||
if (_textWriter != null) _textWriter.WriteLine(line);
|
||
}
|
||
}
|
||
|
||
private void WriteBytes(byte[] bytes)
|
||
{
|
||
if (bytes == null || bytes.Length == 0) return;
|
||
lock (_writeLock)
|
||
{
|
||
if (_stopping || !_serialPort.IsOpen) throw new IOException("串口当前未打开。");
|
||
_serialPort.BaseStream.Write(bytes, 0, bytes.Length);
|
||
_serialPort.BaseStream.Flush();
|
||
}
|
||
}
|
||
|
||
private void TouchWebClient(string clientId)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(clientId) || clientId.Length > 128) return;
|
||
lock (_stateLock)
|
||
{
|
||
_webClients[clientId] = DateTime.UtcNow;
|
||
_hadWebClient = true;
|
||
_noClientSinceUtc = DateTime.MinValue;
|
||
}
|
||
}
|
||
|
||
private void RemoveWebClient(string clientId)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(clientId)) return;
|
||
lock (_stateLock)
|
||
{
|
||
_webClients.Remove(clientId);
|
||
if (_hadWebClient && _webClients.Count == 0 && _noClientSinceUtc == DateTime.MinValue)
|
||
_noClientSinceUtc = DateTime.UtcNow;
|
||
}
|
||
}
|
||
|
||
private void WebClientMonitorLoop()
|
||
{
|
||
while (!_stopping)
|
||
{
|
||
bool shouldStop = false;
|
||
lock (_stateLock)
|
||
{
|
||
DateTime now = DateTime.UtcNow;
|
||
List<string> expiredClients = new List<string>();
|
||
foreach (KeyValuePair<string, DateTime> item in _webClients)
|
||
{
|
||
if ((now - item.Value).TotalSeconds >= WebClientTimeoutSeconds)
|
||
expiredClients.Add(item.Key);
|
||
}
|
||
foreach (string clientId in expiredClients) _webClients.Remove(clientId);
|
||
|
||
if (_webClients.Count > 0)
|
||
{
|
||
_noClientSinceUtc = DateTime.MinValue;
|
||
}
|
||
else if (_hadWebClient)
|
||
{
|
||
if (_noClientSinceUtc == DateTime.MinValue) _noClientSinceUtc = now;
|
||
shouldStop = (now - _noClientSinceUtc).TotalMilliseconds >= LastClientGraceMilliseconds;
|
||
}
|
||
}
|
||
|
||
if (shouldStop)
|
||
{
|
||
WriteSystemLog("最后一个串口网页已关闭,正在停止日志采集并释放串口。");
|
||
_stopping = true;
|
||
try { if (_listener != null) _listener.Stop(); } catch { }
|
||
break;
|
||
}
|
||
Thread.Sleep(250);
|
||
}
|
||
}
|
||
|
||
private void HttpLoop()
|
||
{
|
||
while (!_stopping)
|
||
{
|
||
try
|
||
{
|
||
HttpListenerContext context = _listener.GetContext();
|
||
ThreadPool.QueueUserWorkItem(delegate { HandleHttpRequest(context); });
|
||
}
|
||
catch (HttpListenerException)
|
||
{
|
||
if (!_stopping) SetFatalError("本地网页服务异常停止。");
|
||
}
|
||
catch (ObjectDisposedException)
|
||
{
|
||
// 服务关闭时 GetContext 会退出。
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
if (!_stopping) SetFatalError("本地网页服务失败:" + ex.Message);
|
||
}
|
||
}
|
||
}
|
||
|
||
private void HandleHttpRequest(HttpListenerContext context)
|
||
{
|
||
try
|
||
{
|
||
if (context.Request.RemoteEndPoint == null || !IPAddress.IsLoopback(context.Request.RemoteEndPoint.Address))
|
||
{
|
||
SendTextResponse(context, 403, "text/plain; charset=utf-8", "只允许本机访问。");
|
||
return;
|
||
}
|
||
|
||
string path = context.Request.Url == null ? "/" : context.Request.Url.AbsolutePath;
|
||
string method = context.Request.HttpMethod.ToUpperInvariant();
|
||
|
||
if (method == "GET" && (path == "/" || path == "/index.html"))
|
||
{
|
||
string html = _htmlTemplate
|
||
.Replace("__SESSION_TOKEN__", _sessionToken)
|
||
.Replace("__SERIAL_PORT__", WebUtility.HtmlEncode(_serialPort.PortName));
|
||
SendTextResponse(context, 200, "text/html; charset=utf-8", html);
|
||
return;
|
||
}
|
||
|
||
if (method == "GET" && path == "/api/status")
|
||
{
|
||
TouchWebClient(context.Request.QueryString["client"]);
|
||
SendTextResponse(context, 200, "application/json; charset=utf-8", BuildStatusJson());
|
||
return;
|
||
}
|
||
|
||
if (method == "GET" && path == "/api/log")
|
||
{
|
||
TouchWebClient(context.Request.QueryString["client"]);
|
||
SendLogTail(context, 2 * 1024 * 1024);
|
||
return;
|
||
}
|
||
|
||
if (method == "GET" && path == "/favicon.ico")
|
||
{
|
||
context.Response.StatusCode = 204;
|
||
context.Response.Close();
|
||
return;
|
||
}
|
||
|
||
if (method == "POST")
|
||
{
|
||
if (path == "/api/send")
|
||
{
|
||
ValidateCommandRequest(context);
|
||
string ending = context.Request.QueryString["ending"] ?? "CR";
|
||
string command = ReadRequestBody(context.Request, 16384);
|
||
WriteCommand(command, ending);
|
||
SendTextResponse(context, 200, "application/json; charset=utf-8", "{\"status\":\"written\"}");
|
||
return;
|
||
}
|
||
if (path == "/api/client/disconnect")
|
||
{
|
||
ValidateDisconnectRequest(context);
|
||
RemoveWebClient(context.Request.QueryString["client"]);
|
||
SendTextResponse(context, 200, "application/json; charset=utf-8", "{\"status\":\"disconnected\"}");
|
||
return;
|
||
}
|
||
}
|
||
|
||
SendTextResponse(context, 404, "text/plain; charset=utf-8", "未找到接口。");
|
||
}
|
||
catch (ArgumentException ex)
|
||
{
|
||
SendTextResponse(context, 400, "application/json; charset=utf-8", "{\"error\":\"" + JsonEscape(ex.Message) + "\"}");
|
||
}
|
||
catch (UnauthorizedAccessException ex)
|
||
{
|
||
SendTextResponse(context, 403, "application/json; charset=utf-8", "{\"error\":\"" + JsonEscape(ex.Message) + "\"}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
lock (_stateLock) _lastError = ex.Message;
|
||
SendTextResponse(context, 503, "application/json; charset=utf-8", "{\"error\":\"" + JsonEscape(ex.Message) + "\"}");
|
||
}
|
||
}
|
||
|
||
private void ValidateCommandRequest(HttpListenerContext context)
|
||
{
|
||
string token = context.Request.Headers["X-Serial-Token"];
|
||
if (!string.Equals(token, _sessionToken, StringComparison.Ordinal))
|
||
throw new UnauthorizedAccessException("本地会话令牌无效。");
|
||
|
||
string origin = context.Request.Headers["Origin"];
|
||
if (!string.IsNullOrWhiteSpace(origin) &&
|
||
!string.Equals(origin.TrimEnd('/') + "/", _listenPrefix, StringComparison.OrdinalIgnoreCase))
|
||
throw new UnauthorizedAccessException("请求来源不是当前串口控制台。");
|
||
}
|
||
|
||
private void ValidateDisconnectRequest(HttpListenerContext context)
|
||
{
|
||
// sendBeacon 无法自定义请求头,因此仅离页通知接口允许在查询参数中携带会话令牌。
|
||
string token = context.Request.Headers["X-Serial-Token"];
|
||
if (string.IsNullOrWhiteSpace(token)) token = context.Request.QueryString["token"];
|
||
if (!string.Equals(token, _sessionToken, StringComparison.Ordinal))
|
||
throw new UnauthorizedAccessException("本地会话令牌无效。");
|
||
|
||
string origin = context.Request.Headers["Origin"];
|
||
if (!string.IsNullOrWhiteSpace(origin) &&
|
||
!string.Equals(origin.TrimEnd('/') + "/", _listenPrefix, StringComparison.OrdinalIgnoreCase))
|
||
throw new UnauthorizedAccessException("请求来源不是当前串口控制台。");
|
||
}
|
||
|
||
private static string ReadRequestBody(HttpListenerRequest request, int maxCharacters)
|
||
{
|
||
Encoding bodyEncoding = request.ContentEncoding ?? Encoding.UTF8;
|
||
using (StreamReader reader = new StreamReader(request.InputStream, bodyEncoding, true, 1024, false))
|
||
{
|
||
char[] buffer = new char[maxCharacters + 1];
|
||
int total = 0;
|
||
while (total < buffer.Length)
|
||
{
|
||
int count = reader.Read(buffer, total, buffer.Length - total);
|
||
if (count <= 0) break;
|
||
total += count;
|
||
}
|
||
if (total > maxCharacters) throw new ArgumentException("单次命令不能超过 16384 个字符。");
|
||
return new string(buffer, 0, total);
|
||
}
|
||
}
|
||
|
||
private void SendLogTail(HttpListenerContext context, int maxBytes)
|
||
{
|
||
byte[] data;
|
||
using (FileStream stream = new FileStream(_logPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||
{
|
||
long start = Math.Max(0, stream.Length - maxBytes);
|
||
stream.Position = start;
|
||
data = new byte[stream.Length - start];
|
||
int offset = 0;
|
||
while (offset < data.Length)
|
||
{
|
||
int count = stream.Read(data, offset, data.Length - offset);
|
||
if (count <= 0) break;
|
||
offset += count;
|
||
}
|
||
if (offset != data.Length) Array.Resize(ref data, offset);
|
||
}
|
||
|
||
context.Response.StatusCode = 200;
|
||
context.Response.ContentType = "text/plain; charset=utf-8";
|
||
context.Response.Headers["Cache-Control"] = "no-store";
|
||
context.Response.ContentLength64 = data.Length;
|
||
context.Response.OutputStream.Write(data, 0, data.Length);
|
||
context.Response.Close();
|
||
}
|
||
|
||
private string BuildStatusJson()
|
||
{
|
||
string lastError;
|
||
DateTime lastReceive;
|
||
int activeWebClients;
|
||
lock (_stateLock)
|
||
{
|
||
lastError = _lastError;
|
||
lastReceive = _lastReceiveUtc;
|
||
activeWebClients = _webClients.Count;
|
||
}
|
||
|
||
return "{" +
|
||
"\"port\":\"" + JsonEscape(_serialPort.PortName) + "\"," +
|
||
"\"isOpen\":" + (_serialPort.IsOpen && !_stopping ? "true" : "false") + "," +
|
||
"\"receivedBytes\":" + Interlocked.Read(ref _receivedBytes).ToString(CultureInfo.InvariantCulture) + "," +
|
||
"\"lastReceiveUtc\":\"" + (lastReceive == DateTime.MinValue ? string.Empty : lastReceive.ToString("o", CultureInfo.InvariantCulture)) + "\"," +
|
||
"\"activeWebClients\":" + activeWebClients.ToString(CultureInfo.InvariantCulture) + "," +
|
||
"\"lastError\":\"" + JsonEscape(lastError) + "\"" +
|
||
"}";
|
||
}
|
||
|
||
private static string JsonEscape(string value)
|
||
{
|
||
if (string.IsNullOrEmpty(value)) return string.Empty;
|
||
StringBuilder builder = new StringBuilder(value.Length + 16);
|
||
foreach (char c in value)
|
||
{
|
||
switch (c)
|
||
{
|
||
case '\\': builder.Append("\\\\"); break;
|
||
case '"': builder.Append("\\\""); break;
|
||
case '\r': builder.Append("\\r"); break;
|
||
case '\n': builder.Append("\\n"); break;
|
||
case '\t': builder.Append("\\t"); break;
|
||
default:
|
||
if (c < 32) builder.Append("\\u" + ((int)c).ToString("x4", CultureInfo.InvariantCulture));
|
||
else builder.Append(c);
|
||
break;
|
||
}
|
||
}
|
||
return builder.ToString();
|
||
}
|
||
|
||
private static void SendTextResponse(HttpListenerContext context, int statusCode, string contentType, string text)
|
||
{
|
||
try
|
||
{
|
||
byte[] data = new UTF8Encoding(false).GetBytes(text ?? string.Empty);
|
||
context.Response.StatusCode = statusCode;
|
||
context.Response.ContentType = contentType;
|
||
context.Response.Headers["Cache-Control"] = "no-store";
|
||
context.Response.ContentLength64 = data.Length;
|
||
context.Response.OutputStream.Write(data, 0, data.Length);
|
||
context.Response.Close();
|
||
}
|
||
catch
|
||
{
|
||
try { context.Response.Abort(); } catch { }
|
||
}
|
||
}
|
||
|
||
private void SetFatalError(string message)
|
||
{
|
||
lock (_stateLock) _lastError = message;
|
||
WriteSystemLog(message);
|
||
_stopping = true;
|
||
try { if (_listener != null) _listener.Stop(); } catch { }
|
||
}
|
||
|
||
private void WriteSystemLog(string message)
|
||
{
|
||
string line = "[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) + "] " + message;
|
||
Console.WriteLine(line);
|
||
lock (_logLock)
|
||
{
|
||
if (_textWriter != null) _textWriter.WriteLine(line);
|
||
}
|
||
}
|
||
|
||
public void Stop()
|
||
{
|
||
_stopping = true;
|
||
try { if (_listener != null) _listener.Stop(); } catch { }
|
||
try { if (_serialPort.IsOpen) _serialPort.Close(); } catch { }
|
||
|
||
JoinThread(_httpThread);
|
||
JoinThread(_clientMonitorThread);
|
||
JoinThread(_receiveThread);
|
||
|
||
lock (_logLock)
|
||
{
|
||
if (_textWriter != null && !string.IsNullOrEmpty(_pendingLine))
|
||
{
|
||
_textWriter.WriteLine("[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) + "] " + _pendingLine.TrimEnd('\r'));
|
||
_pendingLine = string.Empty;
|
||
}
|
||
if (_textWriter != null)
|
||
{
|
||
_textWriter.WriteLine("[" + DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture) + "] 串口网页服务结束");
|
||
_textWriter.Dispose();
|
||
_textWriter = null;
|
||
}
|
||
if (_rawStream != null)
|
||
{
|
||
_rawStream.Dispose();
|
||
_rawStream = null;
|
||
}
|
||
}
|
||
|
||
if (_listener != null)
|
||
{
|
||
try { _listener.Close(); } catch { }
|
||
_listener = null;
|
||
}
|
||
_serialPort.Dispose();
|
||
_started = false;
|
||
}
|
||
|
||
private static void JoinThread(Thread thread)
|
||
{
|
||
if (thread == null || thread == Thread.CurrentThread) return;
|
||
try { thread.Join(1500); } catch { }
|
||
}
|
||
|
||
public void Dispose()
|
||
{
|
||
Stop();
|
||
}
|
||
}
|
||
}
|