mirror of
https://github.com/marcoallegretti/WEFT_OS.git
synced 2026-03-27 01:13:09 +00:00
feat(appd): add WebSocket UI endpoint for Servo shell integration
Implements the weft-appd WebSocket server that allows the system-ui.html page running inside Servo to send requests and receive push notifications without requiring custom SpiderMonkey bindings. ws.rs — WebSocket connection handler: - Accepts a tokio TcpStream, performs WebSocket handshake via tokio-tungstenite accept_async. - Reads JSON Text frames, deserializes as Request (serde_json), calls dispatch(), sends Response as JSON Text. - Subscribes to a broadcast::Receiver<Response> for server-push notifications (APP_READY, etc.); forwards to client via select!. - Handles close frames, partial errors, and lagged broadcast gracefully. main.rs — server changes: - broadcast::channel(16) created at startup; WebSocket handlers subscribe for push delivery. - TcpListener bound on 127.0.0.1:7410 (default) or WEFT_APPD_WS_PORT. - ws_port() / write_ws_port(): port written to XDG_RUNTIME_DIR/weft/appd.wsport for runtime discovery. - WS accept branch added to the main select! loop alongside Unix socket. ipc.rs — Response and AppStateKind now derive Clone (required by broadcast::Sender<Response>). system-ui.html — appd WebSocket client: - appdConnect(): opens ws://127.0.0.1:<port>/appd with exponential backoff reconnect (1s → 16s max). - On open: sends QUERY_RUNNING to populate taskbar with live sessions. - handleAppdMessage(): maps LAUNCH_ACK and RUNNING_APPS to taskbar entries; APP_READY shows a timed notification; APP_STATE::stopped removes the taskbar entry. - WEFT_APPD_WS_PORT window global overrides the default port. New deps: tokio-tungstenite 0.24, futures-util 0.3 (sink+std), serde_json 1.
This commit is contained in:
parent
ad40271d69
commit
7cebac4188
6 changed files with 323 additions and 10 deletions
134
Cargo.lock
generated
134
Cargo.lock
generated
|
|
@ -158,6 +158,12 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "byteorder"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bytes"
|
name = "bytes"
|
||||||
version = "1.11.1"
|
version = "1.11.1"
|
||||||
|
|
@ -343,6 +349,12 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "data-encoding"
|
||||||
|
version = "2.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.10.7"
|
version = "0.10.7"
|
||||||
|
|
@ -488,6 +500,12 @@ version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-sink"
|
||||||
|
version = "0.3.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
|
|
@ -501,6 +519,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"slab",
|
"slab",
|
||||||
|
|
@ -550,6 +569,17 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
|
|
@ -619,6 +649,22 @@ version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http"
|
||||||
|
version = "1.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httparse"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "id-arena"
|
name = "id-arena"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
|
|
@ -1325,14 +1371,35 @@ version = "6.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.8.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"rand_chacha 0.3.1",
|
||||||
|
"rand_core 0.6.4",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_chacha",
|
"rand_chacha 0.9.0",
|
||||||
"rand_core",
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1342,7 +1409,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ppv-lite86",
|
"ppv-lite86",
|
||||||
"rand_core",
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.6.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.2.17",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1528,6 +1604,17 @@ dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sha1"
|
||||||
|
version = "0.10.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"cpufeatures",
|
||||||
|
"digest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.9"
|
version = "0.10.9"
|
||||||
|
|
@ -1604,7 +1691,7 @@ dependencies = [
|
||||||
"libseat",
|
"libseat",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"profiling",
|
"profiling",
|
||||||
"rand",
|
"rand 0.9.2",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"sha2",
|
"sha2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
|
@ -1798,6 +1885,18 @@ dependencies = [
|
||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-tungstenite"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"log",
|
||||||
|
"tokio",
|
||||||
|
"tungstenite",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.9.12+spec-1.1.0"
|
version = "0.9.12+spec-1.1.0"
|
||||||
|
|
@ -1920,6 +2019,24 @@ dependencies = [
|
||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tungstenite"
|
||||||
|
version = "0.24.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
|
||||||
|
dependencies = [
|
||||||
|
"byteorder",
|
||||||
|
"bytes",
|
||||||
|
"data-encoding",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
"rand 0.8.5",
|
||||||
|
"sha1",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"utf-8",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
|
|
@ -1956,6 +2073,12 @@ version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf-8"
|
||||||
|
version = "0.7.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
|
|
@ -2275,10 +2398,13 @@ name = "weft-appd"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"futures-util",
|
||||||
"rmp-serde",
|
"rmp-serde",
|
||||||
"sd-notify",
|
"sd-notify",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-tungstenite",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,8 @@ sd-notify = "0.4"
|
||||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "signal", "sync"] }
|
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net", "io-util", "signal", "sync"] }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
rmp-serde = "1"
|
rmp-serde = "1"
|
||||||
|
serde_json = "1"
|
||||||
|
tokio-tungstenite = "0.24"
|
||||||
|
futures-util = { version = "0.3", default-features = false, features = ["sink", "std"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ pub enum Request {
|
||||||
QueryAppState { session_id: u64 },
|
QueryAppState { session_id: u64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
|
#[serde(tag = "type", rename_all = "SCREAMING_SNAKE_CASE")]
|
||||||
pub enum Response {
|
pub enum Response {
|
||||||
LaunchAck {
|
LaunchAck {
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,11 @@ use tokio::io::AsyncWriteExt;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
mod ipc;
|
mod ipc;
|
||||||
|
mod ws;
|
||||||
|
|
||||||
use ipc::{AppStateKind, Request, Response};
|
use ipc::{AppStateKind, Request, Response};
|
||||||
|
|
||||||
type Registry = Arc<Mutex<SessionRegistry>>;
|
pub(crate) type Registry = Arc<Mutex<SessionRegistry>>;
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct SessionRegistry {
|
struct SessionRegistry {
|
||||||
|
|
@ -65,9 +66,18 @@ async fn run() -> anyhow::Result<()> {
|
||||||
.with_context(|| format!("bind {}", socket_path.display()))?;
|
.with_context(|| format!("bind {}", socket_path.display()))?;
|
||||||
tracing::info!(path = %socket_path.display(), "IPC socket listening");
|
tracing::info!(path = %socket_path.display(), "IPC socket listening");
|
||||||
|
|
||||||
let _ = sd_notify::notify(false, &[sd_notify::NotifyState::Ready]);
|
|
||||||
|
|
||||||
let registry: Registry = Arc::new(Mutex::new(SessionRegistry::default()));
|
let registry: Registry = Arc::new(Mutex::new(SessionRegistry::default()));
|
||||||
|
let (broadcast_tx, _) = tokio::sync::broadcast::channel::<Response>(16);
|
||||||
|
|
||||||
|
let ws_port = ws_port();
|
||||||
|
let ws_addr: std::net::SocketAddr = format!("127.0.0.1:{ws_port}").parse()?;
|
||||||
|
let ws_listener = tokio::net::TcpListener::bind(ws_addr)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("bind WebSocket {ws_addr}"))?;
|
||||||
|
tracing::info!(addr = %ws_addr, "WebSocket listener ready");
|
||||||
|
write_ws_port(ws_port)?;
|
||||||
|
|
||||||
|
let _ = sd_notify::notify(false, &[sd_notify::NotifyState::Ready]);
|
||||||
|
|
||||||
let mut shutdown = std::pin::pin!(tokio::signal::ctrl_c());
|
let mut shutdown = std::pin::pin!(tokio::signal::ctrl_c());
|
||||||
|
|
||||||
|
|
@ -78,7 +88,17 @@ async fn run() -> anyhow::Result<()> {
|
||||||
let reg = Arc::clone(®istry);
|
let reg = Arc::clone(®istry);
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
if let Err(e) = handle_connection(stream, reg).await {
|
if let Err(e) = handle_connection(stream, reg).await {
|
||||||
tracing::warn!(error = %e, "connection error");
|
tracing::warn!(error = %e, "unix connection error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
result = ws_listener.accept() => {
|
||||||
|
let (stream, _) = result.context("ws accept")?;
|
||||||
|
let reg = Arc::clone(®istry);
|
||||||
|
let rx = broadcast_tx.subscribe();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(e) = ws::handle_ws_connection(stream, reg, rx).await {
|
||||||
|
tracing::warn!(error = %e, "ws connection error");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -93,6 +113,23 @@ async fn run() -> anyhow::Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ws_port() -> u16 {
|
||||||
|
std::env::var("WEFT_APPD_WS_PORT")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.parse().ok())
|
||||||
|
.unwrap_or(7410)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_ws_port(port: u16) -> anyhow::Result<()> {
|
||||||
|
let runtime_dir = std::env::var("XDG_RUNTIME_DIR").context("XDG_RUNTIME_DIR not set")?;
|
||||||
|
let path = PathBuf::from(runtime_dir).join("weft/appd.wsport");
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
std::fs::create_dir_all(parent)?;
|
||||||
|
}
|
||||||
|
std::fs::write(&path, port.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_connection(
|
async fn handle_connection(
|
||||||
stream: tokio::net::UnixStream,
|
stream: tokio::net::UnixStream,
|
||||||
registry: Registry,
|
registry: Registry,
|
||||||
|
|
@ -110,7 +147,7 @@ async fn handle_connection(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn dispatch(req: Request, registry: &Registry) -> Response {
|
pub(crate) async fn dispatch(req: Request, registry: &Registry) -> Response {
|
||||||
match req {
|
match req {
|
||||||
Request::LaunchApp {
|
Request::LaunchApp {
|
||||||
app_id,
|
app_id,
|
||||||
|
|
|
||||||
54
crates/weft-appd/src/ws.rs
Normal file
54
crates/weft-appd/src/ws.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
use futures_util::{SinkExt, StreamExt};
|
||||||
|
use tokio::sync::broadcast;
|
||||||
|
use tokio_tungstenite::{accept_async, tungstenite::Message};
|
||||||
|
|
||||||
|
use crate::{Registry, dispatch, ipc::Request, ipc::Response};
|
||||||
|
|
||||||
|
pub async fn handle_ws_connection(
|
||||||
|
stream: tokio::net::TcpStream,
|
||||||
|
registry: Registry,
|
||||||
|
broadcast_rx: broadcast::Receiver<Response>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let ws_stream = accept_async(stream).await?;
|
||||||
|
let (mut ws_write, mut ws_read) = ws_stream.split();
|
||||||
|
let mut broadcast_rx = broadcast_rx;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
msg = ws_read.next() => {
|
||||||
|
match msg {
|
||||||
|
Some(Ok(Message::Text(text))) => {
|
||||||
|
let req: Request = match serde_json::from_str(&text) {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = %e, "invalid WS request");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tracing::debug!(?req, "ws request");
|
||||||
|
let resp = dispatch(req, ®istry).await;
|
||||||
|
let json = serde_json::to_string(&resp)?;
|
||||||
|
ws_write.send(Message::Text(json)).await?;
|
||||||
|
}
|
||||||
|
Some(Ok(Message::Close(_))) | None => break,
|
||||||
|
Some(Ok(_)) => {}
|
||||||
|
Some(Err(e)) => {
|
||||||
|
tracing::warn!(error = %e, "ws error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
notification = broadcast_rx.recv() => {
|
||||||
|
match notification {
|
||||||
|
Ok(resp) => {
|
||||||
|
let json = serde_json::to_string(&resp)?;
|
||||||
|
ws_write.send(Message::Text(json)).await?;
|
||||||
|
}
|
||||||
|
Err(broadcast::error::RecvError::Lagged(_)) => {}
|
||||||
|
Err(broadcast::error::RecvError::Closed) => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -161,6 +161,99 @@
|
||||||
launcher.setAttribute('hidden', '');
|
launcher.setAttribute('hidden', '');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var APPD_WS_PORT = window.WEFT_APPD_WS_PORT || 7410;
|
||||||
|
var ws = null;
|
||||||
|
var wsReconnectDelay = 1000;
|
||||||
|
|
||||||
|
function appdConnect() {
|
||||||
|
try {
|
||||||
|
ws = new WebSocket('ws://127.0.0.1:' + APPD_WS_PORT + '/appd');
|
||||||
|
} catch (e) {
|
||||||
|
scheduleReconnect();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.addEventListener('open', function () {
|
||||||
|
wsReconnectDelay = 1000;
|
||||||
|
ws.send(JSON.stringify({ type: 'QUERY_RUNNING' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('message', function (ev) {
|
||||||
|
var msg;
|
||||||
|
try { msg = JSON.parse(ev.data); } catch (e) { return; }
|
||||||
|
handleAppdMessage(msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('close', function () {
|
||||||
|
ws = null;
|
||||||
|
scheduleReconnect();
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addEventListener('error', function () {
|
||||||
|
ws = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleReconnect() {
|
||||||
|
setTimeout(appdConnect, wsReconnectDelay);
|
||||||
|
wsReconnectDelay = Math.min(wsReconnectDelay * 2, 16000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAppdMessage(msg) {
|
||||||
|
if (msg.type === 'APP_READY') {
|
||||||
|
showNotification('App ready (session ' + msg.session_id + ')');
|
||||||
|
} else if (msg.type === 'APP_STATE') {
|
||||||
|
if (msg.state === 'stopped') {
|
||||||
|
removeTaskbarEntry(msg.session_id);
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'RUNNING_APPS') {
|
||||||
|
msg.session_ids.forEach(function (id) {
|
||||||
|
ensureTaskbarEntry(id);
|
||||||
|
});
|
||||||
|
} else if (msg.type === 'LAUNCH_ACK') {
|
||||||
|
ensureTaskbarEntry(msg.session_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureTaskbarEntry(sessionId) {
|
||||||
|
var id = 'task-' + sessionId;
|
||||||
|
if (document.getElementById(id)) { return; }
|
||||||
|
var el = document.createElement('weft-taskbar-app');
|
||||||
|
el.id = id;
|
||||||
|
el.dataset.sessionId = sessionId;
|
||||||
|
el.textContent = '● ' + sessionId;
|
||||||
|
el.style.cssText = 'font-size:12px;color:var(--text-secondary);padding:0 6px;cursor:pointer;';
|
||||||
|
el.title = 'Session ' + sessionId;
|
||||||
|
var taskbar = document.querySelector('weft-taskbar');
|
||||||
|
var clock = document.getElementById('clock');
|
||||||
|
taskbar.insertBefore(el, clock);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTaskbarEntry(sessionId) {
|
||||||
|
var el = document.getElementById('task-' + sessionId);
|
||||||
|
if (el) { el.remove(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification(text) {
|
||||||
|
var center = document.getElementById('notifications');
|
||||||
|
var note = document.createElement('div');
|
||||||
|
note.textContent = text;
|
||||||
|
note.style.cssText = [
|
||||||
|
'background:var(--surface-bg)',
|
||||||
|
'border:1px solid var(--surface-border)',
|
||||||
|
'border-radius:8px',
|
||||||
|
'padding:10px 14px',
|
||||||
|
'font-size:13px',
|
||||||
|
'color:var(--text-primary)',
|
||||||
|
'pointer-events:auto',
|
||||||
|
'backdrop-filter:blur(20px)',
|
||||||
|
].join(';');
|
||||||
|
center.appendChild(note);
|
||||||
|
setTimeout(function () { note.remove(); }, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
|
appdConnect();
|
||||||
}());
|
}());
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue