diff --git a/clients/client-linux.py b/clients/client-linux.py index b79e10b..d96e7b3 100755 --- a/clients/client-linux.py +++ b/clients/client-linux.py @@ -133,6 +133,54 @@ def get_cpu(): result = 100-(t[len(t)-1]*100.00/st) return round(result, 1) +def get_cpu_cores(): + try: + with open('/proc/stat') as f: + cores = sum(1 for line in f if re.match(r'^cpu\d+\s', line)) + if cores > 0: + return cores + except Exception: + pass + return os.cpu_count() or 0 + +def normalize_cpu_model(value): + return re.sub(r'\s+', ' ', str(value or '')).strip()[:160] + +def is_generic_cpu_model(value): + v = normalize_cpu_model(value).lower().replace('-', '').replace('_', '').replace(' ', '') + return v in ('', 'unknown', 'x8664', 'amd64', 'i386', 'i686', 'aarch64', 'arm64') or v.startswith('armv') + +def get_cpu_model(): + try: + fallback = "" + with open('/proc/cpuinfo') as f: + for line in f: + if ':' not in line: + continue + key, value = line.split(':', 1) + key = key.strip().lower() + value = normalize_cpu_model(value) + if not value: + continue + if key == 'model name': + return value + if key in ('hardware', 'processor') and not value.isdigit() and not fallback: + fallback = value + if fallback: + return fallback + except Exception: + pass + for cmd in ('sysctl -n machdep.cpu.brand_string', 'sysctl -n hw.model'): + try: + value = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, timeout=2).decode().strip() + value = normalize_cpu_model(value) + if value: + return value + except Exception: + pass + value = normalize_cpu_model(platform.processor()) + return "" if is_generic_cpu_model(value) else value + def liuliang(): NET_IN = 0 NET_OUT = 0 @@ -505,6 +553,8 @@ if __name__ == '__main__': print(data) raise socket.error + CPUCores = get_cpu_cores() + CPUModel = get_cpu_model() while True: CPU = get_cpu() NET_IN, NET_OUT = liuliang() @@ -530,6 +580,8 @@ if __name__ == '__main__': array['hdd_total'] = HDDTotal array['hdd_used'] = HDDUsed array['cpu'] = CPU + array['cpu_cores'] = CPUCores + array['cpu_model'] = CPUModel array['network_rx'] = netSpeed.get("netrx") array['network_tx'] = netSpeed.get("nettx") array['network_in'] = NET_IN diff --git a/clients/client-psutil.py b/clients/client-psutil.py index 0e4b31f..3ae856e 100755 --- a/clients/client-psutil.py +++ b/clients/client-psutil.py @@ -28,6 +28,7 @@ import sys import json import errno import psutil +import subprocess import threading import platform from queue import Queue @@ -92,6 +93,47 @@ def get_hdd(): def get_cpu(): return psutil.cpu_percent(interval=INTERVAL) +def get_cpu_cores(): + return psutil.cpu_count(logical=True) or 0 + +def normalize_cpu_model(value): + return " ".join(str(value or "").split())[:160] + +def is_generic_cpu_model(value): + v = normalize_cpu_model(value).lower().replace('-', '').replace('_', '').replace(' ', '') + return v in ('', 'unknown', 'x8664', 'amd64', 'i386', 'i686', 'aarch64', 'arm64') or v.startswith('armv') + +def get_cpu_model(): + try: + fallback = "" + with open('/proc/cpuinfo') as f: + for line in f: + if ':' not in line: + continue + key, value = line.split(':', 1) + key = key.strip().lower() + value = normalize_cpu_model(value) + if not value: + continue + if key == 'model name': + return value + if key in ('hardware', 'processor') and not value.isdigit() and not fallback: + fallback = value + if fallback: + return fallback + except Exception: + pass + for cmd in ('sysctl -n machdep.cpu.brand_string', 'sysctl -n hw.model'): + try: + value = subprocess.check_output(cmd, shell=True, stderr=subprocess.DEVNULL, timeout=2).decode().strip() + value = normalize_cpu_model(value) + if value: + return value + except Exception: + pass + value = normalize_cpu_model(platform.processor()) + return "" if is_generic_cpu_model(value) else value + def liuliang(): NET_IN = 0 NET_OUT = 0 @@ -472,6 +514,8 @@ if __name__ == '__main__': print(data) raise socket.error + CPUCores = get_cpu_cores() + CPUModel = get_cpu_model() while 1: CPU = get_cpu() NET_IN, NET_OUT = liuliang() @@ -498,6 +542,8 @@ if __name__ == '__main__': array['hdd_total'] = HDDTotal array['hdd_used'] = HDDUsed array['cpu'] = CPU + array['cpu_cores'] = CPUCores + array['cpu_model'] = CPUModel array['network_rx'] = netSpeed.get("netrx") array['network_tx'] = netSpeed.get("nettx") array['network_in'] = NET_IN diff --git a/server/src/main.cpp b/server/src/main.cpp index 0e26205..7dd1c97 100644 --- a/server/src/main.cpp +++ b/server/src/main.cpp @@ -17,6 +17,48 @@ static volatile int gs_Running = 1; static volatile int gs_ReloadConfig = 0; +static void JsonEscape(const char *pSrc, char *pDst, int DstSize) +{ + if(!pDst || DstSize <= 0) + return; + int Out = 0; + if(!pSrc) + { + pDst[0] = 0; + return; + } + for(const unsigned char *p = (const unsigned char *)pSrc; *p && Out < DstSize - 1; ++p) + { + const char *pEsc = 0; + switch(*p) + { + case '"': pEsc = "\\\""; break; + case '\\': pEsc = "\\\\"; break; + case '\b': pEsc = "\\b"; break; + case '\f': pEsc = "\\f"; break; + case '\n': pEsc = "\\n"; break; + case '\r': pEsc = "\\r"; break; + case '\t': pEsc = "\\t"; break; + default: break; + } + if(pEsc) + { + for(const char *q = pEsc; *q && Out < DstSize - 1; ++q) + pDst[Out++] = *q; + } + else if(*p < 0x20) + { + if(Out < DstSize - 6) + Out += snprintf(pDst + Out, DstSize - Out, "\\u%04x", *p); + else + break; + } + else + pDst[Out++] = *p; + } + pDst[Out] = 0; +} + static int64_t ParseOpenSSLEnddate(const char *line) { // line format: notAfter=Aug 12 23:59:59 2025 GMT @@ -380,6 +422,10 @@ int CMain::HandleMessage(int ClientNetID, char *pMessage) pClient->m_Stats.m_IOWrite = rStart["io_write"].u.integer; if(rStart["cpu"].type) pClient->m_Stats.m_CPU = rStart["cpu"].u.dbl; + if(rStart["cpu_cores"].type) + pClient->m_Stats.m_CPUCores = rStart["cpu_cores"].u.integer; + if(rStart["cpu_model"].type == json_string) + str_copy(pClient->m_Stats.m_aCPUModel, rStart["cpu_model"].u.string.ptr, sizeof(pClient->m_Stats.m_aCPUModel)); if(rStart["online4"].type && pClient->m_ClientNetType == NETTYPE_IPV6) pClient->m_Stats.m_Online4 = rStart["online4"].u.boolean; if(rStart["online6"].type && pClient->m_ClientNetType == NETTYPE_IPV4) @@ -635,28 +681,38 @@ void CMain::JSONUpdateThread(void *pUser) pClients[i].m_LastNetworkOUT = pClients[i].m_Stats.m_NetworkOUT; } + char aCustomEsc[2048] = { 0 }; + char aOSEsc[128] = { 0 }; + char aCPUModelEsc[384] = { 0 }; + JsonEscape(pClients[i].m_Stats.m_aCustom, aCustomEsc, sizeof(aCustomEsc)); + JsonEscape(pClients[i].m_Stats.m_aOS[0] ? pClients[i].m_Stats.m_aOS : "", aOSEsc, sizeof(aOSEsc)); + JsonEscape(pClients[i].m_Stats.m_aCPUModel[0] ? pClients[i].m_Stats.m_aCPUModel : "", aCPUModelEsc, sizeof(aCPUModelEsc)); str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), - "{ \"name\": \"%s\",\"type\": \"%s\",\"host\": \"%s\",\"location\": \"%s\",\"online4\": %s, \"online6\": %s, \"uptime\": \"%s\",\"load_1\": %.2f, \"load_5\": %.2f, \"load_15\": %.2f,\"ping_10010\": %.2f, \"ping_189\": %.2f, \"ping_10086\": %.2f,\"time_10010\": %" PRId64 ", \"time_189\": %" PRId64 ", \"time_10086\": %" PRId64 ", \"tcp_count\": %" PRId64 ", \"udp_count\": %" PRId64 ", \"process_count\": %" PRId64 ", \"thread_count\": %" PRId64 ", \"network_rx\": %" PRId64 ", \"network_tx\": %" PRId64 ", \"network_in\": %" PRId64 ", \"network_out\": %" PRId64 ", \"cpu\": %d, \"memory_total\": %" PRId64 ", \"memory_used\": %" PRId64 ", \"swap_total\": %" PRId64 ", \"swap_used\": %" PRId64 ", \"hdd_total\": %" PRId64 ", \"hdd_used\": %" PRId64 ", \"last_network_in\": %" PRId64 ", \"last_network_out\": %" PRId64 ",\"io_read\": %" PRId64 ", \"io_write\": %" PRId64 ",\"custom\": \"%s\", \"os\": \"%s\" },\n", + "{ \"name\": \"%s\",\"type\": \"%s\",\"host\": \"%s\",\"location\": \"%s\",\"online4\": %s, \"online6\": %s, \"uptime\": \"%s\",\"load_1\": %.2f, \"load_5\": %.2f, \"load_15\": %.2f,\"ping_10010\": %.2f, \"ping_189\": %.2f, \"ping_10086\": %.2f,\"time_10010\": %" PRId64 ", \"time_189\": %" PRId64 ", \"time_10086\": %" PRId64 ", \"tcp_count\": %" PRId64 ", \"udp_count\": %" PRId64 ", \"process_count\": %" PRId64 ", \"thread_count\": %" PRId64 ", \"network_rx\": %" PRId64 ", \"network_tx\": %" PRId64 ", \"network_in\": %" PRId64 ", \"network_out\": %" PRId64 ", \"cpu\": %d, \"cpu_cores\": %" PRId64 ", \"cpu_model\": \"%s\", \"memory_total\": %" PRId64 ", \"memory_used\": %" PRId64 ", \"swap_total\": %" PRId64 ", \"swap_used\": %" PRId64 ", \"hdd_total\": %" PRId64 ", \"hdd_used\": %" PRId64 ", \"last_network_in\": %" PRId64 ", \"last_network_out\": %" PRId64 ",\"io_read\": %" PRId64 ", \"io_write\": %" PRId64 ",\"custom\": \"%s\", \"os\": \"%s\" },\n", pClients[i].m_aName,pClients[i].m_aType,pClients[i].m_aHost,pClients[i].m_aLocation, pClients[i].m_Stats.m_Online4 ? "true" : "false",pClients[i].m_Stats.m_Online6 ? "true" : "false", aUptime, pClients[i].m_Stats.m_Load_1, pClients[i].m_Stats.m_Load_5, pClients[i].m_Stats.m_Load_15, pClients[i].m_Stats.m_ping_10010, pClients[i].m_Stats.m_ping_189, pClients[i].m_Stats.m_ping_10086, pClients[i].m_Stats.m_time_10010, pClients[i].m_Stats.m_time_189, pClients[i].m_Stats.m_time_10086,pClients[i].m_Stats.m_tcpCount,pClients[i].m_Stats.m_udpCount,pClients[i].m_Stats.m_processCount,pClients[i].m_Stats.m_threadCount, - pClients[i].m_Stats.m_NetworkRx, pClients[i].m_Stats.m_NetworkTx, pClients[i].m_Stats.m_NetworkIN, pClients[i].m_Stats.m_NetworkOUT, (int)pClients[i].m_Stats.m_CPU, pClients[i].m_Stats.m_MemTotal, pClients[i].m_Stats.m_MemUsed, + pClients[i].m_Stats.m_NetworkRx, pClients[i].m_Stats.m_NetworkTx, pClients[i].m_Stats.m_NetworkIN, pClients[i].m_Stats.m_NetworkOUT, (int)pClients[i].m_Stats.m_CPU, pClients[i].m_Stats.m_CPUCores, aCPUModelEsc, pClients[i].m_Stats.m_MemTotal, pClients[i].m_Stats.m_MemUsed, pClients[i].m_Stats.m_SwapTotal, pClients[i].m_Stats.m_SwapUsed, pClients[i].m_Stats.m_HDDTotal, pClients[i].m_Stats.m_HDDUsed, pClients[i].m_Stats.m_NetworkIN == 0 || pClients[i].m_LastNetworkIN == 0 ? pClients[i].m_Stats.m_NetworkIN : pClients[i].m_LastNetworkIN, pClients[i].m_Stats.m_NetworkOUT == 0 || pClients[i].m_LastNetworkOUT == 0 ? pClients[i].m_Stats.m_NetworkOUT : pClients[i].m_LastNetworkOUT, pClients[i].m_Stats.m_IORead, pClients[i].m_Stats.m_IOWrite, - pClients[i].m_Stats.m_aCustom, - pClients[i].m_Stats.m_aOS[0] ? pClients[i].m_Stats.m_aOS : ""); + aCustomEsc, + aOSEsc); pBuf += strlen(pBuf); } else { // sava network traffic record to json when close client // last_network_in == last network in record, last_network_out == last network out record - str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), "{ \"name\": \"%s\", \"type\": \"%s\", \"host\": \"%s\", \"location\": \"%s\", \"online4\": false, \"online6\": false, \"last_network_in\": %" PRId64 ", \"last_network_out\": %" PRId64 ", \"os\": \"%s\" },\n", + char aOSEsc[128] = { 0 }; + char aCPUModelEsc[384] = { 0 }; + JsonEscape(pClients[i].m_Stats.m_aOS[0] ? pClients[i].m_Stats.m_aOS : "", aOSEsc, sizeof(aOSEsc)); + JsonEscape(pClients[i].m_Stats.m_aCPUModel[0] ? pClients[i].m_Stats.m_aCPUModel : "", aCPUModelEsc, sizeof(aCPUModelEsc)); + str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), "{ \"name\": \"%s\", \"type\": \"%s\", \"host\": \"%s\", \"location\": \"%s\", \"online4\": false, \"online6\": false, \"last_network_in\": %" PRId64 ", \"last_network_out\": %" PRId64 ", \"os\": \"%s\", \"cpu_model\": \"%s\" },\n", pClients[i].m_aName, pClients[i].m_aType, pClients[i].m_aHost, pClients[i].m_aLocation, pClients[i].m_LastNetworkIN, pClients[i].m_LastNetworkOUT, - pClients[i].m_Stats.m_aOS[0] ? pClients[i].m_Stats.m_aOS : ""); + aOSEsc, aCPUModelEsc); pBuf += strlen(pBuf); } } @@ -1118,4 +1174,3 @@ int main(int argc, const char *argv[]) return RetVal; } - diff --git a/server/src/main.h b/server/src/main.h index 9882857..9e0d7be 100644 --- a/server/src/main.h +++ b/server/src/main.h @@ -76,8 +76,10 @@ class CMain int64_t m_threadCount; int64_t m_IORead; int64_t m_IOWrite; + int64_t m_CPUCores; double m_CPU; char m_aCustom[1024]; + char m_aCPUModel[192]; // OS name reported by client (e.g. linux/windows/darwin/freebsd) char m_aOS[64]; // Options diff --git a/web/css/app.css b/web/css/app.css index 9f42b66..8539998 100644 --- a/web/css/app.css +++ b/web/css/app.css @@ -151,6 +151,10 @@ body.light .virt-pill{background:color-mix(in srgb,var(--virt) 12%,#fff);color:c .node-name{display:inline-block;max-width:160px;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;font-weight:750;letter-spacing:.15px;color:var(--text)} .row-server[data-online="0"] .node-name{color:color-mix(in srgb,var(--text) 72%,var(--text-dim));font-weight:650} .row-server:hover .node-name{color:var(--accent)} +.load-with-cores{display:inline-flex;align-items:flex-start;gap:.28rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:700;line-height:1;white-space:nowrap} +.load-value{display:inline-block;min-width:2.55rem} +.load-core-bubble{display:inline-flex;align-items:center;justify-content:center;height:14px;min-width:24px;padding:0 5px;margin-top:-6px;border-radius:999px;border:1px solid color-mix(in srgb,var(--accent) 34%,var(--border));background:color-mix(in srgb,var(--accent) 13%,var(--bg-alt));color:color-mix(in srgb,var(--accent) 74%,#fff);font-size:9px;font-weight:800;line-height:1;letter-spacing:0;box-shadow:0 1px 3px rgba(0,0,0,.22)} +body.light .load-core-bubble{background:color-mix(in srgb,var(--accent) 8%,#fff);color:color-mix(in srgb,var(--accent) 78%,#111827);box-shadow:0 1px 2px rgba(15,23,42,.08)} /* buckets CU/CT/CM (simple version) */ .buckets{display:flex;align-items:flex-end;gap:8px;min-width:140px} @@ -261,6 +265,10 @@ table.data tbody tr[class*="os-"]:hover{background:linear-gradient(180deg, color /* 弹窗 OS 着色:左侧彩条 + 渐变与卡片一致 */ /* 取消弹窗背景着色,改为仅在标题展示系统胶囊 */ .os-chip{display:inline-flex;align-items:center;padding:2px 8px;margin-left:.5rem;border-radius:999px;font-size:12px;font-weight:600;line-height:1.2;background:var(--os-color, var(--border));color:#fff;border:0;white-space:nowrap;} +.spec-chip{display:inline-flex;align-items:center;padding:2px 8px;margin-left:.4rem;border-radius:999px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;font-weight:750;line-height:1.2;background:color-mix(in srgb,var(--accent) 16%,var(--bg-alt));color:color-mix(in srgb,var(--accent) 72%,#fff);border:1px solid color-mix(in srgb,var(--accent) 38%,var(--border));white-space:nowrap;} +body.light .spec-chip{background:color-mix(in srgb,var(--accent) 10%,#fff);color:color-mix(in srgb,var(--accent) 78%,#111827)} +.cpu-model-chip{display:inline-block;max-width:min(240px,58vw);overflow:hidden;text-overflow:ellipsis;vertical-align:middle;padding:2px 8px;margin-left:.4rem;border-radius:999px;border:1px solid color-mix(in srgb,var(--text-dim) 34%,var(--border));background:color-mix(in srgb,var(--text-dim) 12%,var(--bg-alt));color:color-mix(in srgb,var(--text) 84%,var(--text-dim));font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;font-weight:700;line-height:1.2;white-space:nowrap;} +body.light .cpu-model-chip{background:color-mix(in srgb,var(--text-dim) 9%,#fff);color:color-mix(in srgb,var(--text) 82%,#111827)} /* 为常见系统赋色 */ .os-linux{--os-color: rgba(16,185,129,.85);} /* 绿色 (通用 Linux,保持不变) */ @@ -285,7 +293,9 @@ table.data tbody tr[class*="os-"]:hover{background:linear-gradient(180deg, color /* 旧进度条相关样式已清理 */ .cards .card-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;} -.cards .card-title{font-weight:600;font-size:.95rem;} +.cards .card-title{display:flex;align-items:center;flex-wrap:wrap;gap:.24rem .34rem;min-width:0;font-weight:600;font-size:.95rem;} +.cards .card-spec-chip{display:inline-flex;align-items:center;justify-content:center;height:16px;padding:0 6px;border-radius:999px;border:1px solid color-mix(in srgb,var(--accent) 34%,var(--border));background:color-mix(in srgb,var(--accent) 13%,var(--bg-alt));color:color-mix(in srgb,var(--accent) 74%,#fff);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;font-weight:800;line-height:1;letter-spacing:0;white-space:nowrap} +body.light .cards .card-spec-chip{background:color-mix(in srgb,var(--accent) 8%,#fff);color:color-mix(in srgb,var(--accent) 78%,#111827)} .cards .tag{font-size:.65rem;padding:.15rem .4rem;border-radius:4px;background:var(--surface);color:var(--text);letter-spacing:.5px;} .cards .status-pill{font-size:.6rem;padding:.2rem .45rem;border-radius:999px;font-weight:500;} .cards .status-pill.on{background:var(--ok);color:#fff;} @@ -372,7 +382,8 @@ th[data-sort].sorted-desc:after{border-top-color:var(--accent);opacity:1} .detail-drawer{height:100dvh;max-height:100dvh;width:min(620px,100vw);max-width:min(620px,100vw);border-radius:0;border-top:0;border-right:0;border-bottom:0;padding:1.1rem;overflow:auto;animation:slideIn .24s ease} .detail-drawer.alert-critical{border-color:rgba(239,68,68,.6);background:linear-gradient(180deg, rgba(239,68,68,.16), rgba(239,68,68,.08)), var(--bg-alt);box-shadow:0 0 0 1px rgba(239,68,68,.38),0 10px 28px -10px rgba(239,68,68,.28)} .detail-drawer.alert-warning{border-color:rgba(245,158,11,.55);background:linear-gradient(180deg, rgba(245,158,11,.14), rgba(245,158,11,.06)), var(--bg-alt);box-shadow:0 0 0 1px rgba(245,158,11,.28),0 10px 28px -10px rgba(245,158,11,.2)} -.modal-title{padding-right:2.2rem} +.modal-title{padding-right:2.2rem;display:flex;align-items:center;flex-wrap:wrap;gap:.32rem .4rem} +.modal-title .os-chip,.modal-title .spec-chip,.modal-title .cpu-model-chip{margin-left:0} .detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.55rem} .detail-section{border:1px solid var(--border);border-radius:8px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));padding:.75rem;display:flex;flex-direction:column;gap:.65rem} .detail-section h4{margin:0;font-size:13px;color:var(--text-dim);font-weight:700;letter-spacing:.4px} diff --git a/web/index.html b/web/index.html index 57be97a..37be1c2 100644 --- a/web/index.html +++ b/web/index.html @@ -6,7 +6,7 @@