fix per code review

This commit is contained in:
lda
2026-07-16 22:58:00 +07:00 Verified
parent 6f7363dc0f
commit 2ab320ff17
6 changed files with 93 additions and 66 deletions
+4 -2
View File
@@ -169,8 +169,10 @@ export function TerminalPage({
const selectedAgentSessionCount = sessions.filter( const selectedAgentSessionCount = sessions.filter(
(item) => item.agent_id === selectedAgentId, (item) => item.agent_id === selectedAgentId,
).length; ).length;
const selectedAgentSessionLimit = const selectedAgentSessionLimit = Math.max(
selectedAgent?.capability_options?.terminal?.max_sessions ?? 2; 1,
selectedAgent?.capability_options?.terminal?.max_sessions ?? 2,
);
const agentAtSessionLimit = const agentAtSessionLimit =
selectedAgentSessionCount >= selectedAgentSessionLimit; selectedAgentSessionCount >= selectedAgentSessionLimit;
const canRequestStart = const canRequestStart =
+5 -9
View File
@@ -4,7 +4,10 @@ use std::fmt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::LazyLock; use std::sync::LazyLock;
use crate::protocol::{DEFAULT_TERMINAL_MAX_SESSIONS, DEFAULT_TERMINAL_SESSION_TTL_SECONDS}; use crate::protocol::{
DEFAULT_TERMINAL_MAX_SESSIONS, DEFAULT_TERMINAL_SESSION_TTL_SECONDS,
checked_terminal_session_ttl,
};
pub const DEFAULT_CONFIG_PATH: &str = "/etc/wakey-agent/config.toml"; pub const DEFAULT_CONFIG_PATH: &str = "/etc/wakey-agent/config.toml";
pub const DEFAULT_PID_FILE: &str = "/var/run/wakey-agent.pid"; pub const DEFAULT_PID_FILE: &str = "/var/run/wakey-agent.pid";
@@ -158,14 +161,7 @@ const fn default_terminal_session_ttl_seconds() -> u64 {
impl TerminalConfig { impl TerminalConfig {
pub(crate) fn session_ttl(&self) -> Result<Option<std::time::Duration>> { pub(crate) fn session_ttl(&self) -> Result<Option<std::time::Duration>> {
if self.session_ttl_seconds == 0 { checked_terminal_session_ttl(self.session_ttl_seconds).map_err(anyhow::Error::msg)
return Ok(None);
}
let ttl = std::time::Duration::from_secs(self.session_ttl_seconds);
std::time::Instant::now()
.checked_add(ttl)
.context("terminal.session_ttl_seconds is too large for the platform timer")?;
Ok(Some(ttl))
} }
} }
+16
View File
@@ -2,6 +2,7 @@ use macaddr::MacAddr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
use std::net::IpAddr; use std::net::IpAddr;
use std::time::{Duration, Instant};
use wakey_core::parse::mac; use wakey_core::parse::mac;
use wakey_core::{ use wakey_core::{
Device, DeviceInventory, DhcpLeaseWithState, InterfaceSummary, InventoryQuery, Device, DeviceInventory, DhcpLeaseWithState, InterfaceSummary, InventoryQuery,
@@ -68,6 +69,21 @@ pub enum AgentCapability {
pub const DEFAULT_TERMINAL_MAX_SESSIONS: usize = 2; pub const DEFAULT_TERMINAL_MAX_SESSIONS: usize = 2;
pub const DEFAULT_TERMINAL_SESSION_TTL_SECONDS: u64 = 12 * 60 * 60; pub const DEFAULT_TERMINAL_SESSION_TTL_SECONDS: u64 = 12 * 60 * 60;
/// Validates a terminal TTL against the platform's monotonic timer range.
/// Zero is the explicit unlimited policy.
pub fn checked_terminal_session_ttl(
session_ttl_seconds: u64,
) -> Result<Option<Duration>, &'static str> {
if session_ttl_seconds == 0 {
return Ok(None);
}
let ttl = Duration::from_secs(session_ttl_seconds);
Instant::now()
.checked_add(ttl)
.ok_or("terminal session TTL is too large for the platform timer")?;
Ok(Some(ttl))
}
const fn default_terminal_session_ttl_seconds() -> u64 { const fn default_terminal_session_ttl_seconds() -> u64 {
DEFAULT_TERMINAL_SESSION_TTL_SECONDS DEFAULT_TERMINAL_SESSION_TTL_SECONDS
} }
+15 -15
View File
@@ -153,7 +153,7 @@ pub async fn get_terminal(
State(state): State<AppState>, State(state): State<AppState>,
Path(terminal_id): Path<String>, Path(terminal_id): Path<String>,
) -> Result<Json<TerminalSessionResponse>, ApiError> { ) -> Result<Json<TerminalSessionResponse>, ApiError> {
let (agent_id, created_at_unix, agent_attached, operator_attached, expires_at_unix) = state let summary = state
.terminals .terminals
.summary(&terminal_id) .summary(&terminal_id)
.await .await
@@ -161,11 +161,11 @@ pub async fn get_terminal(
Ok(Json(TerminalSessionResponse { Ok(Json(TerminalSessionResponse {
websocket_url: operator_ws_path(&terminal_id), websocket_url: operator_ws_path(&terminal_id),
terminal_id, terminal_id,
agent_id, agent_id: summary.agent_id,
created_at_unix, created_at_unix: summary.created_at_unix,
expires_at_unix, expires_at_unix: summary.expires_at_unix,
agent_attached, agent_attached: summary.agent_attached,
operator_attached, operator_attached: summary.operator_attached,
attachment_token: None, attachment_token: None,
})) }))
} }
@@ -192,7 +192,7 @@ pub async fn attach_terminal(
.issue_attachment_token_for_operator(&terminal_id, &request.operator_id) .issue_attachment_token_for_operator(&terminal_id, &request.operator_id)
.await .await
.map_err(registry_error)?; .map_err(registry_error)?;
let (agent_id, created_at_unix, agent_attached, operator_attached, expires_at_unix) = state let summary = state
.terminals .terminals
.summary(&terminal_id) .summary(&terminal_id)
.await .await
@@ -200,11 +200,11 @@ pub async fn attach_terminal(
Ok(Json(TerminalSessionResponse { Ok(Json(TerminalSessionResponse {
websocket_url: operator_ws_path(&terminal_id), websocket_url: operator_ws_path(&terminal_id),
terminal_id, terminal_id,
agent_id, agent_id: summary.agent_id,
created_at_unix, created_at_unix: summary.created_at_unix,
expires_at_unix, expires_at_unix: summary.expires_at_unix,
agent_attached, agent_attached: summary.agent_attached,
operator_attached, operator_attached: summary.operator_attached,
attachment_token: Some(attachment_token), attachment_token: Some(attachment_token),
})) }))
} }
@@ -365,10 +365,10 @@ async fn handle_operator_terminal_socket(
warn!(terminal_id, code, "failed to request terminal snapshot"); warn!(terminal_id, code, "failed to request terminal snapshot");
} }
let summary = state.terminals.summary(&terminal_id).await; let summary = state.terminals.summary(&terminal_id).await;
if let Some((agent_id, _, _, _, _)) = &summary { if let Some(summary) = &summary {
append_terminal_audit( append_terminal_audit(
&state, &state,
agent_id, &summary.agent_id,
&terminal_id, &terminal_id,
TerminalAudit { TerminalAudit {
actor_type: "admin_api", actor_type: "admin_api",
@@ -382,7 +382,7 @@ async fn handle_operator_terminal_socket(
} }
let (mut write, mut read) = socket.split(); let (mut write, mut read) = socket.split();
if summary.is_some_and(|(_, _, agent_attached, _, _)| agent_attached) { if summary.is_some_and(|summary| summary.agent_attached) {
let ready = serde_json::to_string(&TerminalControl::Ready) let ready = serde_json::to_string(&TerminalControl::Ready)
.expect("terminal ready control serializes"); .expect("terminal ready control serializes");
if send_relay_frame(&mut write, TerminalRelayFrame::Text(ready)) if send_relay_frame(&mut write, TerminalRelayFrame::Text(ready))
+50 -31
View File
@@ -448,21 +448,12 @@ impl TerminalRegistry {
Some(detached_at) Some(detached_at)
} }
pub async fn summary( pub async fn summary(&self, terminal_id: &str) -> Option<TerminalSummary> {
&self,
terminal_id: &str,
) -> Option<(String, u64, bool, bool, Option<u64>)> {
let mut sessions = self.inner.lock().await; let mut sessions = self.inner.lock().await;
prune_expired_sessions(&mut sessions); prune_expired_sessions(&mut sessions);
sessions.get(terminal_id).map(|session| { sessions
( .get(terminal_id)
session.agent_id.clone(), .map(|session| terminal_summary(terminal_id, session))
session.created_at_unix,
session.agent_tx.is_some(),
session.operator_tx.is_some(),
session.expiry.unix(),
)
})
} }
pub async fn summaries(&self) -> Vec<TerminalSummary> { pub async fn summaries(&self) -> Vec<TerminalSummary> {
@@ -470,20 +461,24 @@ impl TerminalRegistry {
prune_expired_sessions(&mut sessions); prune_expired_sessions(&mut sessions);
let mut summaries = sessions let mut summaries = sessions
.iter() .iter()
.map(|(terminal_id, session)| TerminalSummary { .map(|(terminal_id, session)| terminal_summary(terminal_id, session))
terminal_id: terminal_id.clone(),
agent_id: session.agent_id.clone(),
created_at_unix: session.created_at_unix,
expires_at_unix: session.expiry.unix(),
agent_attached: session.agent_tx.is_some(),
operator_attached: session.operator_tx.is_some(),
})
.collect::<Vec<_>>(); .collect::<Vec<_>>();
summaries.sort_by_key(|session| std::cmp::Reverse(session.created_at_unix)); summaries.sort_by_key(|session| std::cmp::Reverse(session.created_at_unix));
summaries summaries
} }
} }
fn terminal_summary(terminal_id: &str, session: &TerminalSession) -> TerminalSummary {
TerminalSummary {
terminal_id: terminal_id.to_string(),
agent_id: session.agent_id.clone(),
created_at_unix: session.created_at_unix,
expires_at_unix: session.expiry.unix(),
agent_attached: session.agent_tx.is_some(),
operator_attached: session.operator_tx.is_some(),
}
}
fn active_session<'a>( fn active_session<'a>(
sessions: &'a mut HashMap<String, TerminalSession>, sessions: &'a mut HashMap<String, TerminalSession>,
terminal_id: &str, terminal_id: &str,
@@ -739,7 +734,7 @@ mod tests {
.summary(&unlimited.terminal_id) .summary(&unlimited.terminal_id)
.await .await
.expect("unlimited summary") .expect("unlimited summary")
.4, .expires_at_unix,
None None
); );
} }
@@ -763,7 +758,7 @@ mod tests {
.summary(terminal_id.as_str()) .summary(terminal_id.as_str())
.await .await
.expect("adopted unlimited session") .expect("adopted unlimited session")
.4, .expires_at_unix,
None None
); );
} }
@@ -805,9 +800,9 @@ mod tests {
.summary(terminal_id.as_str()) .summary(terminal_id.as_str())
.await .await
.expect("adopted summary"); .expect("adopted summary");
assert_eq!(summary.0, "router"); assert_eq!(summary.agent_id, "router");
assert_eq!(summary.1, created_at_unix); assert_eq!(summary.created_at_unix, created_at_unix);
assert!(summary.2); assert!(summary.agent_attached);
} }
#[tokio::test] #[tokio::test]
@@ -869,7 +864,7 @@ mod tests {
.summary(&created.terminal_id) .summary(&created.terminal_id)
.await .await
.expect("session remains after detach"); .expect("session remains after detach");
assert!(!summary.3); assert!(!summary.operator_attached);
registry registry
.issue_attachment_token_for_operator(&created.terminal_id, OPERATOR_A) .issue_attachment_token_for_operator(&created.terminal_id, OPERATOR_A)
.await .await
@@ -895,7 +890,13 @@ mod tests {
.await .await
.expect("handoff token"); .expect("handoff token");
assert!(!registry.summary(&previous.terminal_id).await.unwrap().3); assert!(
!registry
.summary(&previous.terminal_id)
.await
.unwrap()
.operator_attached
);
registry registry
.attach_operator(&next.terminal_id, &next_token, OPERATOR_A) .attach_operator(&next.terminal_id, &next_token, OPERATOR_A)
.await .await
@@ -930,8 +931,20 @@ mod tests {
.expect_err("occupied target remains protected"); .expect_err("occupied target remains protected");
assert_eq!(error, "terminal_operator_already_attached"); assert_eq!(error, "terminal_operator_already_attached");
assert!(registry.summary(&previous.terminal_id).await.unwrap().3); assert!(
assert!(registry.summary(&occupied.terminal_id).await.unwrap().3); registry
.summary(&previous.terminal_id)
.await
.unwrap()
.operator_attached
);
assert!(
registry
.summary(&occupied.terminal_id)
.await
.unwrap()
.operator_attached
);
} }
#[tokio::test] #[tokio::test]
@@ -959,7 +972,13 @@ mod tests {
.await .await
.is_none() .is_none()
); );
assert!(registry.summary(&created.terminal_id).await.unwrap().3); assert!(
registry
.summary(&created.terminal_id)
.await
.unwrap()
.operator_attached
);
} }
#[tokio::test] #[tokio::test]
+3 -9
View File
@@ -4,14 +4,14 @@ use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade};
use axum::response::IntoResponse; use axum::response::IntoResponse;
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt, StreamExt};
use serde::Deserialize; use serde::Deserialize;
use std::time::{Duration, Instant}; use std::time::Instant;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tracing::{debug, info, info_span, warn}; use tracing::{debug, info, info_span, warn};
use uuid::Uuid; use uuid::Uuid;
use wakey_agent::protocol::{ use wakey_agent::protocol::{
AgentCapability, AgentCapabilityOptions, AgentTerminalSession, DEFAULT_TERMINAL_MAX_SESSIONS, AgentCapability, AgentCapabilityOptions, AgentTerminalSession, DEFAULT_TERMINAL_MAX_SESSIONS,
DEFAULT_TERMINAL_SESSION_TTL_SECONDS, ErrorPayload, RequestId, ServerMessage, DEFAULT_TERMINAL_SESSION_TTL_SECONDS, ErrorPayload, RequestId, ServerMessage,
TerminalCapabilityOptions, TerminalControl, TerminalId, TerminalCapabilityOptions, TerminalControl, TerminalId, checked_terminal_session_ttl,
}; };
use wakey_core::Device; use wakey_core::Device;
@@ -225,13 +225,7 @@ async fn process_agent_text(
.as_ref() .as_ref()
.map(|terminal| terminal.session_ttl_seconds) .map(|terminal| terminal.session_ttl_seconds)
.unwrap_or(DEFAULT_TERMINAL_SESSION_TTL_SECONDS); .unwrap_or(DEFAULT_TERMINAL_SESSION_TTL_SECONDS);
if session_ttl_seconds != 0 checked_terminal_session_ttl(session_ttl_seconds).map_err(anyhow::Error::msg)?;
&& Instant::now()
.checked_add(Duration::from_secs(session_ttl_seconds))
.is_none()
{
anyhow::bail!("terminal session TTL is too large for the platform timer");
}
Some(TerminalCapabilityOptions { Some(TerminalCapabilityOptions {
max_sessions, max_sessions,
session_ttl_seconds, session_ttl_seconds,