Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions crates/cli/src/learn.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ yas terminal attach "$ID" # drive it from here; Ctrl-] detaches
yas quit # shut down the server
```

A disconnected CLI exits with `yas: disconnected by the YAS server: REASON`
when the disconnecting client gave a reason.

Terminals persist until closed or the daemon exits. Clean up when done.

`attach` needs a real tty on stdin and repaints the remote grid in the
Expand Down
44 changes: 34 additions & 10 deletions crates/cli/src/yas_native.rs
Original file line number Diff line number Diff line change
Expand Up @@ -989,11 +989,7 @@ impl NativeClient {
&& frame.header.kind == yas_wire::core::event_kind::GOAWAY
{
let goaway = GoAway::decode(&frame.payload).map_err(wire_error)?;
return Err(format!(
"YAS server is closing with {:?}: {}",
goaway.status,
format_result_detail(&goaway.detail)
));
return Err(goaway_message(&goaway));
}
if frame.header.class == Class::Event
&& frame.header.family == family::CORE
Expand Down Expand Up @@ -1198,11 +1194,7 @@ impl NativeFrameReader {
&& frame.header.kind == yas_wire::core::event_kind::GOAWAY
{
let goaway = GoAway::decode(&frame.payload).map_err(wire_error)?;
return Err(format!(
"YAS server is closing with {:?}: {}",
goaway.status,
format_result_detail(&goaway.detail)
));
return Err(goaway_message(&goaway));
}
if frame.header.class == Class::Event
&& frame.header.family == family::CORE
Expand Down Expand Up @@ -1388,6 +1380,17 @@ fn wire_error(error: yas_wire::Error) -> String {
format!("YAS wire error: {error}")
}

fn goaway_message(goaway: &GoAway) -> String {
match goaway.reason() {
Some(reason) => format!("disconnected by the YAS server: {reason}"),
None => format!(
"YAS server is closing with {:?}: {}",
goaway.status,
format_result_detail(&goaway.detail)
),
}
}

pub(crate) fn format_result_detail(detail: &Extensions) -> String {
if detail.0.is_empty() {
"no detail".to_string()
Expand All @@ -1411,6 +1414,27 @@ pub(crate) fn format_result_detail(detail: &Extensions) -> String {
#[cfg(test)]
mod tests {
use super::*;

#[test]
fn goaway_with_a_disconnect_reason_names_it() {
let kicked = GoAway {
status: yas_wire::core::Status::Ok,
close_deadline_server_ns: 0,
detail: GoAway::reason_detail("Removed by an administrator"),
};
assert_eq!(
goaway_message(&kicked),
"disconnected by the YAS server: Removed by an administrator"
);
let closing = GoAway {
detail: Extensions::default(),
..kicked
};
assert_eq!(
goaway_message(&closing),
"YAS server is closing with Ok: no detail"
);
}
use yas_wire::core::{FamilyDescriptor, Operation, RuntimeState, SessionUpdate};

fn test_server_hello() -> ServerHello {
Expand Down
2 changes: 1 addition & 1 deletion crates/server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -956,7 +956,7 @@ struct NativeYasClient {
identity: NativeClientIdentity,
origin: ConnectionOrigin,
connected_at: Instant,
disconnect: mpsc::Sender<()>,
disconnect: mpsc::Sender<String>,
inbound_bytes: Arc<AtomicU64>,
outbound_bytes: Arc<AtomicU64>,
active_subscriptions: Arc<NativeYasSubscriptions>,
Expand Down
14 changes: 7 additions & 7 deletions crates/server/src/yas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2052,7 +2052,7 @@ async fn serve_registered<S>(
)
});
let native_active_subscriptions = Arc::new(NativeYasSubscriptions::default());
let (native_disconnect_tx, mut native_disconnect_rx) = mpsc::channel(1);
let (native_disconnect_tx, mut native_disconnect_rx) = mpsc::channel::<String>(1);
let native_client_registered = registration.is_some() && services.app_state.is_some();
#[cfg(target_os = "linux")]
let mut native_backend_owner = None;
Expand Down Expand Up @@ -2610,10 +2610,10 @@ async fn serve_registered<S>(
None => break 'session "native Channel terminal event channel closed".to_owned(),
},
disconnect = native_disconnect_rx.recv(), if native_client_registered => {
if disconnect.is_none() {
let Some(reason) = disconnect else {
break 'session "native client disconnect channel closed".to_owned();
}
if session.send_client_goaway().await.is_err() {
};
if session.send_client_goaway(&reason).await.is_err() {
break 'session "native client disconnect requested; GOAWAY failed".to_owned();
}
break 'session "native client disconnect requested; GOAWAY sent".to_owned();
Expand Down Expand Up @@ -7301,11 +7301,11 @@ impl Session {
Ok(())
}

async fn send_client_goaway(&self) -> Result<(), ()> {
async fn send_client_goaway(&self, reason: &str) -> Result<(), ()> {
let payload = GoAway {
status: Status::Ok,
close_deadline_server_ns: monotonic_ns(),
detail: Extensions::default(),
detail: GoAway::reason_detail(reason),
}
.encode()
.map_err(|_| ())?;
Expand Down Expand Up @@ -8675,7 +8675,7 @@ impl Session {
self.send_sensitive_result_confirmed(&frame, status, Vec::new())
.await?;
if let Some(disconnect) = target {
let _ = disconnect.send(()).await;
let _ = disconnect.send(request.reason).await;
}
Ok(())
}
Expand Down
44 changes: 44 additions & 0 deletions crates/yas/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1524,6 +1524,30 @@ pub struct GoAway {
pub detail: Extensions,
}

impl GoAway {
/// Detail carrying the UTF-8 reason a Client DISCONNECT gave; empty for
/// an empty reason.
pub fn reason_detail(reason: &str) -> Extensions {
if reason.is_empty() {
return Extensions::default();
}
Extensions(vec![Extension {
tag: crate::schema::core::GOAWAY_REASON_EXTENSION as u16,
required: false,
value: reason.as_bytes().to_vec(),
}])
}

/// The disconnect reason in `detail`, if the server sent one.
pub fn reason(&self) -> Option<String> {
self.detail
.0
.iter()
.find(|extension| extension.tag == crate::schema::core::GOAWAY_REASON_EXTENSION as u16)
.map(|extension| String::from_utf8_lossy(&extension.value).into_owned())
}
}

impl Encode for GoAway {
fn encode_to(&self, out: &mut Vec<u8>) -> Result<()> {
validate_result_or_event_extensions(&self.detail, "required GOAWAY detail extension")?;
Expand Down Expand Up @@ -1678,6 +1702,26 @@ impl Decode for FamilyUpdate {
mod tests {
use super::*;

#[test]
fn goaway_reason_round_trips_as_optional_detail() {
let goaway = GoAway {
status: Status::Ok,
close_deadline_server_ns: 7,
detail: GoAway::reason_detail("Removed by an administrator"),
};
let decoded = GoAway::decode(&goaway.encode().unwrap()).unwrap();
assert_eq!(
decoded.reason().as_deref(),
Some("Removed by an administrator")
);
assert!(GoAway::reason_detail("").0.is_empty());
let silent = GoAway {
detail: Extensions::default(),
..goaway
};
assert_eq!(silent.reason(), None);
}

fn limits() -> ReceiveLimits {
ReceiveLimits {
max_frame: 1024,
Expand Down
4 changes: 3 additions & 1 deletion docs/design/yas.md
Original file line number Diff line number Diff line change
Expand Up @@ -2139,7 +2139,9 @@ present in a selected family descriptor and cannot exceed their canonical hard
maximums.

DISCONNECT names a session ID, operation ID, and UTF-8 reason. Its Result is
queued before the target receives Core GOAWAY. Targeting the caller is valid
queued before the target receives Core GOAWAY. A nonempty reason travels in
that GOAWAY's detail as optional Core extension `GOAWAY_REASON_EXTENSION` (1),
whose value is the reason's UTF-8 bytes. Targeting the caller is valid
and becomes an orderly self-disconnect after the Result is sent. Core SHUTDOWN,
not this family, stops the whole server.

Expand Down
5 changes: 5 additions & 0 deletions js/core/src/yas/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export const YAS_CORE_EXTENSION_SUPPORT_WASMI = 2 as const;
export const YAS_CORE_EXTENSION_SUPPORT_QUICKJS = 4 as const;
export const YAS_CORE_EXTENSION_SUPPORT_COMMAND_PROVIDER = 8 as const;
export const YAS_CORE_SESSION_INFO_SERVER_DIAGNOSTICS_EXTENSION = 1 as const;
export const YAS_CORE_GOAWAY_REASON_EXTENSION = 1 as const;
export const YAS_FAMILY_TRANSFER = 1 as const;
export const YAS_TRANSFER_VERSION = 1 as const;
export const YAS_TRANSFER_BYTE_DATA = 0 as const;
Expand Down Expand Up @@ -3214,6 +3215,10 @@ export const YAS_SCHEMA = {
{
"name": "SESSION_INFO_SERVER_DIAGNOSTICS_EXTENSION",
"value": 1
},
{
"name": "GOAWAY_REASON_EXTENSION",
"value": 1
}
]
},
Expand Down
4 changes: 4 additions & 0 deletions protocol/yas/families/core.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,10 @@ value = 8
name = "SESSION_INFO_SERVER_DIAGNOSTICS_EXTENSION"
value = 1

[[constant]]
name = "GOAWAY_REASON_EXTENSION"
value = 1

[[request]]
name = "HELLO"
kind = 0x0000
Expand Down
2 changes: 2 additions & 0 deletions protocol/yas/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,7 @@ pub const EXTENSION_SUPPORT_WASMI: u64 = 2;
pub const EXTENSION_SUPPORT_QUICKJS: u64 = 4;
pub const EXTENSION_SUPPORT_COMMAND_PROVIDER: u64 = 8;
pub const SESSION_INFO_SERVER_DIAGNOSTICS_EXTENSION: u64 = 1;
pub const GOAWAY_REASON_EXTENSION: u64 = 1;
pub static OPERATIONS: &[super::OperationMetadata] = &[
super::OperationMetadata { name: "HELLO", class: 1, kind: 0, direction: 0, sensitive: 0, compression: 2, datagram: 0, layout: "ClientHello; ResultPrefix + ServerHello" },
super::OperationMetadata { name: "PING", class: 1, kind: 1, direction: 2, sensitive: 0, compression: 0, datagram: 0, layout: "sender_monotonic_ns:u64; ResultPrefix + receiver_receive_ns:u64,receiver_send_ns:u64" },
Expand Down Expand Up @@ -406,6 +407,7 @@ super::ConstantMetadata { name: "EXTENSION_SUPPORT_WASMI", value: 2 },
super::ConstantMetadata { name: "EXTENSION_SUPPORT_QUICKJS", value: 4 },
super::ConstantMetadata { name: "EXTENSION_SUPPORT_COMMAND_PROVIDER", value: 8 },
super::ConstantMetadata { name: "SESSION_INFO_SERVER_DIAGNOSTICS_EXTENSION", value: 1 },
super::ConstantMetadata { name: "GOAWAY_REASON_EXTENSION", value: 1 },
];
}
pub mod transfer {
Expand Down
4 changes: 4 additions & 0 deletions protocol/yas/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@
{
"name": "SESSION_INFO_SERVER_DIAGNOSTICS_EXTENSION",
"value": 1
},
{
"name": "GOAWAY_REASON_EXTENSION",
"value": 1
}
]
},
Expand Down
Loading