forked from KNEMC/KLALB
✨ feat: JavaFX 初实现
This commit is contained in:
@@ -14,3 +14,6 @@ dashboard/.env.*.local
|
||||
|
||||
/klalb-config.json
|
||||
/klalbconfig-old.json
|
||||
|
||||
# Local agent workflow state
|
||||
.slim/deepwork/
|
||||
|
||||
Vendored
+14
@@ -22,6 +22,20 @@
|
||||
"${workspaceFolder}/lib/*"
|
||||
],
|
||||
"vmArgs": "--enable-native-access=ALL-UNNAMED --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED"
|
||||
},
|
||||
{
|
||||
"type": "java",
|
||||
"name": "KLALB JavaFX Preview (bin)",
|
||||
"request": "launch",
|
||||
"mainClass": "org.kne.cloud.network.klalb.KLALBMain",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"classPaths": [
|
||||
"${workspaceFolder}/bin",
|
||||
"${workspaceFolder}/src",
|
||||
"${workspaceFolder}/lib/*"
|
||||
],
|
||||
"args": "--javafx",
|
||||
"vmArgs": "--enable-native-access=ALL-UNNAMED,javafx.graphics --add-opens=java.base/jdk.internal.misc=ALL-UNNAMED --add-modules=javafx.controls,javafx.graphics -Djava.library.path=${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,101 +1,42 @@
|
||||
# AGENTS.md
|
||||
# KLALB Agent Guide
|
||||
|
||||
KLALB ("KLALB Decentralized SRv6 Network") — Java load-balancing/tunnel system that merges multiple WAN links into one virtual IPv6/SRv6 network. Version constant lives in `src/org/kne/cloud/network/klalb/CONST.java`. Protocol specs and manuals are the Chinese `.docx` files in the repo root.
|
||||
KLALB is a Java SRv6/WAN load-balancing system. `KLALBMain` loads `klalb-config.json` from the current directory, builds `KLALBProxySystem`, optionally opens a desktop UI, optionally starts the web UI, and always binds the virtual server to `[::]:4564`.
|
||||
|
||||
## Build & run
|
||||
## Java Build And Run
|
||||
|
||||
No Maven/Gradle. Plain Eclipse/IntelliJ project: dependencies are vendored jars in `lib/`, output goes to `bin/` (gitignored). When adding a jar, update **both** `.classpath` and `KLALB.iml`.
|
||||
|
||||
Compile (`javac` is NOT on PATH — use the full JDK path; `-encoding UTF-8` is mandatory because sources contain Chinese text):
|
||||
- This is a plain Java project: vendored dependencies are in `lib/`, `bin/` is generated, and sources/resources are in `src/`. There is no Maven, Gradle, test suite, or CI.
|
||||
- VSCode is configured for `C:\Program Files\Zulu\zulu-25`. `javac` is not on `PATH`; source files require UTF-8 and now reference JavaFX modules.
|
||||
|
||||
```powershell
|
||||
& "C:\Program Files\Zulu\zulu-25\bin\javac.exe" -encoding UTF-8 -cp "lib/*" -d bin (Get-ChildItem -Recurse src -Filter *.java | ForEach-Object FullName)
|
||||
& "C:\Program Files\Zulu\zulu-25\bin\javac.exe" -encoding UTF-8 --add-modules=javafx.controls,javafx.graphics -cp "lib/*" -d bin (Get-ChildItem -Recurse src -Filter *.java | ForEach-Object FullName)
|
||||
```
|
||||
|
||||
Warnings about `ThreadTool` varargs / deprecated `finalize` are pre-existing and expected — success = exit code 0. After recompiling, restart the running app (IDE-debugged JVMs keep old classes).
|
||||
- The documented `ThreadTool` varargs and deprecated-API warnings are pre-existing; compilation succeeds with exit code 0. Restart a debugged JVM after compiling because it keeps old classes.
|
||||
- Run from the repository root. `src` must stay on the runtime classpath after manual compilation because it contains `klalb_*.properties` and `/assets/*` resources.
|
||||
- Use `KLALB JavaFX Preview (bin)` in VSCode for the opt-in `--javafx` UI. Default startup remains Swing. The preview configuration supplies required JavaFX modules, native-access flags, and `java.library.path`.
|
||||
- Native files are expected in the repository root. `fastcopy.dll` is currently absent, so `FastLib` logs a recoverable `UnsatisfiedLinkError`; restore the matching binary before testing native acceleration. Set `enableTUN: false` for non-admin UI testing; otherwise TUN creation requires elevation.
|
||||
|
||||
Run from the repo root — CWD matters:
|
||||
- reads `klalb-config.json` from CWD
|
||||
- loads native libs from CWD: `tuntap4j.dll/.so/.dylib`, `wintun.dll`, `fastcopy.dll` (TUN device support)
|
||||
- classpath must include `src` as well as `bin`: i18n bundles (`/klalb_*.properties`) and images (`/assets/*`) are classpath resources that Eclipse copies to `bin` but manual `javac` does not
|
||||
## Architecture And UI
|
||||
|
||||
```powershell
|
||||
java --enable-native-access=ALL-UNNAMED "--add-opens=java.base/jdk.internal.misc=ALL-UNNAMED" -cp "bin;src;lib/*" org.kne.cloud.network.klalb.KLALBMain
|
||||
```
|
||||
- Keep `KLALBController` and networking work off UI threads. The current JavaFX layer is `src/org/kne/cloud/network/klalb/ui/fx/`, starts only with `--javafx`, and exposes immutable snapshots/typed commands through `KLALBJavaFXGateway`; do not leak controller or link objects into JavaFX views.
|
||||
- Swing remains in `...klalb.ui`. Do not assume feature parity: Overview and Connections are migrated; routing, topology, settings, and detailed monitor views remain Swing/placeholder workspaces.
|
||||
- UI strings use the active `UIEnv` bundle. Add every new key to both `src/klalb_zh_CN.properties` and `src/klalb_en_US.properties`; keep them on the runtime classpath.
|
||||
|
||||
IDE metadata targets JDK 26 (`jdk-26.0.1`); the tree also compiles cleanly on JDK 25. `.classpath` now references the standard container `JavaSE-25` — an execution-environment spec that any JDK ≥25 satisfies, so it works unchanged on JDK 26 machines too (the original named `jdk-26.0.1` VM broke VS Code import on machines without it). `.vscode/settings.json` maps `JavaSE-25` to the locally installed Adoptium JDK; register every installed JDK there when adding another one. Keep compiler compliance ≤25 (`.settings` pins 19) so both JDKs stay usable.
|
||||
## Config, APIs, And Protocols
|
||||
|
||||
Runtime gotchas (all verified):
|
||||
- On JDK 25, `KNEOptimize.jar`'s `FastLib` reflects into `jdk.internal.misc.Unsafe`; without the two JVM flags above it throws `InaccessibleObjectException` at startup (app still runs).
|
||||
- Creating the SRv6 TUN adapter (`WintunCreateAdapter`) requires an elevated shell; without admin rights it logs "创建虚拟网卡失败" and continues with only the `inLoopBack` interface — links/bridges still work.
|
||||
- To disable TUN creation completely (e.g. for non-admin UI/routing testing), set `"enableTUN": false` in `klalb-config.json` or toggle off "启用 TUN 虚拟网卡" in GUI/Web settings.
|
||||
- Routing broadcast (`RouterInfo`) transmits `deviceName`, which topology and node overview panels display. `deviceDescription` is NOT broadcast — it only leaves the node in full node-info query responses (see srv6 API below); `ExtraRoutes` remain local controller configs.
|
||||
- `klalb-config.json` is a `Type`-discriminated list. New item types need a `KLALBConfigItem` subclass plus both default Gson serializer/deserializer registrations; unknown items must remain preservable.
|
||||
- Config saves use canonical keys but reads and `/api/config` accept legacy aliases. Keep aliases in `KLALBConfigItem` and field-by-field `KLALBWebServer.handleConfig` parsing when changing controller settings; whole-object Gson mapping breaks polymorphic address lists.
|
||||
- Gson is vendored version 2.1. For a runtime `JsonElement`, use `JsonElement.toString()` rather than `gson.toJson(Object)`; old Gson serializes subclasses incorrectly. Keep the existing guard in `sendJsonResponse`.
|
||||
- SRv6 JSON datagrams have a two-byte UTF-8 payload length; keep encoded messages below 64 KiB.
|
||||
- The web dashboard is served from `dashboard/dist` when `webUI` is enabled (normally `:4665`); without it, the backend serves fallback HTML.
|
||||
|
||||
## Dashboard Submodule
|
||||
|
||||
- `dashboard/` is a separate Git repository. Commit frontend work there first, then update the parent submodule pointer.
|
||||
- Run frontend commands inside `dashboard/`: `pnpm dev`, `pnpm build`, `pnpm lint`, and `pnpm typecheck`. Vite proxies `/api` to `http://127.0.0.1:4665`.
|
||||
- Install shadcn components only with `pnpm dlx shadcn@latest add <component>`; use `pnpm add` for other dependencies. The dashboard README's `npx` instruction is stale.
|
||||
|
||||
## Verification
|
||||
|
||||
No test suite, no CI. Classes named `*Test*` (`nathole/`, `ntp/`) are manual `main()` harnesses requiring real network peers. Practical check = compile succeeds + app launches.
|
||||
|
||||
## Architecture
|
||||
|
||||
- Entrypoint `org.kne.cloud.network.klalb.KLALBMain`: load config → build `KLALBProxySystem` → open Swing GUI (`KLALBStateGUI3`) unless `"nogui": true` → start `KLALBWebServer` (if `"webUI": true` or web server enabled, default port `4665`) → interactive console (`help`, `links-state`, `route`, `kperf`, ...).
|
||||
- `org.kne.cloud.network.klalb.web.KLALBWebServer` — built-in HTTP/SSE server (JDK `HttpServer`):
|
||||
- API endpoints: `/api/status`, `/api/events` (SSE stream, 200ms intervals), `/api/links`, `/api/links/action`, `/api/links/reconnect`, `/api/routing-table`, `/api/nodes` (topology graph), `/api/node-info?address=<ipv6>` (on-demand full node info), `/api/config`.
|
||||
- Static file hosting / SPA fallback: serves `dashboard/dist/` assets directly.
|
||||
- `org.kne.cloud.network` — generic socket framework: `VirtualSocket*` hierarchy, `SocketBridge` port-forwarding proxies, `ProtocolDetector` (multi-protocol mux on one port), `MultiProtocolSocketAddress` = URI-style addresses (`tcp://`, `udp://`, `kltp://`, `ntp://`) dispatched through the `SocketType` registry.
|
||||
- `...network.klalb` — app core: `KLALBController` (the virtual SRv6 network), `KLALBRemoteLink` (WAN lines), `*Packet` wire-format classes, virtual socket implementations.
|
||||
- `...network.congestion` — pluggable congestion control (BBR, Vegas2, DCTCP...), chosen via `"congestionAlgorithm"` in config.
|
||||
- `...network.kltp` — custom reliable transport protocol (packets/streams).
|
||||
- `...network.ipv6`, `...network.srv6` — packet codecs, route table, Dijkstra path computation.
|
||||
- **Node-info query API** (`...network.srv6`, JSON datagrams on `KLALBRoutingProtocol.DEFAULT_PORT=1001`): `KLALBRoutingProtocolAPIServer/Client` speak two request types —
|
||||
- `nodeinfotinyreq/resp` → device name ONLY; never gated by any flag (name is public via broadcast anyway).
|
||||
- `nodeinfofullreq/resp` → externalEndpoints + deviceName + deviceDescription. `denyExternalEndpointQuery=true` hides ONLY the endpoint list (`data=null`); name/description still answer.
|
||||
- GUI rule: opening `NodeInformationPanel` = Full query; use `requestNodeInfoTiny` for lightweight/background lookups. Legacy `openlines*` message types were removed — mixed-version meshes get silence, so upgrade the whole network together.
|
||||
- `JsonDataPacket` stores its UTF-8 payload length in a 2-byte header field: keep every JSON message under 64 KiB.
|
||||
- `...network.frpc` — frp client integration.
|
||||
- `...klalb.ui` — all Swing UI code.
|
||||
|
||||
## Frontend (Dashboard)
|
||||
|
||||
Located in `dashboard/`:
|
||||
- **Git layout**: `dashboard/` is a separate git repo wired in as a submodule (own origin on `git.code.cq.cn`). Commit frontend changes inside `dashboard/` first, then bump the submodule pointer in the parent repo — parent-repo commits alone do not capture them.
|
||||
- **Stack**: Vite + React 19 + TypeScript + Tailwind CSS v4 + `@base-ui/react` (style: `base-nova`, icons: `lucide-react`, toasts: `@base-ui/react/toast`).
|
||||
- **Routing**: Hash-based routing (`#/overview`, `#/connections`, `#/routing-table`, `#/topology`, `#/settings`) for seamless SPA hosting under Java `KLALBWebServer`.
|
||||
- **Package Manager**: `pnpm` (run all commands from `dashboard/` directory).
|
||||
- **Component installation**: **Must** use CLI via `pnpm dlx shadcn@latest add <component>` (e.g. `pnpm dlx shadcn@latest add alert card badge toast`). Never create or fake shadcn components manually. Non-shadcn libs (`@xyflow/react`, `d3-force`) are installed via plain `pnpm add`.
|
||||
- **Commands**:
|
||||
- `pnpm dev` — Start Vite dev server (proxies `/api` to backend `http://127.0.0.1:4665`).
|
||||
- `pnpm build` — Typecheck and build SPA to `dashboard/dist` (which Java `KLALBWebServer` serves directly).
|
||||
- `pnpm lint` / `pnpm typecheck` — Verification.
|
||||
- **Pages & data flow**:
|
||||
- Overview / Connections read the SSE stream (`use-klalb-sse.ts`, 200ms pushes of status + links).
|
||||
- Settings loads/saves `/api/config` (`use-klalb-config.ts`); save payload must keep legacy field aliases alongside new names for compatibility.
|
||||
- Routing table polls `/api/routing-table` every 1s (`use-routing-table.ts`); `cost` is delay-derived and displayed in milliseconds.
|
||||
- Topology polls `/api/nodes` every 1s (`use-topology.ts`) — SSE does NOT carry topology.
|
||||
- Selecting a topology node queries `/api/node-info`; remote node descriptions require a full SRv6 node-info request and can time out after 3s.
|
||||
- Topology layout: `d3-force` headless simulation (recomputed only when node/edge structure changes) rendered by `@xyflow/react` with custom `device-node` / `link-edge` components in `src/components/topology/`.
|
||||
- React hooks lint rule forbids `setState` synchronously inside effects — initialize form state via component `key` remount + lazy `useState(() => ...)` initializers (see `SettingsForm` pattern).
|
||||
|
||||
## Config
|
||||
|
||||
`klalb-config.json` is an array of items discriminated by their `"Type"` field. Adding a new item type requires a `KLALBConfigItem` subclass **plus** new cases in both `KLALBConfigItem.getDefaultJsonDeserializer()` and `getDefaultJsonSerializer()`; unknown types are preserved as `UnknownKLALBConfigItem`. Any Gson instance handling config must register these adapters via `registerToGsonBuilder` (see `KLALBProxySystem`).
|
||||
|
||||
Key controller config fields:
|
||||
- `externalEndpoints` / `autoConnections`: published vs auto-connect endpoint lists (renamed from `openConnections`, which itself replaced legacy `LineTable`; the old name was a developer naming mistake — these addresses are this node's externally published endpoints, not "connections").
|
||||
- `ntpServers`: time server list (replaces `ntpServerTable`).
|
||||
- `denyExternalEndpointQuery` / `denyExternalEndpointBroadcast`: safety flags — the query flag hides ONLY the external-endpoint list in full node-info responses (device name/description still answer; Tiny queries are never gated), the broadcast flag disables LAN multicast discovery (renamed from `denyConnectionQuery` / `denyConnectionBroadcast`, which replaced `denyLineTableQuery` / `denyLineTableBroadcast`).
|
||||
- `enableTUN`: boolean flag for TUN interface creation (`"TUNName"` configures device name).
|
||||
- `webListen`: Web API listen address, normally `http://0.0.0.0:4665`; legacy `webPort` is accepted on load/API input.
|
||||
|
||||
Legacy JSON keys are still accepted on load: `KLALBConfigItem.getDefaultJsonDeserializer()` normalizes old key names (`openConnections`/`LineTable`, `denyConnectionQuery`, `denyLineTable*`, ...) before reflective deserialization (manual rewrite because gson-2.1 has no `@SerializedName(alternate=...)`), and `handleConfig` in the web server accepts them too. New saves always write canonical names.
|
||||
|
||||
Gson quirks:
|
||||
- **gson-2.1 (vendored) is ancient**: its `JSON_ELEMENT` adapter factory only matches exact `JsonElement.class`, NOT subclasses. Calling `gson.toJson(Object)` with a runtime `JsonObject`/`JsonArray` reflectively serializes the internal field as `{"members": {...}}`. `KLALBWebServer.sendJsonResponse` guards against this by using `JsonElement.toString()` for JsonElement instances — keep that guard when adding new response paths. SSE avoids the issue entirely via `JsonObject.toString()`.
|
||||
- `/api/config` GET/POST is parsed field-by-field in `KLALBWebServer.handleConfig` (NOT whole-object Gson reflection) because polymorphic fields (`List<InetAddress>`, `List<MultiProtocolSocketAddress>`) break reflective mapping. Keep new config fields in sync there, accepting both legacy and new JSON key names.
|
||||
- `InetAddress`, `MultiProtocolSocketAddress`, and `KLALBConfigItem` custom adapters are registered on the shared Gson in `KLALBProxySystem`; the web server reuses that instance via `proxySystem.getGson()`.
|
||||
- Saving via web API persists through `KLALBProxySystem.saveConfigToFile()` (GUI save consumer takes precedence when present).
|
||||
|
||||
## Conventions
|
||||
|
||||
- Sources are UTF-8; comments, log/UI strings, and commit messages are largely Chinese.
|
||||
- UI strings go through `UIEnv.getRsb().getString(...)`; add keys to **both** `src/klalb_zh_CN.properties` and `src/klalb_en_US.properties`.
|
||||
- `client.cfg`, `server.cfg`, `linetable.txt` at the root are example line-table/port-rule files loaded via the GUI file picker — not hardwired paths.
|
||||
- Practical Java verification is the full compile command above plus a controlled launch. Existing `*Test*` classes are manual harnesses that need real network peers.
|
||||
- For dashboard changes, run `pnpm lint`, `pnpm typecheck`, and `pnpm build`.
|
||||
|
||||
@@ -117,3 +117,77 @@ enablewebapi=Enable Web API
|
||||
weblistenaddr=Web API listen address
|
||||
invaildweblistenaddr=Invalid Web API listen address
|
||||
enabletun=Enable TUN adapter
|
||||
nav.overview=Overview
|
||||
nav.connections=Connections
|
||||
nav.routing=Routing
|
||||
nav.topology=Topology
|
||||
nav.settings=Settings
|
||||
overview.title=Network overview
|
||||
overview.subtitle=Live local node and data-plane status
|
||||
overview.dataPlane=Data plane
|
||||
overview.localAddress=Local node address
|
||||
connections.title=Connections
|
||||
connections.subtitle=Remote link status and controls
|
||||
routing.title=Routing
|
||||
topology.title=Topology
|
||||
settings.title=Settings
|
||||
connections.endpoint=Remote endpoint
|
||||
connections.endpointPrompt=Enter endpoint address
|
||||
connections.add=Add endpoint
|
||||
connections.added=Endpoint added
|
||||
connections.reconnectAll=Reconnect all
|
||||
connections.reconnecting=Reconnecting
|
||||
connections.showOffline=Show offline
|
||||
connections.hideOffline=Hide offline
|
||||
connections.empty=No connections to display
|
||||
connections.reconnect=Reconnect
|
||||
connections.disconnect=Force disconnect
|
||||
connections.disconnected=Connection disconnected
|
||||
connections.remove=Remove
|
||||
connections.removed=Connection removed
|
||||
connections.confirmTitle=Confirm connection removal
|
||||
connections.confirmHeader=This removes connections matching the endpoint
|
||||
state.online=Online
|
||||
state.offline=Offline
|
||||
metric.links=Links online / total
|
||||
metric.totalThroughput=Total throughput
|
||||
metric.totalPps=Total PPS
|
||||
metric.upload=Upload
|
||||
metric.download=Download
|
||||
metric.pps=PPS
|
||||
metric.up=up
|
||||
metric.down=down
|
||||
format.bytesPerSecond=B/s
|
||||
format.kilobytesPerSecond=KB/s
|
||||
format.megabytesPerSecond=MB/s
|
||||
format.gigabytesPerSecond=GB/s
|
||||
format.milliseconds=ms
|
||||
copyHelp=Copy address to clipboard
|
||||
shell.productHint=SRv6 network console
|
||||
shell.workspace=Workspace
|
||||
shell.awaitingGateway=Awaiting gateway
|
||||
shell.noData=No runtime data connected
|
||||
shell.footer=Runtime status · Data supplied by gateway
|
||||
shell.connected=Gateway connected
|
||||
shell.links=links
|
||||
shell.error=Temporarily unavailable
|
||||
shell.refreshFailed=Refresh failed. Try again later.
|
||||
shell.pendingView=This view is scheduled for a later migration
|
||||
metric.devices=Online devices
|
||||
metric.totalTransferred=Transferred
|
||||
overview.backplane=Backplane
|
||||
overview.clock=Clock
|
||||
metric.delay=Delay
|
||||
metric.ppsMax=PPS peak
|
||||
metric.ecnLoss=ECN / loss
|
||||
metric.clockTime=Current time
|
||||
metric.frequency=Frequency
|
||||
metric.ppm=Frequency offset
|
||||
format.percent=%
|
||||
format.hz=Hz
|
||||
format.ppm=PPM
|
||||
state.unstable=Unstable
|
||||
state.accessible=Connection state
|
||||
connections.copyEndpoint=Copy endpoint address
|
||||
connections.copyVirtual=Copy virtual address
|
||||
connections.copyBind=Copy bind address
|
||||
|
||||
@@ -116,4 +116,78 @@ webapisettings=Web API 设置
|
||||
enablewebapi=启用 Web API
|
||||
weblistenaddr=Web API 监听地址:端口
|
||||
invaildweblistenaddr=无效的 Web API 监听地址:端口
|
||||
enabletun=启用TUN虚拟网卡
|
||||
enabletun=启用TUN虚拟网卡
|
||||
nav.overview=概览
|
||||
nav.connections=连接
|
||||
nav.routing=路由
|
||||
nav.topology=拓扑
|
||||
nav.settings=设置
|
||||
overview.title=网络概览
|
||||
overview.subtitle=本地节点与数据平面实时状态
|
||||
overview.dataPlane=数据平面
|
||||
overview.localAddress=本地节点地址
|
||||
connections.title=连接
|
||||
connections.subtitle=远端链路状态与控制
|
||||
routing.title=路由
|
||||
topology.title=拓扑
|
||||
settings.title=设置
|
||||
connections.endpoint=远端端点
|
||||
connections.endpointPrompt=输入端点地址
|
||||
connections.add=添加端点
|
||||
connections.added=端点已添加
|
||||
connections.reconnectAll=全部重连
|
||||
connections.reconnecting=正在重连
|
||||
connections.showOffline=显示离线
|
||||
connections.hideOffline=隐藏离线
|
||||
connections.empty=当前没有可显示的连接
|
||||
connections.reconnect=重连
|
||||
connections.disconnect=强制断开
|
||||
connections.disconnected=连接已断开
|
||||
connections.remove=移除
|
||||
connections.removed=连接已移除
|
||||
connections.confirmTitle=确认移除连接
|
||||
connections.confirmHeader=此操作将移除匹配端点的连接
|
||||
state.online=在线
|
||||
state.offline=离线
|
||||
metric.links=链路在线数 / 总数
|
||||
metric.totalThroughput=总吞吐
|
||||
metric.totalPps=总 PPS
|
||||
metric.upload=上传
|
||||
metric.download=下载
|
||||
metric.pps=PPS
|
||||
metric.up=上传
|
||||
metric.down=下载
|
||||
format.bytesPerSecond=B/s
|
||||
format.kilobytesPerSecond=KB/s
|
||||
format.megabytesPerSecond=MB/s
|
||||
format.gigabytesPerSecond=GB/s
|
||||
format.milliseconds=毫秒
|
||||
copyHelp=复制地址到剪贴板
|
||||
shell.productHint=SRv6 网络控制台
|
||||
shell.workspace=工作区
|
||||
shell.awaitingGateway=等待网关连接
|
||||
shell.noData=尚未接入运行数据
|
||||
shell.footer=运行状态 · 数据由网关提供
|
||||
shell.connected=网关已连接
|
||||
shell.links=条链路
|
||||
shell.error=暂时不可用
|
||||
shell.refreshFailed=刷新失败,请稍后重试
|
||||
shell.pendingView=该视图将在后续迁移中提供
|
||||
metric.devices=在线设备
|
||||
metric.totalTransferred=累计流量
|
||||
overview.backplane=骨干面
|
||||
overview.clock=时钟
|
||||
metric.delay=延迟
|
||||
metric.ppsMax=PPS 峰值
|
||||
metric.ecnLoss=ECN / 丢包
|
||||
metric.clockTime=当前时间
|
||||
metric.frequency=频率
|
||||
metric.ppm=频偏
|
||||
format.percent=%
|
||||
format.hz=Hz
|
||||
format.ppm=PPM
|
||||
state.unstable=不稳定
|
||||
state.accessible=连接状态
|
||||
connections.copyEndpoint=复制端点地址
|
||||
connections.copyVirtual=复制虚拟地址
|
||||
connections.copyBind=复制绑定地址
|
||||
|
||||
@@ -11,23 +11,31 @@ import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Scanner;
|
||||
|
||||
import org.kne.cloud.network.MultiProtocolSocketAddress;
|
||||
import org.kne.cloud.network.SocketChannelListener;
|
||||
import org.kne.cloud.network.ipv6.IPv6NetworkLink;
|
||||
import org.kne.cloud.network.ipv6.RouteItem;
|
||||
import org.kne.cloud.network.klalb.ui.KLALBStateGUI3;
|
||||
import org.kne.cloud.network.klalb.ui.UIEnv;
|
||||
import java.util.Scanner;
|
||||
|
||||
import javax.swing.SwingUtilities;
|
||||
|
||||
import javafx.scene.Scene;
|
||||
|
||||
import org.kne.cloud.network.MultiProtocolSocketAddress;
|
||||
import org.kne.cloud.network.SocketChannelListener;
|
||||
import org.kne.cloud.network.ipv6.IPv6NetworkLink;
|
||||
import org.kne.cloud.network.ipv6.RouteItem;
|
||||
import org.kne.cloud.network.klalb.ui.KLALBStateGUI3;
|
||||
import org.kne.cloud.network.klalb.ui.UIEnv;
|
||||
import org.kne.cloud.network.klalb.ui.fx.FxOperationsConsoleShell;
|
||||
import org.kne.cloud.network.klalb.ui.fx.KLALBJavaFXGateway;
|
||||
import org.kne.cloud.network.perf.Kperf;
|
||||
import org.kne.cloud.network.perf.NodeBenchmark;
|
||||
import org.kne.debug.TimeDebugger;
|
||||
import org.kne.membandboost.MembandBenchmark;
|
||||
import org.kne.membandboost.ObjectPoolBandwidthTest;
|
||||
|
||||
public class KLALBMain {
|
||||
public static KLALBStateGUI3 ksg;
|
||||
public static void main(String[] args) throws IOException {
|
||||
public class KLALBMain {
|
||||
public static KLALBStateGUI3 ksg;
|
||||
private static KLALBJavaFXGateway fxGateway;
|
||||
private static final Object swingGuiLock = new Object();
|
||||
public static void main(String[] args) throws IOException {
|
||||
try {
|
||||
UIEnv.inituie();
|
||||
}catch(Throwable e) {
|
||||
@@ -46,9 +54,14 @@ public class KLALBMain {
|
||||
System.out.println("=".repeat(65));
|
||||
System.out.println(" - IPv6 Address / End SID: "+kpcje.getKlalbController().getSelf().getAddress());
|
||||
System.out.println("=".repeat(65));
|
||||
try {
|
||||
if(!kpcje.getControllerConfig().isNogui())
|
||||
openGUI(kpcje);
|
||||
try {
|
||||
if(!kpcje.getControllerConfig().isNogui()) {
|
||||
if (hasArgument(args, "--javafx")) {
|
||||
openJavaFX(kpcje);
|
||||
} else {
|
||||
openGUI(kpcje);
|
||||
}
|
||||
}
|
||||
}catch(Throwable e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
@@ -267,14 +280,46 @@ public class KLALBMain {
|
||||
}
|
||||
|
||||
}
|
||||
private static void openGUI(KLALBProxySystem kpcje) throws RuntimeException{
|
||||
Thread t=new Thread(()->{
|
||||
if(ksg==null) {
|
||||
ksg=kpcje.getKLALBGUI();
|
||||
}
|
||||
ksg.setVisible(true);
|
||||
});
|
||||
t.start();
|
||||
}
|
||||
|
||||
}
|
||||
private static void openGUI(KLALBProxySystem kpcje) throws RuntimeException{
|
||||
Runnable showWindow = () -> {
|
||||
synchronized (swingGuiLock) {
|
||||
if (ksg == null) {
|
||||
ksg = kpcje.getKLALBGUI();
|
||||
}
|
||||
ksg.setVisible(true);
|
||||
}
|
||||
};
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
showWindow.run();
|
||||
} else {
|
||||
SwingUtilities.invokeLater(showWindow);
|
||||
}
|
||||
}
|
||||
|
||||
private static void openJavaFX(KLALBProxySystem kpcje) {
|
||||
if (fxGateway == null) {
|
||||
fxGateway = new KLALBJavaFXGateway(kpcje.getKlalbController());
|
||||
}
|
||||
fxGateway.openStage(stage -> {
|
||||
FxOperationsConsoleShell shell = new FxOperationsConsoleShell(fxGateway);
|
||||
stage.setTitle(CONST.klalb);
|
||||
stage.setScene(new Scene(shell.createRoot()));
|
||||
stage.setOnShown(event -> shell.startRefresh());
|
||||
stage.setOnHidden(event -> shell.dispose());
|
||||
}).whenComplete((stage, error) -> {
|
||||
if (error != null) {
|
||||
System.err.println("Failed to start JavaFX: " + error.getMessage());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean hasArgument(String[] args, String expected) {
|
||||
for (String arg : args) {
|
||||
if (expected.equals(arg)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package org.kne.cloud.network.klalb.ui.fx;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.Locale;
|
||||
|
||||
final class FxFormat {
|
||||
private static final DecimalFormat NUMBER = new DecimalFormat("#,##0");
|
||||
private FxFormat() { }
|
||||
static String number(long value) { return NUMBER.format(Math.max(0, value)); }
|
||||
static String rate(long bytesPerSecond, FxText text) { double value = Math.max(0, bytesPerSecond); String[] units = { "format.bytesPerSecond", "format.kilobytesPerSecond", "format.megabytesPerSecond", "format.gigabytesPerSecond" }; int unit = 0; while (value >= 1024 && unit < units.length - 1) { value /= 1024; unit++; } return (unit == 0 ? number((long) value) : new DecimalFormat("#,##0.0").format(value)) + " " + text.get(units[unit]); }
|
||||
static String delay(long nanos, FxText text) { long safe = Math.max(0, nanos); if (safe < 1_000) return safe + " ns"; if (safe < 1_000_000) return decimal(safe / 1_000.0, "#,##0.###") + " us"; double millis = safe / 1_000_000.0; String pattern = millis < 10 ? "#,##0.###" : millis < 100 ? "#,##0.##" : "#,##0.#"; return decimal(millis, pattern) + " " + text.get("format.milliseconds"); }
|
||||
private static String decimal(double value, String pattern) { return new DecimalFormat(pattern, DecimalFormatSymbols.getInstance(Locale.ROOT)).format(value); }
|
||||
static String bytes(long bytes, FxText text) { return rate(bytes, text).replace("/s", ""); }
|
||||
static String percent(double value, FxText text) { return String.format(java.util.Locale.ROOT, "%.2f %s", Math.max(0, value) * 100, text.get("format.percent")); }
|
||||
static String frequency(long value, FxText text) { return number(value) + " " + text.get("format.hz"); }
|
||||
static String ppm(double value, FxText text) { return String.format(java.util.Locale.ROOT, "%.3f %s", value, text.get("format.ppm")); }
|
||||
}
|
||||
@@ -0,0 +1,364 @@
|
||||
package org.kne.cloud.network.klalb.ui.fx;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonBar.ButtonData;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.Separator;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.scene.layout.FlowPane;
|
||||
|
||||
/** Native JavaFX shell for the first migrated operations views. */
|
||||
public final class FxOperationsConsoleShell {
|
||||
private static final int STATE_DOWN = 0;
|
||||
private static final int STATE_UNSTABLE = 1;
|
||||
private static final int STATE_UP = 2;
|
||||
public enum Section {
|
||||
OVERVIEW("overview", "Overview", "overview.title"),
|
||||
CONNECTIONS("connections", "Connections", "connections.title"),
|
||||
ROUTING("routing", "Routing", "routing.title"),
|
||||
TOPOLOGY("topology", "Topology", "topology.title"),
|
||||
SETTINGS("settings", "Settings", "settings.title");
|
||||
|
||||
private final String key;
|
||||
private final String accessibleName;
|
||||
private final String titleKey;
|
||||
|
||||
Section(String key, String accessibleName, String titleKey) {
|
||||
this.key = key;
|
||||
this.accessibleName = accessibleName;
|
||||
this.titleKey = titleKey;
|
||||
}
|
||||
}
|
||||
|
||||
private final KLALBJavaFXPresentation presentation;
|
||||
private final KLALBJavaFXGateway gateway;
|
||||
private final ReadOnlyObjectWrapper<Section> selectedSection =
|
||||
new ReadOnlyObjectWrapper<>(Section.OVERVIEW);
|
||||
private final BorderPane root = new BorderPane();
|
||||
private final StackPane content = new StackPane();
|
||||
private final Label pageTitle = new Label();
|
||||
private final Label connectionState = new Label();
|
||||
private final Label statusDetail = new Label();
|
||||
private final VBox navigation = new VBox(6);
|
||||
private final FxText text = new FxText();
|
||||
private KLALBJavaFXRefreshCoordinator refreshCoordinator;
|
||||
private boolean showOffline;
|
||||
private KLALBStatusSnapshot latestStatus;
|
||||
private KLALBConnectionsSnapshot latestConnections;
|
||||
private StackPane connectionsView;
|
||||
private TextField endpointInput;
|
||||
private Button addEndpointButton;
|
||||
private Button reconnectAllButton;
|
||||
private Button offlineButton;
|
||||
private ScrollPane connectionsScroll;
|
||||
private VBox connectionRows;
|
||||
private Label emptyConnections;
|
||||
private final Map<String, ConnectionRowView> connectionRowViews = new LinkedHashMap<>();
|
||||
|
||||
public FxOperationsConsoleShell(KLALBJavaFXPresentation presentation) {
|
||||
this(presentation, null);
|
||||
}
|
||||
|
||||
/** Allows a lifecycle owner to supply the bounded coordinator it already owns. */
|
||||
public FxOperationsConsoleShell(KLALBJavaFXPresentation presentation, KLALBJavaFXRefreshCoordinator coordinator) {
|
||||
this.presentation = presentation;
|
||||
this.gateway = null;
|
||||
this.refreshCoordinator = coordinator;
|
||||
build();
|
||||
}
|
||||
|
||||
public FxOperationsConsoleShell(KLALBJavaFXGateway gateway) {
|
||||
this.presentation = gateway;
|
||||
this.gateway = gateway;
|
||||
build();
|
||||
}
|
||||
|
||||
/** Compatibility constructor for lifecycle wiring before the presentation is available. */
|
||||
public FxOperationsConsoleShell() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public Parent createRoot() { return root; }
|
||||
|
||||
public KLALBJavaFXPresentation getPresentation() { return presentation; }
|
||||
|
||||
public ReadOnlyObjectProperty<Section> selectedSectionProperty() {
|
||||
return selectedSection.getReadOnlyProperty();
|
||||
}
|
||||
|
||||
public Section getSelectedSection() { return selectedSection.get(); }
|
||||
|
||||
public void showSection(Section section) {
|
||||
if (section == null) return;
|
||||
selectedSection.set(section);
|
||||
pageTitle.setText(text.get(section.titleKey));
|
||||
refreshNavigationState();
|
||||
if (section == Section.OVERVIEW && latestStatus != null) showOverview(latestStatus);
|
||||
else if (section == Section.CONNECTIONS && latestConnections != null) showConnections(latestConnections);
|
||||
else showPlaceholder(section);
|
||||
}
|
||||
|
||||
/** Starts one bounded overview/connections request chain while this shell is visible. */
|
||||
public void startRefresh() {
|
||||
if (presentation == null) return;
|
||||
if (refreshCoordinator == null && gateway != null) refreshCoordinator = gateway.createRefreshCoordinator(Duration.ofSeconds(1));
|
||||
if (refreshCoordinator != null) {
|
||||
refreshCoordinator.start(new KLALBJavaFXRefreshCoordinator.Listener() {
|
||||
public void onOverview(KLALBStatusSnapshot snapshot) { latestStatus = snapshot; if (getSelectedSection() == Section.OVERVIEW) showOverview(snapshot); updateHeader(snapshot); }
|
||||
public void onConnections(KLALBConnectionsSnapshot snapshot) { latestConnections = snapshot; if (getSelectedSection() == Section.CONNECTIONS) showConnections(snapshot); }
|
||||
public void onError(Throwable error) { showError(error); }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Parent lifecycle must call this when the stage is hidden or disposed. */
|
||||
public void dispose() {
|
||||
if (refreshCoordinator != null) { refreshCoordinator.close(); refreshCoordinator = null; }
|
||||
}
|
||||
|
||||
private void build() {
|
||||
root.getStyleClass().add("fx-console");
|
||||
java.net.URL stylesheet = getClass().getResource("klalb-operations.css");
|
||||
if (stylesheet != null) root.getStylesheets().add(stylesheet.toExternalForm());
|
||||
root.setTop(buildHeader()); root.setLeft(buildSidebar()); root.setCenter(content); root.setBottom(buildFooter());
|
||||
root.setMinSize(760, 500); showSection(Section.OVERVIEW);
|
||||
}
|
||||
|
||||
private Parent buildHeader() {
|
||||
Label product = new Label("KLALB"); product.getStyleClass().add("product-mark");
|
||||
Label hint = new Label(text.get("shell.productHint")); hint.getStyleClass().add("product-hint");
|
||||
HBox identity = new HBox(12, product, new Separator(javafx.geometry.Orientation.VERTICAL), hint);
|
||||
identity.setAlignment(Pos.CENTER_LEFT);
|
||||
pageTitle.getStyleClass().add("page-title");
|
||||
Region spacer = new Region(); HBox.setHgrow(spacer, Priority.ALWAYS);
|
||||
Label dot = new Label("●"); dot.getStyleClass().add("status-dot");
|
||||
connectionState.getStyleClass().add("status-label"); statusDetail.getStyleClass().add("status-detail");
|
||||
HBox status = new HBox(8, dot, connectionState, statusDetail); status.setAlignment(Pos.CENTER_RIGHT);
|
||||
HBox header = new HBox(24, identity, pageTitle, spacer, status); header.getStyleClass().add("topbar");
|
||||
header.setAlignment(Pos.CENTER_LEFT); header.setPadding(new Insets(18, 28, 18, 28));
|
||||
connectionState.setText(text.get("shell.awaitingGateway")); statusDetail.setText(text.get("shell.noData"));
|
||||
return header;
|
||||
}
|
||||
|
||||
private Parent buildSidebar() {
|
||||
navigation.getStyleClass().add("navigation");
|
||||
for (Section section : Section.values()) {
|
||||
Button item = new Button(text.get("nav." + section.key)); item.setMaxWidth(Double.MAX_VALUE); item.setMinHeight(42);
|
||||
item.setMnemonicParsing(false); item.setAccessibleText(section.accessibleName); item.setAccessibleHelp(text.get(section.titleKey));
|
||||
item.setContentDisplay(ContentDisplay.LEFT); item.setOnAction(event -> showSection(section)); item.getProperties().put("section", section);
|
||||
navigation.getChildren().add(item);
|
||||
}
|
||||
Label caption = new Label(text.get("shell.workspace")); caption.getStyleClass().add("nav-caption");
|
||||
VBox sidebar = new VBox(12, caption, navigation); sidebar.getStyleClass().add("sidebar"); sidebar.setPadding(new Insets(25, 16, 20, 16)); sidebar.setPrefWidth(188);
|
||||
return sidebar;
|
||||
}
|
||||
|
||||
private Parent buildFooter() {
|
||||
Label footer = new Label(text.get("shell.footer")); footer.getStyleClass().add("footer-label");
|
||||
HBox bar = new HBox(footer); bar.getStyleClass().add("footer"); bar.setPadding(new Insets(9, 28, 10, 28));
|
||||
return bar;
|
||||
}
|
||||
|
||||
private void showOverview(KLALBStatusSnapshot s) {
|
||||
VBox body = pageBody("overview.title", "overview.subtitle");
|
||||
FlowPane stats = new FlowPane(12, 12, metric(text.get("metric.links"), s.getActiveLinkCount() + " / " + s.getLinkCount()),
|
||||
metric(text.get("metric.devices"), FxFormat.number(s.getOnlineDeviceCount())),
|
||||
metric(text.get("metric.totalThroughput"), FxFormat.rate(s.getUploadBytesPerSecond() + s.getDownloadBytesPerSecond(), text)),
|
||||
metric(text.get("metric.totalPps"), FxFormat.number(s.getUploadPacketsPerSecond() + s.getDownloadPacketsPerSecond())));
|
||||
stats.getStyleClass().add("metric-row");
|
||||
VBox traffic = panel(text.get("overview.dataPlane"),
|
||||
line(text.get("metric.upload"), FxFormat.rate(s.getDataUploadBytesPerSecond(), text) + " · " + FxFormat.number(s.getDataUploadPacketsPerSecond()) + " " + text.get("metric.pps")),
|
||||
line(text.get("metric.download"), FxFormat.rate(s.getDataDownloadBytesPerSecond(), text) + " · " + FxFormat.number(s.getDataDownloadPacketsPerSecond()) + " " + text.get("metric.pps")),
|
||||
line(text.get("metric.totalTransferred"), FxFormat.bytes(s.getDataUploadBytes() + s.getDataDownloadBytes(), text)));
|
||||
VBox backplane = panel(text.get("overview.backplane"),
|
||||
line(text.get("metric.delay"), FxFormat.delay(s.getBackplaneDelayNanos(), text)),
|
||||
line(text.get("metric.pps"), FxFormat.number(s.getBackplanePacketsPerSecond())),
|
||||
line(text.get("metric.ppsMax"), FxFormat.number(s.getBackplanePacketsMax())),
|
||||
line(text.get("metric.ecnLoss"), FxFormat.percent(s.getBackplaneEcnRate(), text) + " / " + FxFormat.percent(s.getBackplaneLossRate(), text)));
|
||||
FlowPane health = new FlowPane(12, 12, traffic, backplane); health.getStyleClass().add("overview-panels");
|
||||
VBox clock = panel(text.get("overview.clock"),
|
||||
line(text.get("metric.clockTime"), nullToDash(s.getClockTime())),
|
||||
line(text.get("metric.frequency"), FxFormat.frequency(s.getClockFrequency(), text)),
|
||||
line(text.get("metric.ppm"), FxFormat.ppm(s.getClockFrequencyOffsetPpm(), text)));
|
||||
HBox node = new HBox(10, new Label(text.get("overview.localAddress")), new Label(s.getNodeAddress()), copyButton(s.getNodeAddress()));
|
||||
node.getStyleClass().add("address-row"); node.setAlignment(Pos.CENTER_LEFT);
|
||||
body.getChildren().addAll(stats, health, clock, node); content.setPadding(new Insets(26, 30, 30, 30)); content.getChildren().setAll(wrap(body));
|
||||
}
|
||||
|
||||
private void showConnections(KLALBConnectionsSnapshot snapshot) {
|
||||
ensureConnectionsView();
|
||||
applyConnectionsSnapshot(snapshot);
|
||||
content.setPadding(new Insets(26, 30, 30, 30)); content.getChildren().setAll(connectionsView);
|
||||
}
|
||||
|
||||
private void ensureConnectionsView() {
|
||||
if (connectionsView != null) return;
|
||||
VBox body = pageBody("connections.title", "connections.subtitle");
|
||||
HBox actions = new HBox(8); actions.getStyleClass().add("action-row");
|
||||
endpointInput = new TextField(); endpointInput.setPromptText(text.get("connections.endpointPrompt")); endpointInput.setAccessibleText(text.get("connections.endpoint")); HBox.setHgrow(endpointInput, Priority.ALWAYS);
|
||||
addEndpointButton = action(text.get("connections.add"));
|
||||
addEndpointButton.setOnAction(e -> runAddAction(presentation.addEndpoint(endpointInput.getText().trim()), addEndpointButton));
|
||||
endpointInput.setOnAction(e -> addEndpointButton.fire());
|
||||
reconnectAllButton = action(text.get("connections.reconnectAll"));
|
||||
reconnectAllButton.setOnAction(e -> runAction(presentation.reconnectNow(), reconnectAllButton, text.get("connections.reconnecting")));
|
||||
offlineButton = action(showOffline ? text.get("connections.hideOffline") : text.get("connections.showOffline"));
|
||||
offlineButton.setOnAction(e -> { showOffline = !showOffline; updateOfflineButton(); applyConnectionsSnapshot(latestConnections); });
|
||||
actions.getChildren().addAll(endpointInput, addEndpointButton, reconnectAllButton, offlineButton);
|
||||
body.getChildren().add(actions);
|
||||
connectionRows = new VBox(8); connectionRows.getStyleClass().add("connection-list");
|
||||
connectionsScroll = new ScrollPane(connectionRows); connectionsScroll.setFitToWidth(true); connectionsScroll.getStyleClass().add("connection-scroll"); VBox.setVgrow(connectionsScroll, Priority.ALWAYS); body.getChildren().add(connectionsScroll);
|
||||
connectionsView = wrap(body);
|
||||
}
|
||||
|
||||
private void applyConnectionsSnapshot(KLALBConnectionsSnapshot snapshot) {
|
||||
if (snapshot == null) return;
|
||||
List<ConnectionRowView> visibleRows = new ArrayList<>();
|
||||
for (KLALBConnectionSnapshot row : snapshot.getRows()) {
|
||||
String token = row.getIdentityToken();
|
||||
ConnectionRowView view = connectionRowViews.get(token);
|
||||
if (view == null) {
|
||||
view = new ConnectionRowView(row);
|
||||
connectionRowViews.put(token, view);
|
||||
} else {
|
||||
view.update(row);
|
||||
}
|
||||
if (showOffline || row.getState() != STATE_DOWN) visibleRows.add(view);
|
||||
}
|
||||
connectionRowViews.keySet().retainAll(tokens(snapshot));
|
||||
emptyConnections = emptyConnections == null ? empty(text.get("connections.empty")) : emptyConnections;
|
||||
reconcileConnectionRows(visibleRows);
|
||||
}
|
||||
|
||||
private void reconcileConnectionRows(List<ConnectionRowView> visibleRows) {
|
||||
if (visibleRows.isEmpty()) {
|
||||
for (int i = connectionRows.getChildren().size() - 1; i >= 0; i--) {
|
||||
if (connectionRows.getChildren().get(i) != emptyConnections) connectionRows.getChildren().remove(i);
|
||||
}
|
||||
if (!connectionRows.getChildren().contains(emptyConnections)) connectionRows.getChildren().add(emptyConnections);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = connectionRows.getChildren().size() - 1; i >= 0; i--) {
|
||||
if (!containsRoot(visibleRows, connectionRows.getChildren().get(i))) connectionRows.getChildren().remove(i);
|
||||
}
|
||||
for (ConnectionRowView row : visibleRows) {
|
||||
if (!connectionRows.getChildren().contains(row.root)) connectionRows.getChildren().add(row.root);
|
||||
}
|
||||
FXCollections.sort(connectionRows.getChildren(), (left, right) -> Integer.compare(
|
||||
indexOfRoot(visibleRows, left), indexOfRoot(visibleRows, right)));
|
||||
}
|
||||
|
||||
private boolean containsRoot(List<ConnectionRowView> rows, javafx.scene.Node node) {
|
||||
for (ConnectionRowView row : rows) if (row.root == node) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private int indexOfRoot(List<ConnectionRowView> rows, javafx.scene.Node node) {
|
||||
for (int i = 0; i < rows.size(); i++) if (rows.get(i).root == node) return i;
|
||||
return rows.size();
|
||||
}
|
||||
|
||||
private List<String> tokens(KLALBConnectionsSnapshot snapshot) {
|
||||
List<String> tokens = new ArrayList<>();
|
||||
for (KLALBConnectionSnapshot row : snapshot.getRows()) tokens.add(row.getIdentityToken());
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private void updateOfflineButton() {
|
||||
if (offlineButton != null) offlineButton.setText(text.get(showOffline ? "connections.hideOffline" : "connections.showOffline"));
|
||||
}
|
||||
|
||||
private final class ConnectionRowView {
|
||||
private String identityToken;
|
||||
private KLALBConnectionSnapshot currentSnapshot;
|
||||
private final HBox root;
|
||||
private final Label state = new Label();
|
||||
private final Label name = new Label();
|
||||
private final Label address = new Label();
|
||||
private final Label metrics = new Label();
|
||||
private final Button copyEndpoint;
|
||||
private final Button copyVirtual;
|
||||
private final Button copyBind;
|
||||
|
||||
private ConnectionRowView(KLALBConnectionSnapshot row) {
|
||||
state.getStyleClass().add("connection-state"); name.getStyleClass().add("row-name"); address.getStyleClass().add("row-address"); metrics.getStyleClass().add("row-metrics");
|
||||
VBox info = new VBox(5, new HBox(9, state, name), address, metrics); HBox.setHgrow(info, Priority.ALWAYS);
|
||||
copyEndpoint = copyButton(row.getSocketAddress(), "connections.copyEndpoint");
|
||||
copyVirtual = copyButton(row.getRemoteAddress(), "connections.copyVirtual");
|
||||
copyBind = copyButton(row.getBindAddress(), "connections.copyBind");
|
||||
HBox copies = new HBox(5, copyEndpoint, copyVirtual, copyBind);
|
||||
Button reconnect = action(text.get("connections.reconnect")); reconnect.setOnAction(e -> runAction(presentation.reconnectLink(identityToken), reconnect, text.get("connections.reconnecting")));
|
||||
Button disconnect = action(text.get("connections.disconnect")); disconnect.setOnAction(e -> runAction(presentation.forceDisconnect(identityToken), disconnect, text.get("connections.disconnected")));
|
||||
Button remove = action(text.get("connections.remove")); remove.setOnAction(e -> confirmRemove(currentSnapshot, remove));
|
||||
VBox buttons = new VBox(5, copies, new HBox(5, reconnect, disconnect, remove)); buttons.getStyleClass().add("row-actions");
|
||||
root = new HBox(14, info, buttons); root.getStyleClass().add("connection-row"); root.setAlignment(Pos.CENTER_LEFT);
|
||||
update(row);
|
||||
}
|
||||
|
||||
private void update(KLALBConnectionSnapshot row) {
|
||||
currentSnapshot = row;
|
||||
identityToken = row.getIdentityToken();
|
||||
boolean unstable = row.getState() == STATE_UNSTABLE;
|
||||
boolean online = row.getState() == STATE_UP;
|
||||
state.setText(unstable ? text.get("state.unstable") : online ? text.get("state.online") : text.get("state.offline"));
|
||||
state.getStyleClass().removeAll("state-unstable", "state-online", "state-offline");
|
||||
state.getStyleClass().add(unstable ? "state-unstable" : online ? "state-online" : "state-offline");
|
||||
state.setAccessibleText(text.get("state.accessible") + " " + state.getText());
|
||||
name.setText(row.getName() == null ? row.getConnectionId() : row.getName());
|
||||
address.setText(nullToDash(row.getRemoteAddress()));
|
||||
metrics.setText(FxFormat.rate(row.getUploadBytesPerSecond(), text) + " " + text.get("metric.up") + " " + FxFormat.rate(row.getDownloadBytesPerSecond(), text) + " " + text.get("metric.down") + " " + FxFormat.delay(row.getUploadDelayNanos(), text) + " / " + FxFormat.delay(row.getDownloadDelayNanos(), text));
|
||||
copyEndpoint.setOnAction(e -> copy(currentSnapshot.getSocketAddress()));
|
||||
copyVirtual.setOnAction(e -> copy(currentSnapshot.getRemoteAddress()));
|
||||
copyBind.setOnAction(e -> copy(currentSnapshot.getBindAddress()));
|
||||
}
|
||||
}
|
||||
|
||||
private void confirmRemove(KLALBConnectionSnapshot row, Button source) {
|
||||
Alert alert = new Alert(Alert.AlertType.CONFIRMATION); alert.setTitle(text.get("connections.confirmTitle")); alert.setHeaderText(text.get("connections.confirmHeader")); alert.setContentText(nullToDash(row.getSocketAddress()));
|
||||
ButtonType yes = new ButtonType(text.get("connections.remove"), ButtonData.OK_DONE); alert.getButtonTypes().setAll(yes, ButtonType.CANCEL);
|
||||
Optional<ButtonType> result = alert.showAndWait(); if (result.isPresent() && result.get() == yes) runAction(presentation.removeLink(row.getIdentityToken()), source, text.get("connections.removed"));
|
||||
}
|
||||
|
||||
private VBox pageBody(String titleKey, String subtitleKey) { Label title = new Label(text.get(titleKey)); title.getStyleClass().add("view-title"); Label sub = new Label(text.get(subtitleKey)); sub.getStyleClass().add("view-subtitle"); VBox body = new VBox(18, title, sub); body.getStyleClass().add("page-body"); return body; }
|
||||
private VBox metric(String label, String value) { Label l = new Label(label); l.getStyleClass().add("metric-label"); Label v = new Label(value); v.getStyleClass().add("metric-value"); VBox box = new VBox(8, l, v); box.getStyleClass().add("metric"); HBox.setHgrow(box, Priority.ALWAYS); return box; }
|
||||
private VBox panel(String title, Parent... children) { Label h = new Label(title); h.getStyleClass().add("panel-title"); VBox p = new VBox(12); p.getStyleClass().add("data-panel"); p.getChildren().add(h); p.getChildren().addAll(children); return p; }
|
||||
private HBox line(String label, String value) { Label l = new Label(label); l.getStyleClass().add("line-label"); Label v = new Label(value); v.getStyleClass().add("line-value"); HBox row = new HBox(l, v); HBox.setHgrow(v, Priority.ALWAYS); row.getStyleClass().add("data-line"); return row; }
|
||||
private Button action(String label) { Button b = new Button(label); b.setMnemonicParsing(false); return b; }
|
||||
private Button copyButton(String value) { return copyButton(value, "copyHelp"); }
|
||||
private Button copyButton(String value, String helpKey) { Button b = action(text.get("copy")); b.setAccessibleHelp(text.get(helpKey)); b.setOnAction(e -> copy(value)); return b; }
|
||||
private void copy(String value) { if (value != null && !value.isEmpty()) { ClipboardContent c = new ClipboardContent(); c.putString(value); Clipboard.getSystemClipboard().setContent(c); } }
|
||||
private StackPane wrap(Parent child) { StackPane pane = new StackPane(child); pane.setAlignment(Pos.TOP_LEFT); return pane; }
|
||||
private Label empty(String value) { Label l = new Label(value); l.getStyleClass().add("empty-message"); return l; }
|
||||
private void showPlaceholder(Section section) { content.getChildren().setAll(wrap(pageBody(section.titleKey, "shell.pendingView"))); }
|
||||
private void updateHeader(KLALBStatusSnapshot s) { connectionState.setText(text.get("shell.connected")); statusDetail.setText(s.getActiveLinkCount() + " / " + s.getLinkCount() + " " + text.get("shell.links")); }
|
||||
private void showError(Throwable error) { connectionState.setText(text.get("shell.error")); statusDetail.setText(text.get("shell.refreshFailed")); if (getSelectedSection() == Section.OVERVIEW) content.getChildren().setAll(wrap(pageBody("shell.error", "shell.refreshFailed"))); }
|
||||
private void runAction(java.util.concurrent.CompletableFuture<?> future, Button source, String success) { if (future == null) return; source.setDisable(true); future.whenComplete((v, e) -> { source.setDisable(false); if (e != null) showError(e); else statusDetail.setText(success); }); }
|
||||
private void runAddAction(java.util.concurrent.CompletableFuture<Integer> future, Button source) { if (future == null) return; source.setDisable(true); future.whenComplete((count, e) -> { source.setDisable(false); if (e != null) showError(e); else statusDetail.setText(text.get("connections.added") + " (" + count + ")"); }); }
|
||||
private void refreshNavigationState() { for (javafx.scene.Node node : navigation.getChildren()) { Button b = (Button) node; b.pseudoClassStateChanged(javafx.css.PseudoClass.getPseudoClass("selected"), b.getProperties().get("section") == selectedSection.get()); } }
|
||||
private static String nullToDash(String value) { return value == null || value.isEmpty() ? "-" : value; }
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.kne.cloud.network.klalb.ui.fx;
|
||||
|
||||
import java.util.ResourceBundle;
|
||||
import org.kne.cloud.network.klalb.ui.UIEnv;
|
||||
|
||||
/** Bundle bridge for JavaFX copy; follows the application's active bundle. */
|
||||
final class FxText {
|
||||
private final ResourceBundle bundle = UIEnv.getRsb();
|
||||
String get(String key) { return bundle.containsKey(key) ? bundle.getString(key) : key; }
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package org.kne.cloud.network.klalb.ui.fx;
|
||||
|
||||
import org.kne.cloud.network.klalb.KLALBRemoteLink;
|
||||
import org.kne.cloud.network.ipv6.IPv6AddressGroup;
|
||||
import org.kne.cloud.network.monitor.LinkStatus;
|
||||
import org.kne.cloud.network.monitor.SpeedAndTrafficAndDelayMonitorDataImpl;
|
||||
|
||||
/**
|
||||
* Immutable presentation data for one remote connection.
|
||||
*/
|
||||
public final class KLALBConnectionSnapshot {
|
||||
private final String identityToken;
|
||||
private final String connectionId;
|
||||
private final String name;
|
||||
private final String socketAddress;
|
||||
private final String bindAddress;
|
||||
private final String remoteAddress;
|
||||
private final int state;
|
||||
private final String stateLabel;
|
||||
private final boolean up;
|
||||
private final long uploadBytesPerSecond;
|
||||
private final long downloadBytesPerSecond;
|
||||
private final long uploadPacketsPerSecond;
|
||||
private final long downloadPacketsPerSecond;
|
||||
private final long uploadDelayNanos;
|
||||
private final long downloadDelayNanos;
|
||||
private final long queueUsed;
|
||||
|
||||
private KLALBConnectionSnapshot(String identityToken, String connectionId, String name, String socketAddress,
|
||||
String bindAddress, String remoteAddress, int state, String stateLabel, boolean up,
|
||||
long uploadBytesPerSecond, long downloadBytesPerSecond,
|
||||
long uploadPacketsPerSecond, long downloadPacketsPerSecond, long uploadDelayNanos,
|
||||
long downloadDelayNanos, long queueUsed) {
|
||||
this.identityToken = identityToken;
|
||||
this.connectionId = connectionId;
|
||||
this.name = name;
|
||||
this.socketAddress = socketAddress;
|
||||
this.bindAddress = bindAddress;
|
||||
this.remoteAddress = remoteAddress;
|
||||
this.state = state;
|
||||
this.stateLabel = stateLabel;
|
||||
this.up = up;
|
||||
this.uploadBytesPerSecond = uploadBytesPerSecond;
|
||||
this.downloadBytesPerSecond = downloadBytesPerSecond;
|
||||
this.uploadPacketsPerSecond = uploadPacketsPerSecond;
|
||||
this.downloadPacketsPerSecond = downloadPacketsPerSecond;
|
||||
this.uploadDelayNanos = uploadDelayNanos;
|
||||
this.downloadDelayNanos = downloadDelayNanos;
|
||||
this.queueUsed = queueUsed;
|
||||
}
|
||||
|
||||
static KLALBConnectionSnapshot capture(KLALBRemoteLink link, String identityToken) {
|
||||
SpeedAndTrafficAndDelayMonitorDataImpl monitor = link.getMonitor();
|
||||
String socketAddress = text(link.getSocketAddress());
|
||||
String name = link.getName();
|
||||
IPv6AddressGroup remoteVaddr = link.getRemoteVaddr();
|
||||
return new KLALBConnectionSnapshot(
|
||||
identityToken,
|
||||
connectionId(socketAddress),
|
||||
name,
|
||||
socketAddress,
|
||||
text(link.getBindAddress()),
|
||||
remoteVaddr == null ? null : remoteVaddr.getAddress().toString(),
|
||||
link.getState(),
|
||||
LinkStatus.stateToString(link.getState()),
|
||||
link.isUp(),
|
||||
monitor.getOutSpeed(),
|
||||
monitor.getInSpeed(),
|
||||
monitor.getOutPPS(),
|
||||
monitor.getInPPS(),
|
||||
monitor.getOutDelay(),
|
||||
monitor.getInDelay(),
|
||||
link.getQueueUsed());
|
||||
}
|
||||
|
||||
private static String connectionId(String socketAddress) {
|
||||
return socketAddress == null ? "" : socketAddress;
|
||||
}
|
||||
|
||||
private static String text(Object value) {
|
||||
return value == null ? null : value.toString();
|
||||
}
|
||||
|
||||
public String getConnectionId() {
|
||||
return connectionId;
|
||||
}
|
||||
|
||||
public String getIdentityToken() {
|
||||
return identityToken;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getSocketAddress() {
|
||||
return socketAddress;
|
||||
}
|
||||
|
||||
public String getBindAddress() {
|
||||
return bindAddress;
|
||||
}
|
||||
|
||||
public String getRemoteAddress() {
|
||||
return remoteAddress;
|
||||
}
|
||||
|
||||
public int getState() {
|
||||
return state;
|
||||
}
|
||||
|
||||
public String getStateLabel() {
|
||||
return stateLabel;
|
||||
}
|
||||
|
||||
public boolean isUp() {
|
||||
return up;
|
||||
}
|
||||
|
||||
public long getUploadBytesPerSecond() {
|
||||
return uploadBytesPerSecond;
|
||||
}
|
||||
|
||||
public long getDownloadBytesPerSecond() {
|
||||
return downloadBytesPerSecond;
|
||||
}
|
||||
|
||||
public long getUploadPacketsPerSecond() {
|
||||
return uploadPacketsPerSecond;
|
||||
}
|
||||
|
||||
public long getDownloadPacketsPerSecond() {
|
||||
return downloadPacketsPerSecond;
|
||||
}
|
||||
|
||||
public long getUploadDelayNanos() {
|
||||
return uploadDelayNanos;
|
||||
}
|
||||
|
||||
public long getDownloadDelayNanos() {
|
||||
return downloadDelayNanos;
|
||||
}
|
||||
|
||||
public long getQueueUsed() {
|
||||
return queueUsed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package org.kne.cloud.network.klalb.ui.fx;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.kne.cloud.network.ipv6.IPv6NetworkLink;
|
||||
import org.kne.cloud.network.klalb.KLALBController;
|
||||
import org.kne.cloud.network.klalb.KLALBRemoteLink;
|
||||
|
||||
/**
|
||||
* Immutable snapshot of the current remote connection rows.
|
||||
*/
|
||||
public final class KLALBConnectionsSnapshot {
|
||||
private final List<KLALBConnectionSnapshot> rows;
|
||||
|
||||
private KLALBConnectionsSnapshot(List<KLALBConnectionSnapshot> rows) {
|
||||
this.rows = Collections.unmodifiableList(new ArrayList<>(rows));
|
||||
}
|
||||
|
||||
static KLALBConnectionsSnapshot capture(KLALBController controller, ConnectionIdentity identity) {
|
||||
return capture(controller, controller.getLines(), identity);
|
||||
}
|
||||
|
||||
static KLALBConnectionsSnapshot capture(KLALBController controller,
|
||||
List<IPv6NetworkLink> links, ConnectionIdentity identity) {
|
||||
List<KLALBConnectionSnapshot> rows = new ArrayList<>();
|
||||
for (IPv6NetworkLink link : links) {
|
||||
if (link instanceof KLALBRemoteLink) {
|
||||
KLALBRemoteLink remoteLink = (KLALBRemoteLink) link;
|
||||
rows.add(KLALBConnectionSnapshot.capture(remoteLink, identity.tokenFor(remoteLink)));
|
||||
}
|
||||
}
|
||||
return new KLALBConnectionsSnapshot(rows);
|
||||
}
|
||||
|
||||
static KLALBConnectionsSnapshot of(List<KLALBConnectionSnapshot> rows) {
|
||||
return new KLALBConnectionsSnapshot(rows);
|
||||
}
|
||||
|
||||
public List<KLALBConnectionSnapshot> getRows() {
|
||||
return rows;
|
||||
}
|
||||
|
||||
public int getConnectionCount() {
|
||||
return rows.size();
|
||||
}
|
||||
|
||||
interface ConnectionIdentity {
|
||||
String tokenFor(KLALBRemoteLink link);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.kne.cloud.network.klalb.ui.fx;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.BooleanSupplier;
|
||||
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
/**
|
||||
* Owns the JavaFX toolkit lifecycle without owning any application view.
|
||||
*/
|
||||
public final class KLALBJavaFXApplication extends Application {
|
||||
private static final AtomicBoolean launchRequested = new AtomicBoolean();
|
||||
private static final CompletableFuture<KLALBJavaFXApplication> ready = new CompletableFuture<>();
|
||||
|
||||
/**
|
||||
* Starts JavaFX asynchronously. The JavaFX toolkit is process-global and
|
||||
* can only be launched once, so every caller receives the same future.
|
||||
*/
|
||||
public static CompletableFuture<KLALBJavaFXApplication> startAsync() {
|
||||
if (launchRequested.compareAndSet(false, true)) {
|
||||
Thread launcher = new Thread(() -> {
|
||||
try {
|
||||
Application.launch(KLALBJavaFXApplication.class);
|
||||
} catch (Throwable error) {
|
||||
ready.completeExceptionally(error);
|
||||
}
|
||||
}, "KLALB JavaFX launcher");
|
||||
launcher.setDaemon(true);
|
||||
launcher.start();
|
||||
}
|
||||
return ready;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Stage primaryStage) {
|
||||
// The primary stage is intentionally not shown. Views open their own
|
||||
// stages through KLALBJavaFXGateway when a desktop workflow is selected.
|
||||
Platform.setImplicitExit(false);
|
||||
ready.complete(this);
|
||||
}
|
||||
|
||||
CompletableFuture<Stage> openStage(Consumer<Stage> initializer) {
|
||||
return openStage(() -> true, initializer);
|
||||
}
|
||||
|
||||
CompletableFuture<Stage> openStage(BooleanSupplier allowed, Consumer<Stage> initializer) {
|
||||
Objects.requireNonNull(initializer, "initializer");
|
||||
CompletableFuture<Stage> result = new CompletableFuture<>();
|
||||
try {
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
if (!allowed.getAsBoolean()) {
|
||||
result.completeExceptionally(new IllegalStateException("JavaFX gateway is closed"));
|
||||
return;
|
||||
}
|
||||
Stage stage = new Stage();
|
||||
initializer.accept(stage);
|
||||
stage.show();
|
||||
result.complete(stage);
|
||||
} catch (Throwable error) {
|
||||
result.completeExceptionally(error);
|
||||
}
|
||||
});
|
||||
} catch (Throwable error) {
|
||||
result.completeExceptionally(error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
CompletableFuture<Void> runOnFx(Runnable action) {
|
||||
return runOnFx(() -> true, action);
|
||||
}
|
||||
|
||||
CompletableFuture<Void> runOnFx(BooleanSupplier allowed, Runnable action) {
|
||||
Objects.requireNonNull(action, "action");
|
||||
CompletableFuture<Void> result = new CompletableFuture<>();
|
||||
try {
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
if (!allowed.getAsBoolean()) {
|
||||
result.completeExceptionally(new IllegalStateException("JavaFX gateway is closed"));
|
||||
return;
|
||||
}
|
||||
action.run();
|
||||
result.complete(null);
|
||||
} catch (Throwable error) {
|
||||
result.completeExceptionally(error);
|
||||
}
|
||||
});
|
||||
} catch (Throwable error) {
|
||||
result.completeExceptionally(error);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package org.kne.cloud.network.klalb.ui.fx;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.IdentityHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.stage.Stage;
|
||||
|
||||
import org.kne.cloud.network.ipv6.IPv6NetworkLink;
|
||||
import org.kne.cloud.network.klalb.KLALBController;
|
||||
import org.kne.cloud.network.klalb.KLALBRemoteLink;
|
||||
import org.kne.cloud.network.MultiProtocolSocketAddress;
|
||||
|
||||
/**
|
||||
* Typed presentation boundary for JavaFX view models.
|
||||
*
|
||||
* Controller snapshots are captured on a dedicated background executor and
|
||||
* their futures are completed on the JavaFX application thread. Stage setup
|
||||
* and scene-graph work are dispatched to that same JavaFX thread.
|
||||
*/
|
||||
public final class KLALBJavaFXGateway implements AutoCloseable, KLALBJavaFXPresentation {
|
||||
private final KLALBController controller;
|
||||
private final ExecutorService controllerExecutor;
|
||||
private final Set<CompletableFuture<?>> pendingWork = ConcurrentHashMap.newKeySet();
|
||||
private final Set<KLALBJavaFXRefreshCoordinator> refreshCoordinators = ConcurrentHashMap.newKeySet();
|
||||
private final IdentityHashMap<KLALBRemoteLink, String> linkTokens = new IdentityHashMap<>();
|
||||
private final Map<String, KLALBRemoteLink> liveLinksByToken = new HashMap<>();
|
||||
private final Object lifecycleLock = new Object();
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
|
||||
public KLALBJavaFXGateway(KLALBController controller) {
|
||||
this.controller = Objects.requireNonNull(controller, "controller");
|
||||
this.controllerExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Thread thread = new Thread(runnable, "KLALB JavaFX controller");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public CompletableFuture<KLALBJavaFXApplication> startAsync() {
|
||||
return KLALBJavaFXApplication.startAsync();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<KLALBStatusSnapshot> requestStatusSnapshot() {
|
||||
return requestOverviewSnapshot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<KLALBStatusSnapshot> requestOverviewSnapshot() {
|
||||
return submitController(KLALBStatusSnapshot::capture);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<KLALBConnectionsSnapshot> requestConnectionsSnapshot() {
|
||||
return submitController(this::captureConnections);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<KLALBOverviewConnectionsSnapshot> requestOverviewAndConnectionsSnapshot() {
|
||||
return submitController(this::captureOverviewAndConnections);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Integer> addEndpoint(String endpoint) {
|
||||
return submitController(currentController -> {
|
||||
int added = currentController.addRemoteLines(new MultiProtocolSocketAddress(endpoint)).size();
|
||||
if (added == 0) {
|
||||
throw new IllegalStateException("No links were added for endpoint: " + endpoint);
|
||||
}
|
||||
return added;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> reconnectNow() {
|
||||
return submitController(currentController -> {
|
||||
currentController.reconnectImmediately();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> reconnectLink(String identityToken) {
|
||||
Objects.requireNonNull(identityToken, "identityToken");
|
||||
return submitController(currentController -> {
|
||||
KLALBRemoteLink remoteLink = findLiveLink(currentController, identityToken);
|
||||
if (remoteLink == null) {
|
||||
throw new IllegalStateException("Connection is no longer available");
|
||||
}
|
||||
remoteLink.reconnectImmediately();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> forceDisconnect(String identityToken) {
|
||||
Objects.requireNonNull(identityToken, "identityToken");
|
||||
return submitController(currentController -> {
|
||||
KLALBRemoteLink remoteLink = findLiveLink(currentController, identityToken);
|
||||
if (remoteLink == null) {
|
||||
throw new IllegalStateException("Connection is no longer available");
|
||||
}
|
||||
remoteLink.dislink();
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<Void> removeLink(String identityToken) {
|
||||
Objects.requireNonNull(identityToken, "identityToken");
|
||||
return submitController(currentController -> {
|
||||
KLALBRemoteLink target = findLiveLink(currentController, identityToken);
|
||||
if (target == null) {
|
||||
throw new IllegalStateException("Connection is no longer available");
|
||||
}
|
||||
MultiProtocolSocketAddress socketAddress = target.getSocketAddress();
|
||||
if (socketAddress == null) {
|
||||
target.close();
|
||||
} else if (currentController.removeRemoteLines(socketAddress).isEmpty()) {
|
||||
throw new IllegalStateException("No matching link found");
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public KLALBJavaFXRefreshCoordinator createRefreshCoordinator(Duration interval) {
|
||||
synchronized (lifecycleLock) {
|
||||
if (closed.get()) {
|
||||
throw new IllegalStateException("JavaFX gateway is closed");
|
||||
}
|
||||
KLALBJavaFXRefreshCoordinator coordinator =
|
||||
new KLALBJavaFXRefreshCoordinator(this, interval);
|
||||
refreshCoordinators.add(coordinator);
|
||||
return coordinator;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a stage after JavaFX is ready. The initializer may configure only
|
||||
* JavaFX objects; controller or network reads belong in request methods.
|
||||
*/
|
||||
public CompletableFuture<Stage> openStage(Consumer<Stage> initializer) {
|
||||
Objects.requireNonNull(initializer, "initializer");
|
||||
if (closed.get()) {
|
||||
return failedFuture(closedException());
|
||||
}
|
||||
return startAsync().thenCompose(application -> application.openStage(
|
||||
() -> !closed.get(), initializer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules scene-graph work on the JavaFX application thread.
|
||||
*/
|
||||
public CompletableFuture<Void> runOnFx(Runnable action) {
|
||||
Objects.requireNonNull(action, "action");
|
||||
if (closed.get()) {
|
||||
return failedFuture(closedException());
|
||||
}
|
||||
return startAsync().thenCompose(application -> application.runOnFx(
|
||||
() -> !closed.get(), action));
|
||||
}
|
||||
|
||||
public boolean isFxApplicationThread() {
|
||||
return Platform.isFxApplicationThread();
|
||||
}
|
||||
|
||||
public boolean isClosed() {
|
||||
return closed.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
Set<CompletableFuture<?>> workToCancel;
|
||||
Set<KLALBJavaFXRefreshCoordinator> coordinatorsToClose;
|
||||
synchronized (lifecycleLock) {
|
||||
if (!closed.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
controllerExecutor.shutdownNow();
|
||||
workToCancel = Set.copyOf(pendingWork);
|
||||
pendingWork.clear();
|
||||
coordinatorsToClose = Set.copyOf(refreshCoordinators);
|
||||
refreshCoordinators.clear();
|
||||
}
|
||||
for (KLALBJavaFXRefreshCoordinator coordinator : coordinatorsToClose) {
|
||||
coordinator.closeFromGateway();
|
||||
}
|
||||
for (CompletableFuture<?> work : workToCancel) {
|
||||
completeOnFx(work, null, closedException());
|
||||
}
|
||||
}
|
||||
|
||||
void unregisterRefreshCoordinator(KLALBJavaFXRefreshCoordinator coordinator) {
|
||||
refreshCoordinators.remove(coordinator);
|
||||
}
|
||||
|
||||
private <T> CompletableFuture<T> submitController(ControllerOperation<T> operation) {
|
||||
Objects.requireNonNull(operation, "operation");
|
||||
CompletableFuture<T> result = new CompletableFuture<>();
|
||||
result.whenComplete((value, error) -> pendingWork.remove(result));
|
||||
synchronized (lifecycleLock) {
|
||||
if (closed.get()) {
|
||||
result.completeExceptionally(closedException());
|
||||
return result;
|
||||
}
|
||||
pendingWork.add(result);
|
||||
startAsync();
|
||||
try {
|
||||
controllerExecutor.execute(() -> {
|
||||
if (closed.get()) {
|
||||
completeOnFx(result, null, closedException());
|
||||
return;
|
||||
}
|
||||
try {
|
||||
completeOnFx(result, operation.run(controller), null);
|
||||
} catch (Throwable error) {
|
||||
completeOnFx(result, null, error);
|
||||
}
|
||||
});
|
||||
} catch (Throwable error) {
|
||||
completeOnFx(result, null, error);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private KLALBConnectionsSnapshot captureConnections(KLALBController currentController) {
|
||||
return captureConnections(currentController, new ArrayList<>(currentController.getLines()));
|
||||
}
|
||||
|
||||
private KLALBConnectionsSnapshot captureConnections(KLALBController currentController,
|
||||
List<IPv6NetworkLink> links) {
|
||||
IdentityHashMap<KLALBRemoteLink, String> currentTokens = new IdentityHashMap<>();
|
||||
Map<String, KLALBRemoteLink> currentLiveLinks = new HashMap<>();
|
||||
KLALBConnectionsSnapshot connections = KLALBConnectionsSnapshot.capture(currentController, links,
|
||||
remoteLink -> {
|
||||
String token = linkTokens.get(remoteLink);
|
||||
if (token == null) {
|
||||
token = UUID.randomUUID().toString();
|
||||
}
|
||||
currentTokens.put(remoteLink, token);
|
||||
currentLiveLinks.put(token, remoteLink);
|
||||
return token;
|
||||
});
|
||||
linkTokens.clear();
|
||||
linkTokens.putAll(currentTokens);
|
||||
liveLinksByToken.clear();
|
||||
liveLinksByToken.putAll(currentLiveLinks);
|
||||
return connections;
|
||||
}
|
||||
|
||||
private KLALBOverviewConnectionsSnapshot captureOverviewAndConnections(KLALBController currentController) {
|
||||
List<IPv6NetworkLink> links = new ArrayList<>(currentController.getLines());
|
||||
KLALBConnectionsSnapshot connections = captureConnections(currentController, links);
|
||||
return KLALBOverviewConnectionsSnapshot.of(
|
||||
KLALBStatusSnapshot.capture(currentController, connections), connections);
|
||||
}
|
||||
|
||||
private KLALBRemoteLink findLiveLink(KLALBController currentController, String identityToken) {
|
||||
KLALBRemoteLink remoteLink = liveLinksByToken.get(identityToken);
|
||||
if (remoteLink != null && !remoteLink.isClosed() && currentController.getLines().contains(remoteLink)) {
|
||||
return remoteLink;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private <T> void completeOnFx(CompletableFuture<T> result, T value, Throwable error) {
|
||||
if (result.isDone()) {
|
||||
pendingWork.remove(result);
|
||||
return;
|
||||
}
|
||||
KLALBJavaFXApplication.startAsync().whenComplete((application, startError) -> {
|
||||
if (startError != null) {
|
||||
result.completeExceptionally(startError);
|
||||
pendingWork.remove(result);
|
||||
return;
|
||||
}
|
||||
application.runOnFx(() -> {
|
||||
synchronized (lifecycleLock) {
|
||||
if (result.isDone()) {
|
||||
return;
|
||||
}
|
||||
if (closed.get()) {
|
||||
result.completeExceptionally(closedException());
|
||||
} else if (error == null) {
|
||||
result.complete(value);
|
||||
} else {
|
||||
result.completeExceptionally(error);
|
||||
}
|
||||
}
|
||||
}).whenComplete((ignored, dispatchError) -> {
|
||||
if (dispatchError != null) {
|
||||
result.completeExceptionally(dispatchError);
|
||||
pendingWork.remove(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static <T> CompletableFuture<T> failedFuture(Throwable error) {
|
||||
CompletableFuture<T> result = new CompletableFuture<>();
|
||||
result.completeExceptionally(error);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static IllegalStateException closedException() {
|
||||
return new IllegalStateException("JavaFX gateway is closed");
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface ControllerOperation<T> {
|
||||
T run(KLALBController controller) throws Exception;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package org.kne.cloud.network.klalb.ui.fx;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Typed presentation boundary exposed to JavaFX view models.
|
||||
*/
|
||||
public interface KLALBJavaFXPresentation {
|
||||
/**
|
||||
* Requests a controller snapshot. Completion handlers are dispatched on the
|
||||
* JavaFX application thread by the gateway.
|
||||
*/
|
||||
CompletableFuture<KLALBStatusSnapshot> requestStatusSnapshot();
|
||||
|
||||
CompletableFuture<KLALBStatusSnapshot> requestOverviewSnapshot();
|
||||
|
||||
CompletableFuture<KLALBConnectionsSnapshot> requestConnectionsSnapshot();
|
||||
|
||||
CompletableFuture<KLALBOverviewConnectionsSnapshot> requestOverviewAndConnectionsSnapshot();
|
||||
|
||||
CompletableFuture<Integer> addEndpoint(String endpoint);
|
||||
|
||||
CompletableFuture<Void> reconnectNow();
|
||||
|
||||
CompletableFuture<Void> reconnectLink(String identityToken);
|
||||
|
||||
CompletableFuture<Void> forceDisconnect(String identityToken);
|
||||
|
||||
/** Resolves one row token, then removes every live link at its socket endpoint. */
|
||||
CompletableFuture<Void> removeLink(String identityToken);
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package org.kne.cloud.network.klalb.ui.fx;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.ThreadFactory;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* Bounded background refresh for JavaFX presentation snapshots.
|
||||
*
|
||||
* At most one overview/connections request chain and one FX delivery are in
|
||||
* flight at a time.
|
||||
*/
|
||||
public final class KLALBJavaFXRefreshCoordinator implements AutoCloseable {
|
||||
private final KLALBJavaFXGateway gateway;
|
||||
private final Duration interval;
|
||||
private final ScheduledExecutorService scheduler;
|
||||
private final AtomicBoolean running = new AtomicBoolean();
|
||||
private final AtomicBoolean closed = new AtomicBoolean();
|
||||
private final AtomicReference<CompletableFuture<?>> inFlight = new AtomicReference<>();
|
||||
private volatile ScheduledFuture<?> ticker;
|
||||
private volatile Listener listener;
|
||||
|
||||
KLALBJavaFXRefreshCoordinator(KLALBJavaFXGateway gateway, Duration interval) {
|
||||
this.gateway = Objects.requireNonNull(gateway, "gateway");
|
||||
this.interval = validateInterval(interval);
|
||||
this.scheduler = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
|
||||
@Override
|
||||
public Thread newThread(Runnable runnable) {
|
||||
Thread thread = new Thread(runnable, "KLALB JavaFX refresh");
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public synchronized void start(Listener listener) {
|
||||
Objects.requireNonNull(listener, "listener");
|
||||
if (closed.get()) {
|
||||
throw new IllegalStateException("JavaFX refresh coordinator is closed");
|
||||
}
|
||||
if (!running.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
this.listener = listener;
|
||||
ticker = scheduler.scheduleWithFixedDelay(this::tick, 0, interval.toMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
if (!running.compareAndSet(true, false)) {
|
||||
return;
|
||||
}
|
||||
ScheduledFuture<?> currentTicker = ticker;
|
||||
if (currentTicker != null) {
|
||||
currentTicker.cancel(false);
|
||||
ticker = null;
|
||||
}
|
||||
CompletableFuture<?> currentRequest = inFlight.getAndSet(null);
|
||||
if (currentRequest != null) {
|
||||
currentRequest.cancel(false);
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized void tick() {
|
||||
if (!running.get() || closed.get() || gateway.isClosed()) {
|
||||
return;
|
||||
}
|
||||
if (inFlight.get() != null) {
|
||||
return;
|
||||
}
|
||||
CompletableFuture<KLALBOverviewConnectionsSnapshot> refresh =
|
||||
gateway.requestOverviewAndConnectionsSnapshot();
|
||||
inFlight.set(refresh);
|
||||
refresh.whenComplete((snapshot, error) -> finish(refresh, snapshot, error));
|
||||
}
|
||||
|
||||
private void finish(CompletableFuture<KLALBOverviewConnectionsSnapshot> request,
|
||||
KLALBOverviewConnectionsSnapshot snapshot, Throwable error) {
|
||||
if (!running.get() || closed.get() || gateway.isClosed()) {
|
||||
inFlight.compareAndSet(request, null);
|
||||
return;
|
||||
}
|
||||
gateway.runOnFx(() -> {
|
||||
inFlight.compareAndSet(request, null);
|
||||
if (!running.get() || closed.get() || gateway.isClosed()) {
|
||||
return;
|
||||
}
|
||||
Listener currentListener = listener;
|
||||
if (error != null) {
|
||||
currentListener.onError(error);
|
||||
} else {
|
||||
currentListener.onOverview(snapshot.getOverview());
|
||||
currentListener.onConnections(snapshot.getConnections());
|
||||
}
|
||||
}).whenComplete((ignored, dispatchError) -> inFlight.compareAndSet(request, null));
|
||||
}
|
||||
|
||||
void closeFromGateway() {
|
||||
if (closed.compareAndSet(false, true)) {
|
||||
stop();
|
||||
scheduler.shutdownNow();
|
||||
gateway.unregisterRefreshCoordinator(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
closeFromGateway();
|
||||
}
|
||||
|
||||
private static Duration validateInterval(Duration interval) {
|
||||
Objects.requireNonNull(interval, "interval");
|
||||
if (interval.isNegative() || interval.isZero() || interval.toMillis() <= 0) {
|
||||
throw new IllegalArgumentException("refresh interval must be positive");
|
||||
}
|
||||
return interval;
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
/** All callbacks are delivered on the JavaFX application thread. */
|
||||
void onOverview(KLALBStatusSnapshot snapshot);
|
||||
|
||||
void onConnections(KLALBConnectionsSnapshot snapshot);
|
||||
|
||||
void onError(Throwable error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package org.kne.cloud.network.klalb.ui.fx;
|
||||
|
||||
/**
|
||||
* Immutable overview and connection data captured in one controller pass.
|
||||
*/
|
||||
public final class KLALBOverviewConnectionsSnapshot {
|
||||
private final KLALBStatusSnapshot overview;
|
||||
private final KLALBConnectionsSnapshot connections;
|
||||
|
||||
private KLALBOverviewConnectionsSnapshot(KLALBStatusSnapshot overview,
|
||||
KLALBConnectionsSnapshot connections) {
|
||||
this.overview = overview;
|
||||
this.connections = connections;
|
||||
}
|
||||
|
||||
static KLALBOverviewConnectionsSnapshot of(KLALBStatusSnapshot overview,
|
||||
KLALBConnectionsSnapshot connections) {
|
||||
return new KLALBOverviewConnectionsSnapshot(overview, connections);
|
||||
}
|
||||
|
||||
public KLALBStatusSnapshot getOverview() {
|
||||
return overview;
|
||||
}
|
||||
|
||||
public KLALBConnectionsSnapshot getConnections() {
|
||||
return connections;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package org.kne.cloud.network.klalb.ui.fx;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.kne.cloud.network.ipv6.IPv6NetworkLink;
|
||||
import org.kne.cloud.network.klalb.KLALBController;
|
||||
import org.kne.cloud.network.klalb.KLALBRemoteLink;
|
||||
|
||||
/**
|
||||
* Immutable presentation data captured from the controller.
|
||||
*/
|
||||
public final class KLALBStatusSnapshot {
|
||||
private final String nodeAddress;
|
||||
private final int linkCount;
|
||||
private final int activeLinkCount;
|
||||
private final long uploadBytesPerSecond;
|
||||
private final long downloadBytesPerSecond;
|
||||
private final long uploadPacketsPerSecond;
|
||||
private final long downloadPacketsPerSecond;
|
||||
private final long uploadBytes;
|
||||
private final long downloadBytes;
|
||||
private final long uploadSpeedMax;
|
||||
private final long downloadSpeedMax;
|
||||
private final long uploadPacketsMax;
|
||||
private final long downloadPacketsMax;
|
||||
private final long dataUploadBytesPerSecond;
|
||||
private final long dataDownloadBytesPerSecond;
|
||||
private final long dataUploadPacketsPerSecond;
|
||||
private final long dataDownloadPacketsPerSecond;
|
||||
private final long dataUploadBytes;
|
||||
private final long dataDownloadBytes;
|
||||
private final long onlineDeviceCount;
|
||||
private final long backplaneDelayNanos;
|
||||
private final long backplanePacketsPerSecond;
|
||||
private final long backplanePacketsMax;
|
||||
private final double backplaneEcnRate;
|
||||
private final double backplaneLossRate;
|
||||
private final String clockTime;
|
||||
private final long clockFrequency;
|
||||
private final double clockFrequencyOffsetPpm;
|
||||
|
||||
private KLALBStatusSnapshot(String nodeAddress, int linkCount, int activeLinkCount,
|
||||
long uploadBytesPerSecond, long downloadBytesPerSecond, long uploadPacketsPerSecond,
|
||||
long downloadPacketsPerSecond, long uploadBytes, long downloadBytes, long uploadSpeedMax,
|
||||
long downloadSpeedMax, long uploadPacketsMax, long downloadPacketsMax,
|
||||
long dataUploadBytesPerSecond, long dataDownloadBytesPerSecond,
|
||||
long dataUploadPacketsPerSecond, long dataDownloadPacketsPerSecond,
|
||||
long dataUploadBytes, long dataDownloadBytes, long onlineDeviceCount,
|
||||
long backplaneDelayNanos, long backplanePacketsPerSecond, long backplanePacketsMax,
|
||||
double backplaneEcnRate, double backplaneLossRate, String clockTime,
|
||||
long clockFrequency, double clockFrequencyOffsetPpm) {
|
||||
this.nodeAddress = nodeAddress;
|
||||
this.linkCount = linkCount;
|
||||
this.activeLinkCount = activeLinkCount;
|
||||
this.uploadBytesPerSecond = uploadBytesPerSecond;
|
||||
this.downloadBytesPerSecond = downloadBytesPerSecond;
|
||||
this.uploadPacketsPerSecond = uploadPacketsPerSecond;
|
||||
this.downloadPacketsPerSecond = downloadPacketsPerSecond;
|
||||
this.uploadBytes = uploadBytes;
|
||||
this.downloadBytes = downloadBytes;
|
||||
this.uploadSpeedMax = uploadSpeedMax;
|
||||
this.downloadSpeedMax = downloadSpeedMax;
|
||||
this.uploadPacketsMax = uploadPacketsMax;
|
||||
this.downloadPacketsMax = downloadPacketsMax;
|
||||
this.dataUploadBytesPerSecond = dataUploadBytesPerSecond;
|
||||
this.dataDownloadBytesPerSecond = dataDownloadBytesPerSecond;
|
||||
this.dataUploadPacketsPerSecond = dataUploadPacketsPerSecond;
|
||||
this.dataDownloadPacketsPerSecond = dataDownloadPacketsPerSecond;
|
||||
this.dataUploadBytes = dataUploadBytes;
|
||||
this.dataDownloadBytes = dataDownloadBytes;
|
||||
this.onlineDeviceCount = onlineDeviceCount;
|
||||
this.backplaneDelayNanos = backplaneDelayNanos;
|
||||
this.backplanePacketsPerSecond = backplanePacketsPerSecond;
|
||||
this.backplanePacketsMax = backplanePacketsMax;
|
||||
this.backplaneEcnRate = backplaneEcnRate;
|
||||
this.backplaneLossRate = backplaneLossRate;
|
||||
this.clockTime = clockTime;
|
||||
this.clockFrequency = clockFrequency;
|
||||
this.clockFrequencyOffsetPpm = clockFrequencyOffsetPpm;
|
||||
}
|
||||
|
||||
static KLALBStatusSnapshot capture(KLALBController controller) {
|
||||
return capture(controller, controller.getLines());
|
||||
}
|
||||
|
||||
static KLALBStatusSnapshot capture(KLALBController controller, List<IPv6NetworkLink> links) {
|
||||
int linkCount = 0;
|
||||
int activeLinkCount = 0;
|
||||
for (IPv6NetworkLink link : links) {
|
||||
if (!(link instanceof KLALBRemoteLink)) {
|
||||
continue;
|
||||
}
|
||||
linkCount++;
|
||||
if (link.isUp()) {
|
||||
activeLinkCount++;
|
||||
}
|
||||
}
|
||||
return capture(controller, linkCount, activeLinkCount);
|
||||
}
|
||||
|
||||
static KLALBStatusSnapshot capture(KLALBController controller,
|
||||
KLALBConnectionsSnapshot connections) {
|
||||
int activeLinkCount = 0;
|
||||
for (KLALBConnectionSnapshot connection : connections.getRows()) {
|
||||
if (connection.isUp()) {
|
||||
activeLinkCount++;
|
||||
}
|
||||
}
|
||||
return capture(controller, connections.getConnectionCount(), activeLinkCount);
|
||||
}
|
||||
|
||||
private static KLALBStatusSnapshot capture(KLALBController controller,
|
||||
int linkCount, int activeLinkCount) {
|
||||
org.kne.cloud.network.monitor.SpeedAndTrafficMonitorData linkMonitor = controller.getLinkMonitor();
|
||||
org.kne.cloud.network.monitor.SpeedAndTrafficMonitorData dataMonitor = controller.getDatatMonitor();
|
||||
org.kne.cloud.network.srv6.SRv6Router router = controller.getIpv6Router();
|
||||
long onlineDeviceCount = router == null || router.getKlalbRouteProtol() == null
|
||||
? 0 : router.getKlalbRouteProtol().getDevicesFound();
|
||||
long backplaneDelayNanos = router == null ? 0 : router.getBackplaneTime();
|
||||
long backplanePacketsPerSecond = router == null ? 0 : router.getBackplanePPS();
|
||||
long backplanePacketsMax = router == null ? 0 : router.getBackplanePPSMax();
|
||||
double backplaneEcnRate = router == null ? 0 : router.getECNRate();
|
||||
double backplaneLossRate = router == null ? 0 : router.getLossRate();
|
||||
long clockFrequency = controller.getClock().getFrequency();
|
||||
return new KLALBStatusSnapshot(
|
||||
controller.getSelf().getAddress().toString(),
|
||||
linkCount,
|
||||
activeLinkCount,
|
||||
linkMonitor.getOutSpeed(),
|
||||
linkMonitor.getInSpeed(),
|
||||
linkMonitor.getOutPPS(),
|
||||
linkMonitor.getInPPS(),
|
||||
linkMonitor.getOutTraffic(),
|
||||
linkMonitor.getInTraffic(),
|
||||
linkMonitor.getOutSpeedMax(),
|
||||
linkMonitor.getInSpeedMax(),
|
||||
linkMonitor.getOutPPSMax(),
|
||||
linkMonitor.getInPPSMax(),
|
||||
dataMonitor.getOutSpeed(),
|
||||
dataMonitor.getInSpeed(),
|
||||
dataMonitor.getOutPPS(),
|
||||
dataMonitor.getInPPS(),
|
||||
dataMonitor.getOutTraffic(),
|
||||
dataMonitor.getInTraffic(),
|
||||
onlineDeviceCount,
|
||||
backplaneDelayNanos,
|
||||
backplanePacketsPerSecond,
|
||||
backplanePacketsMax,
|
||||
backplaneEcnRate,
|
||||
backplaneLossRate,
|
||||
controller.getClock().toString2(),
|
||||
clockFrequency,
|
||||
(clockFrequency - 1000000000L) / 1000.0);
|
||||
}
|
||||
|
||||
public String getNodeAddress() {
|
||||
return nodeAddress;
|
||||
}
|
||||
|
||||
public int getLinkCount() {
|
||||
return linkCount;
|
||||
}
|
||||
|
||||
public int getActiveLinkCount() {
|
||||
return activeLinkCount;
|
||||
}
|
||||
|
||||
public long getUploadBytesPerSecond() {
|
||||
return uploadBytesPerSecond;
|
||||
}
|
||||
|
||||
public long getDownloadBytesPerSecond() {
|
||||
return downloadBytesPerSecond;
|
||||
}
|
||||
|
||||
public long getUploadPacketsPerSecond() {
|
||||
return uploadPacketsPerSecond;
|
||||
}
|
||||
|
||||
public long getDownloadPacketsPerSecond() {
|
||||
return downloadPacketsPerSecond;
|
||||
}
|
||||
|
||||
public long getUploadBytes() {
|
||||
return uploadBytes;
|
||||
}
|
||||
|
||||
public long getDownloadBytes() {
|
||||
return downloadBytes;
|
||||
}
|
||||
|
||||
public long getUploadSpeedMax() {
|
||||
return uploadSpeedMax;
|
||||
}
|
||||
|
||||
public long getDownloadSpeedMax() {
|
||||
return downloadSpeedMax;
|
||||
}
|
||||
|
||||
public long getUploadPacketsMax() {
|
||||
return uploadPacketsMax;
|
||||
}
|
||||
|
||||
public long getDownloadPacketsMax() {
|
||||
return downloadPacketsMax;
|
||||
}
|
||||
|
||||
public long getDataUploadBytesPerSecond() {
|
||||
return dataUploadBytesPerSecond;
|
||||
}
|
||||
|
||||
public long getDataDownloadBytesPerSecond() {
|
||||
return dataDownloadBytesPerSecond;
|
||||
}
|
||||
|
||||
public long getDataUploadPacketsPerSecond() {
|
||||
return dataUploadPacketsPerSecond;
|
||||
}
|
||||
|
||||
public long getDataDownloadPacketsPerSecond() {
|
||||
return dataDownloadPacketsPerSecond;
|
||||
}
|
||||
|
||||
public long getDataUploadBytes() {
|
||||
return dataUploadBytes;
|
||||
}
|
||||
|
||||
public long getDataDownloadBytes() {
|
||||
return dataDownloadBytes;
|
||||
}
|
||||
|
||||
public long getOnlineDeviceCount() {
|
||||
return onlineDeviceCount;
|
||||
}
|
||||
|
||||
public long getBackplaneDelayNanos() {
|
||||
return backplaneDelayNanos;
|
||||
}
|
||||
|
||||
public long getBackplanePacketsPerSecond() {
|
||||
return backplanePacketsPerSecond;
|
||||
}
|
||||
|
||||
public long getBackplanePacketsMax() {
|
||||
return backplanePacketsMax;
|
||||
}
|
||||
|
||||
public double getBackplaneEcnRate() {
|
||||
return backplaneEcnRate;
|
||||
}
|
||||
|
||||
public double getBackplaneLossRate() {
|
||||
return backplaneLossRate;
|
||||
}
|
||||
|
||||
public String getClockTime() {
|
||||
return clockTime;
|
||||
}
|
||||
|
||||
public long getClockFrequency() {
|
||||
return clockFrequency;
|
||||
}
|
||||
|
||||
public double getClockFrequencyOffsetPpm() {
|
||||
return clockFrequencyOffsetPpm;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
.fx-console {
|
||||
-fx-font-family: "Microsoft YaHei UI", "Noto Sans CJK SC", sans-serif;
|
||||
-fx-background-color: #f4f6f8;
|
||||
-fx-text-fill: #17212b;
|
||||
-fx-focus-color: #167d78;
|
||||
-fx-faint-focus-color: rgba(22, 125, 120, .18);
|
||||
}
|
||||
.topbar { -fx-background-color: #ffffff; -fx-border-color: #dfe5e8; -fx-border-width: 0 0 1 0; }
|
||||
.product-mark { -fx-font-size: 20px; -fx-font-weight: bold; -fx-text-fill: #123b43; }
|
||||
.product-hint { -fx-font-size: 12px; -fx-text-fill: #6d7b83; }
|
||||
.page-title { -fx-font-size: 16px; -fx-font-weight: bold; -fx-text-fill: #17212b; }
|
||||
.status-dot { -fx-font-size: 12px; -fx-text-fill: #bd7b18; }
|
||||
.status-label { -fx-font-size: 13px; -fx-font-weight: bold; -fx-text-fill: #3e4c54; }
|
||||
.status-detail { -fx-font-size: 12px; -fx-text-fill: #829098; }
|
||||
.sidebar { -fx-background-color: #132c35; }
|
||||
.nav-caption { -fx-font-size: 11px; -fx-font-weight: bold; -fx-text-fill: #86a1a6; }
|
||||
.navigation .button {
|
||||
-fx-background-color: transparent; -fx-background-radius: 4px; -fx-border-radius: 4px;
|
||||
-fx-alignment: CENTER_LEFT; -fx-padding: 0 14px; -fx-font-size: 13px; -fx-text-fill: #c5d3d5;
|
||||
}
|
||||
.navigation .button:hover { -fx-background-color: #1e424c; -fx-text-fill: #ffffff; }
|
||||
.navigation .button:selected { -fx-background-color: #247b78; -fx-text-fill: #ffffff; }
|
||||
.navigation .button:focused { -fx-border-color: #8dd2c7; -fx-border-width: 1px; }
|
||||
.content-stage { -fx-background-color: #f4f6f8; }
|
||||
.empty-state { -fx-padding: 32px; -fx-border-color: #d9e0e3; -fx-border-width: 1px; -fx-border-radius: 5px; -fx-background-color: #ffffff; }
|
||||
.eyebrow { -fx-font-size: 11px; -fx-font-weight: bold; -fx-text-fill: #247b78; }
|
||||
.empty-title { -fx-font-size: 22px; -fx-font-weight: bold; -fx-text-fill: #17212b; }
|
||||
.empty-message { -fx-font-size: 13px; -fx-text-fill: #697980; -fx-line-spacing: 3px; }
|
||||
.loading-state { -fx-border-color: #c6ddd9; }
|
||||
.error-state { -fx-border-color: #e4c9c2; }
|
||||
.footer { -fx-background-color: #ffffff; -fx-border-color: #dfe5e8; -fx-border-width: 1px 0 0 0; }
|
||||
.footer-label { -fx-font-size: 11px; -fx-text-fill: #829098; }
|
||||
.page-body { -fx-max-width: 1120px; }
|
||||
.view-title { -fx-font-size: 22px; -fx-font-weight: bold; -fx-text-fill: #17212b; }
|
||||
.view-subtitle { -fx-font-size: 13px; -fx-text-fill: #697980; }
|
||||
.metric-row, .overview-panels { -fx-hgap: 12px; -fx-vgap: 12px; -fx-max-width: 1120px; }
|
||||
.metric { -fx-background-color: #ffffff; -fx-border-color: #d9e0e3; -fx-border-radius: 5px; -fx-background-radius: 5px; -fx-padding: 18px; -fx-min-height: 92px; -fx-min-width: 160px; -fx-pref-width: 220px; -fx-max-width: 280px; }
|
||||
.metric-label { -fx-font-size: 12px; -fx-text-fill: #697980; }
|
||||
.metric-value { -fx-font-size: 22px; -fx-font-weight: bold; -fx-text-fill: #123b43; }
|
||||
.data-panel { -fx-background-color: #ffffff; -fx-border-color: #d9e0e3; -fx-border-radius: 5px; -fx-background-radius: 5px; -fx-padding: 18px; }
|
||||
.overview-panels .data-panel { -fx-min-width: 280px; -fx-pref-width: 420px; -fx-max-width: 1120px; }
|
||||
.panel-title { -fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #17212b; }
|
||||
.data-line { -fx-padding: 7px 0; -fx-border-color: #edf0f1; -fx-border-width: 0 0 1px 0; }
|
||||
.line-label { -fx-text-fill: #697980; -fx-font-size: 13px; }
|
||||
.line-value { -fx-alignment: CENTER_RIGHT; -fx-text-fill: #17212b; -fx-font-size: 13px; }
|
||||
.address-row { -fx-background-color: #eaf3f2; -fx-background-radius: 4px; -fx-padding: 12px 14px; }
|
||||
.address-row .label { -fx-text-fill: #24565a; -fx-font-size: 13px; }
|
||||
.action-row { -fx-padding: 0 0 4px 0; }
|
||||
.action-row .button, .connection-row .button { -fx-background-color: #ffffff; -fx-border-color: #cfd9dc; -fx-border-radius: 4px; -fx-background-radius: 4px; -fx-text-fill: #24444c; }
|
||||
.action-row .button:hover, .connection-row .button:hover { -fx-background-color: #eaf3f2; -fx-border-color: #247b78; }
|
||||
.action-row .button:focused, .connection-row .button:focused { -fx-border-color: #167d78; -fx-border-width: 2px; }
|
||||
.connection-scroll { -fx-background-color: transparent; -fx-border-color: transparent; }
|
||||
.connection-scroll > .viewport { -fx-background-color: transparent; }
|
||||
.connection-row { -fx-background-color: #ffffff; -fx-border-color: #d9e0e3; -fx-border-radius: 5px; -fx-background-radius: 5px; -fx-padding: 14px; }
|
||||
.row-actions { -fx-alignment: CENTER_RIGHT; }
|
||||
.row-actions .button { -fx-wrap-text: true; -fx-min-height: 28px; }
|
||||
.connection-row .vbox { -fx-min-width: 0; }
|
||||
.row-name { -fx-font-size: 14px; -fx-font-weight: bold; -fx-text-fill: #17212b; }
|
||||
.row-address, .row-metrics { -fx-font-size: 12px; -fx-text-fill: #697980; }
|
||||
.state-online, .state-unstable, .state-offline { -fx-font-size: 11px; -fx-font-weight: bold; -fx-padding: 3px 7px; -fx-background-radius: 3px; }
|
||||
.state-online { -fx-background-color: #dcefe8; -fx-text-fill: #216c55; }
|
||||
.state-offline { -fx-background-color: #f1e4df; -fx-text-fill: #95513d; }
|
||||
.state-unstable { -fx-background-color: #f4ecd6; -fx-text-fill: #87631d; }
|
||||
.empty-message { -fx-font-size: 13px; -fx-text-fill: #697980; }
|
||||
Reference in New Issue
Block a user