use crate::server::AppState;
use axum::extract::ws::{Message, WebSocket};
use axum::extract::{State, WebSocketUpgrade};
use axum::response::IntoResponse;
use futures::{SinkExt, StreamExt};
use harness_proto::{ClientMessage, Hello, ServerMessage, UserInfo, Welcome, PROTOCOL_VERSION};
use std::collections::HashSet;
use tracing::{debug, info, warn};
pub async fn handler(ws: WebSocketUpgrade, State(st): State<AppState>) -> impl IntoResponse {
ws.on_upgrade(move |socket| connection(socket, st))
}
async fn send(
sink: &mut futures::stream::SplitSink<WebSocket, Message>,
msg: &ServerMessage,
) -> anyhow::Result<()> {
let text = serde_json::to_string(msg)?;
sink.send(Message::Text(text)).await?;
Ok(())
}
async fn connection(socket: WebSocket, st: AppState) {
let (mut sink, mut stream) = socket.split();
let hello: Hello = match stream.next().await {
Some(Ok(Message::Text(t))) => match serde_json::from_str::<ClientMessage>(&t) {
Ok(ClientMessage::Hello(h)) => h,
_ => {
let _ = send(
&mut sink,
&ServerMessage::Error {
code: "bad_handshake".into(),
message: "first frame must be `hello`".into(),
},
)
.await;
return;
}
},
_ => return,
};
if hello.protocol > PROTOCOL_VERSION || hello.protocol + 1 < PROTOCOL_VERSION {
let _ = send(
&mut sink,
&ServerMessage::Error {
code: "unsupported_protocol".into(),
message: format!("daemon speaks protocol {PROTOCOL_VERSION}"),
},
)
.await;
return;
}
if !st.accepts_token(hello.device_token.as_deref()) {
let _ = send(
&mut sink,
&ServerMessage::Error {
code: "unauthorized".into(),
message: "device token rejected".into(),
},
)
.await;
return;
}
info!(client = %hello.client, "client connected");
let welcome = ServerMessage::Welcome(Welcome {
protocol: PROTOCOL_VERSION,
daemon: format!("harnessd/{}", st.version),
capabilities: vec!["multi_provider".into(), "event_log".into()],
user: UserInfo {
name: st.runtime.config().user_name.clone(),
},
});
if send(&mut sink, &welcome).await.is_err() {
return;
}
let device = hello.client.clone();
let mut bus = st.runtime.subscribe();
let mut subscribed: HashSet<String> = HashSet::new();
loop {
tokio::select! {
ev = bus.recv() => {
match ev {
Ok(env) => {
if subscribed.contains(&env.session)
&& send(&mut sink, &ServerMessage::Event(env)).await.is_err()
{
break;
}
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => {
warn!(skipped = n, "client lagged the event bus");
}
Err(_) => break,
}
}
frame = stream.next() => {
match frame {
Some(Ok(Message::Text(t))) => {
match serde_json::from_str::<ClientMessage>(&t) {
Ok(msg) => {
if handle_client_msg(&st, &mut sink, &mut subscribed, &device, msg).await.is_err() {
break;
}
}
Err(e) => {
debug!(error = %e, "ignoring unparseable client frame");
}
}
}
Some(Ok(Message::Close(_))) | None => break,
Some(Ok(_)) => {} Some(Err(e)) => { debug!(error = %e, "ws error"); break; }
}
}
}
}
info!(client = %hello.client, "client disconnected");
}
async fn handle_client_msg(
st: &AppState,
sink: &mut futures::stream::SplitSink<WebSocket, Message>,
subscribed: &mut HashSet<String>,
device: &str,
msg: ClientMessage,
) -> anyhow::Result<()> {
match msg {
ClientMessage::Hello(_) => { }
ClientMessage::Subscribe { session, from_seq } => {
match st.runtime.store().events_since(&session, from_seq) {
Ok(events) => {
for env in events {
send(sink, &ServerMessage::Event(env)).await?;
}
subscribed.insert(session);
send(sink, &ServerMessage::Ack { ok: true }).await?;
}
Err(e) => {
send(
sink,
&ServerMessage::Error {
code: e.code().into(),
message: e.to_string(),
},
)
.await?;
}
}
}
ClientMessage::Unsubscribe { session } => {
subscribed.remove(&session);
}
ClientMessage::SendMessage { session, content } => {
let rt = st.runtime.clone();
tokio::spawn(async move {
if let Err(e) = rt.handle_user_message(&session, &content).await {
warn!(error = %e, %session, "turn failed");
}
});
send(sink, &ServerMessage::Ack { ok: true }).await?;
}
ClientMessage::Approve {
session,
call_id,
always,
} => {
let ok = st
.runtime
.approve(&session, &call_id, device, always)
.is_ok();
send(sink, &ServerMessage::Ack { ok }).await?;
}
ClientMessage::Deny {
session,
call_id,
note,
} => {
let ok = st.runtime.deny(&session, &call_id, device, note).is_ok();
send(sink, &ServerMessage::Ack { ok }).await?;
}
ClientMessage::CancelTurn { session } => {
let ok = st.runtime.cancel_turn(&session).is_ok();
send(sink, &ServerMessage::Ack { ok }).await?;
}
ClientMessage::ResolvePlan {
session,
plan_id,
approve,
note,
} => {
let ok = st
.runtime
.resolve_plan(&session, &plan_id, approve, device, note)
.is_ok();
send(sink, &ServerMessage::Ack { ok }).await?;
}
ClientMessage::SetAutoApprove { session, enabled } => {
st.runtime.set_auto_approve(&session, enabled);
send(sink, &ServerMessage::Ack { ok: true }).await?;
}
}
Ok(())
}