pub mod actions;
use crate::console_debug;
use crate::llm::action_helper::call_llm;
use crate::llm::actions::protocol_trait::ActionResult;
use crate::llm::ollama_client::OllamaClient;
use crate::protocol::Event;
use crate::server::connection::ConnectionId;
use crate::state::app_state::AppState;
use actions::{ZookeeperProtocol, ZOOKEEPER_REQUEST_EVENT};
use anyhow::Result;
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::mpsc;
const CONNECT_REQUEST_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
const MIN_IDLE_AFTER_CONNECT: std::time::Duration = std::time::Duration::from_secs(30);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"";
fn idle_timeout_for(session: &ZookeeperSession) -> std::time::Duration {
std::time::Duration::from_millis(session.timeout_ms.max(0) as u64 * 2)
.max(MIN_IDLE_AFTER_CONNECT)
}
use tracing::{debug, error, info, trace, warn};
const ZK_SYSTEM_ERROR: i64 = -1;
pub struct ZookeeperServer {
llm_client: OllamaClient,
#[allow(dead_code)]
app_state: Arc<AppState>,
#[allow(dead_code)]
status_tx: mpsc::UnboundedSender<String>,
server_id: Option<crate::state::ServerId>,
}
impl ZookeeperServer {
pub fn new(
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: Option<crate::state::ServerId>,
) -> Self {
Self {
llm_client,
app_state,
status_tx,
server_id,
}
}
pub async fn spawn_with_llm_actions(
listen_addr: SocketAddr,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
_send_first: bool,
server_id: crate::state::ServerId,
) -> Result<SocketAddr> {
let listener = TcpListener::bind(listen_addr).await?;
let actual_addr = listener.local_addr()?;
info!("ZooKeeper server starting on {}", actual_addr);
let _ = status_tx.send(format!(
"[INFO] ZooKeeper server listening on {}",
actual_addr
));
let server = Arc::new(ZookeeperServer::new(
llm_client,
app_state.clone(),
status_tx.clone(),
Some(server_id),
));
let status_tx_clone = status_tx.clone();
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"ZooKeeper",
Some(&status_tx),
)
.await
{
Ok((stream, addr, permit)) => {
console_debug!(status_tx, "ZooKeeper connection from {}", addr);
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
let server_clone = Arc::clone(&server);
let status_tx_conn = status_tx_clone.clone();
let app_state_conn = app_state.clone();
let server_id_opt = server.server_id;
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
let _permit = permit;
if let Err(e) = Self::handle_connection(
stream,
server_clone,
status_tx_conn,
connection_id,
app_state_conn,
server_id_opt,
)
.await
{
error!("ZooKeeper connection error: {}", e);
}
})
.await;
}
Err(e) => {
error!("ZooKeeper accept error, stopping accept loop: {}", e);
let _ = status_tx_clone.send(format!(
"[ERROR] ZooKeeper accept failed, listener stopped: {}",
e
));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(actual_addr)
}
async fn handle_connection(
stream: TcpStream,
server: Arc<ZookeeperServer>,
status_tx: mpsc::UnboundedSender<String>,
connection_id: ConnectionId,
app_state: Arc<AppState>,
server_id: Option<crate::state::ServerId>,
) -> Result<()> {
let peer_addr = stream.peer_addr().ok();
let local_addr = stream.local_addr().ok();
if let (Some(server_id), Some(remote_addr)) = (server_id, peer_addr) {
let now = crate::utils::clock::Instant::now();
app_state
.add_connection_to_server(
server_id,
crate::state::server::ConnectionState {
id: connection_id,
remote_addr,
local_addr: local_addr.unwrap_or(remote_addr),
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: crate::state::server::ConnectionStatus::Active,
status_changed_at: now,
protocol_info: crate::state::server::ProtocolConnectionInfo::empty(),
},
)
.await;
}
let (read_half, write_half) = tokio::io::split(stream);
let write_half = Arc::new(tokio::sync::Mutex::new(write_half));
if let Some(server_id) = server_id {
let peer_rx = crate::server::peer_support::register_peer_channel(
&app_state,
server_id,
connection_id.as_u32(),
)
.await;
Self::spawn_peer_command_task(
peer_rx,
app_state.clone(),
server_id,
connection_id,
write_half.clone(),
status_tx.clone(),
)
.await;
}
let _ = status_tx.send("__UPDATE_UI__".to_string());
let result = Self::run_connection(
read_half,
&write_half,
server,
connection_id,
app_state.clone(),
server_id,
)
.await;
if let Some(server_id) = server_id {
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
app_state
.close_connection_on_server(server_id, connection_id)
.await;
}
let _ = status_tx.send("__UPDATE_UI__".to_string());
result
}
async fn spawn_peer_command_task<W>(
mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
app_state: Arc<AppState>,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
write_half: Arc<tokio::sync::Mutex<W>>,
status_tx: mpsc::UnboundedSender<String>,
) where
W: AsyncWriteExt + Unpin + Send + 'static,
{
use crate::state::client_handles::ClientSendOutcome;
use crate::state::AccessLogOwner;
let protocol: Arc<ZookeeperProtocol> = Arc::new(ZookeeperProtocol::new());
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
while let Some(command) = command_rx.recv().await {
let action = command.action.clone();
let outcome = Self::execute_injected_action(
protocol.as_ref(),
&app_state,
server_id,
connection_id,
&write_half,
&action,
)
.await;
let outcome_json = match &outcome {
Ok(outcome) => {
serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null)
}
Err(e) => serde_json::json!({"error": e.to_string()}),
};
app_state
.record_access_log(
AccessLogOwner::Server(server_id.as_u32()),
"zookeeper",
Some(connection_id.as_u32()),
"injected_action",
action,
vec![outcome_json],
)
.await;
match &outcome {
Err(e) => warn!(
"injected action on ZooKeeper server #{} connection {} failed: {}",
server_id.as_u32(),
connection_id,
e
),
Ok(ClientSendOutcome::Disconnected) => {
app_state
.remove_peer_handle(server_id, connection_id.as_u32())
.await;
app_state
.close_connection_on_server(server_id, connection_id)
.await;
}
Ok(_) => {}
}
let _ = status_tx.send("__UPDATE_UI__".to_string());
let _ = command.reply_tx.send(outcome);
}
debug!(
"peer command task for ZooKeeper server #{} connection {} ended",
server_id.as_u32(),
connection_id
);
})
.await;
}
async fn execute_injected_action<W>(
protocol: &ZookeeperProtocol,
app_state: &Arc<AppState>,
server_id: crate::state::ServerId,
connection_id: ConnectionId,
write_half: &Arc<tokio::sync::Mutex<W>>,
action: &serde_json::Value,
) -> Result<crate::state::client_handles::ClientSendOutcome>
where
W: AsyncWriteExt + Unpin,
{
use crate::state::client_handles::ClientSendOutcome;
let result = crate::llm::actions::executor::execute_actions(
vec![action.clone()],
app_state,
Some(protocol as &dyn crate::llm::actions::protocol_trait::Server),
Some(server_id),
None,
)
.await?;
let mut bytes_sent = 0usize;
let mut closed = false;
let mut details: Vec<String> = Vec::new();
let mut stack: Vec<ActionResult> = result.protocol_results;
stack.reverse();
while let Some(item) = stack.pop() {
match item {
ActionResult::Custom { name, data } if name == "zookeeper_response" => {
let body = Self::custom_reply_body(&data, INJECTED_DEFAULT_XID);
bytes_sent += Self::write_frame(
write_half,
app_state,
Some(server_id),
connection_id,
&body,
)
.await?;
}
ActionResult::Output(bytes) => {
let mut write = write_half.lock().await;
write.write_all(&bytes).await?;
write.flush().await?;
drop(write);
Self::count_written(app_state, Some(server_id), connection_id, bytes.len())
.await;
bytes_sent += bytes.len();
}
ActionResult::Multiple(items) => {
for inner in items.into_iter().rev() {
stack.push(inner);
}
}
ActionResult::CloseConnection => {
let mut write = write_half.lock().await;
write.shutdown().await?;
closed = true;
}
ActionResult::WaitForMore | ActionResult::NoAction => {}
other => details.push(format!("{other:?}")),
}
}
if closed {
Ok(ClientSendOutcome::Disconnected)
} else if bytes_sent > 0 {
Ok(ClientSendOutcome::Sent { bytes_sent })
} else if details.is_empty() {
Ok(ClientSendOutcome::Executed {
detail: "executed (nothing to write)".to_string(),
})
} else {
Ok(ClientSendOutcome::Executed {
detail: crate::utils::truncate_for_log(&details.join("; "), 160),
})
}
}
async fn run_connection<R, W>(
mut read_half: R,
write_half: &Arc<tokio::sync::Mutex<W>>,
server: Arc<ZookeeperServer>,
connection_id: ConnectionId,
app_state: Arc<AppState>,
server_id: Option<crate::state::ServerId>,
) -> Result<()>
where
R: AsyncReadExt + Unpin,
W: AsyncWriteExt + Unpin,
{
debug!("ZooKeeper connection {} established", connection_id);
let mut session: Option<ZookeeperSession> = None;
loop {
let read_timeout = match session.as_ref() {
Some(session) => idle_timeout_for(session),
None => CONNECT_REQUEST_READ_TIMEOUT,
};
let mut len_buf = [0u8; 4];
let header_read = match tokio::time::timeout(
read_timeout,
read_half.read_exact(&mut len_buf),
)
.await
{
Ok(read) => read,
Err(_) => {
debug!(
"ZooKeeper connection {} sent nothing for {}s; closing idle \
connection",
connection_id,
read_timeout.as_secs()
);
break;
}
};
match header_read {
Ok(_) => {}
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
debug!("ZooKeeper client disconnected");
break;
}
Err(e) => {
return Err(e.into());
}
}
let declared_len = i32::from_be_bytes(len_buf);
if declared_len < MIN_REQUEST_BYTES || declared_len > MAX_REQUEST_BYTES {
return Err(anyhow::anyhow!(
"ZooKeeper request length out of range: {} (expected {}..={})",
declared_len,
MIN_REQUEST_BYTES,
MAX_REQUEST_BYTES
));
}
let len = declared_len as usize;
let mut payload = vec![0u8; len];
match tokio::time::timeout(read_timeout, read_half.read_exact(&mut payload)).await {
Ok(read) => read?,
Err(_) => {
debug!(
"ZooKeeper connection {} announced {} octets and stalled for {}s \
without delivering them; closing",
connection_id,
len,
read_timeout.as_secs()
);
break;
}
};
if let Some(server_id) = server_id {
app_state
.update_connection_stats(
server_id,
connection_id,
Some((4 + len) as u64),
None,
Some(1),
None,
)
.await;
}
trace!(
"ZooKeeper connection {} received {} bytes",
connection_id,
len
);
if session.is_none() {
let connect = match Self::parse_connect_request(&payload) {
Ok(connect) => connect,
Err(e) => {
warn!(
"ZooKeeper connection {}: first frame is not a ConnectRequest ({}); \
closing",
connection_id, e
);
return Err(e);
}
};
let established = ZookeeperSession::negotiate(&connect, connection_id);
Self::write_frame(
write_half,
&app_state,
server_id,
connection_id,
&established.connect_response(),
)
.await?;
info!(
"ZooKeeper connection {} session established: id={:#018x} timeout={}ms \
(client asked {}ms) read_only={}",
connection_id,
established.session_id,
established.timeout_ms,
connect.timeout_ms,
established.read_only,
);
session = Some(established);
continue;
}
let request_info = Self::parse_request(&payload)?;
match request_info.op_code {
OP_PING => {
trace!("ZooKeeper connection {} ping", connection_id);
Self::write_frame(
write_half,
&app_state,
server_id,
connection_id,
&Self::reply(request_info.xid, 0, 0, &[]),
)
.await?;
continue;
}
OP_CLOSE_SESSION => {
debug!(
"ZooKeeper connection {} closed its session on request",
connection_id
);
Self::write_frame(
write_half,
&app_state,
server_id,
connection_id,
&Self::reply(request_info.xid, 0, 0, &[]),
)
.await?;
return Ok(());
}
_ => {}
}
let event = Event::new(
&ZOOKEEPER_REQUEST_EVENT,
serde_json::json!({
"xid": request_info.xid,
"operation": request_info.operation,
"op_code": request_info.op_code,
"path": request_info.path,
}),
);
let server_id = server_id.unwrap_or_else(|| crate::state::ServerId::new(0));
let protocol = Arc::new(ZookeeperProtocol::new());
match call_llm(
&server.llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution_result) => {
let mut answered = false;
for result in execution_result.protocol_results {
match result {
ActionResult::Custom { name, data } if name == "zookeeper_response" => {
answered = true;
let error_code = data
.get("error_code")
.and_then(|v| v.as_i64())
.unwrap_or(ZK_SYSTEM_ERROR);
let response = Self::custom_reply_body(&data, request_info.xid);
let sent = Self::write_frame(
write_half,
&app_state,
Some(server_id),
connection_id,
&response,
)
.await?;
if error_code != 0 {
debug!(
"ZooKeeper connection {} decision=model_reject \
operation={} xid={} error_code={}",
connection_id,
request_info.operation,
request_info.xid,
error_code
);
}
trace!(
"ZooKeeper sent {} bytes to connection {}",
sent,
connection_id,
);
}
ActionResult::CloseConnection => {
debug!("ZooKeeper closing connection {}", connection_id);
return Ok(());
}
_ => {}
}
}
if !answered {
warn!(
"ZooKeeper connection {} decision=fail_closed_no_answer operation={} \
xid={}: handler produced no zookeeper_response; replying \
ZSYSTEMERROR",
connection_id, request_info.operation, request_info.xid
);
let _ = server.status_tx.send(format!(
"[WARN] ZooKeeper {} on connection {} unanswered by handler; \
replying ZSYSTEMERROR",
request_info.operation, connection_id
));
Self::write_frame(
write_half,
&app_state,
Some(server_id),
connection_id,
&Self::reply(request_info.xid, 0, ZSYSTEMERROR, &[]),
)
.await?;
}
}
Err(e) => {
let failure = crate::utils::WireFailure::classify(&e);
let (code, class) = match failure {
crate::utils::WireFailure::Overloaded => {
(ZOPERATIONTIMEOUT, "fail_closed_overloaded")
}
crate::utils::WireFailure::Unavailable => {
(ZSYSTEMERROR, "fail_closed_llm_error")
}
};
error!(
"ZooKeeper connection {} decision={} operation={} xid={} \
reply_error_code={}: LLM error: {}",
connection_id, class, request_info.operation, request_info.xid, code, e
);
let _ = server.status_tx.send(format!(
"[ERROR] ZooKeeper {} on connection {} failed ({}): {}",
request_info.operation,
connection_id,
class,
crate::utils::truncate_for_log(&e.to_string(), 200)
));
Self::write_frame(
write_half,
&app_state,
Some(server_id),
connection_id,
&Self::reply(request_info.xid, 0, code, &[]),
)
.await?;
}
}
}
Ok(())
}
fn custom_reply_body(data: &serde_json::Value, default_xid: i32) -> Vec<u8> {
let xid = data
.get("xid")
.and_then(|v| v.as_i64())
.map(|v| v as i32)
.unwrap_or(default_xid);
let zxid = data.get("zxid").and_then(|v| v.as_i64()).unwrap_or(0);
let error_code = data
.get("error_code")
.and_then(|v| v.as_i64())
.and_then(|v| i32::try_from(v).ok())
.unwrap_or(ZK_SYSTEM_ERROR as i32);
let body = data
.get("body_hex")
.and_then(|v| v.as_str())
.map(hex::decode)
.transpose()
.unwrap_or_default()
.unwrap_or_default();
Self::reply(xid, zxid, error_code, &body)
}
async fn write_frame<W: AsyncWriteExt + Unpin>(
write_half: &Arc<tokio::sync::Mutex<W>>,
app_state: &Arc<AppState>,
server_id: Option<crate::state::ServerId>,
connection_id: ConnectionId,
body: &[u8],
) -> Result<usize> {
{
let mut write = write_half.lock().await;
write.write_all(&(body.len() as i32).to_be_bytes()).await?;
write.write_all(body).await?;
write.flush().await?;
}
let sent = 4 + body.len();
Self::count_written(app_state, server_id, connection_id, sent).await;
Ok(sent)
}
async fn count_written(
app_state: &Arc<AppState>,
server_id: Option<crate::state::ServerId>,
connection_id: ConnectionId,
bytes: usize,
) {
if let Some(server_id) = server_id {
app_state
.update_connection_stats(
server_id,
connection_id,
None,
Some(bytes as u64),
None,
Some(1),
)
.await;
}
}
fn reply(xid: i32, zxid: i64, error_code: i32, body: &[u8]) -> Vec<u8> {
let mut response = Vec::with_capacity(16 + body.len());
response.extend_from_slice(&xid.to_be_bytes());
response.extend_from_slice(&zxid.to_be_bytes());
response.extend_from_slice(&error_code.to_be_bytes());
response.extend_from_slice(body);
response
}
fn parse_connect_request(payload: &[u8]) -> Result<ZookeeperConnectRequest> {
if payload.len() < CONNECT_REQUEST_LEN {
return Err(anyhow::anyhow!(
"ConnectRequest too short: {} bytes (expected at least {})",
payload.len(),
CONNECT_REQUEST_LEN
));
}
if payload.len() > CONNECT_REQUEST_LEN + 1 {
return Err(anyhow::anyhow!(
"ConnectRequest too long: {} bytes (expected {} or {})",
payload.len(),
CONNECT_REQUEST_LEN,
CONNECT_REQUEST_LEN + 1
));
}
let protocol_version = i32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
if protocol_version != 0 {
return Err(anyhow::anyhow!(
"unsupported ZooKeeper protocol version {}",
protocol_version
));
}
let last_zxid_seen = i64::from_be_bytes([
payload[4],
payload[5],
payload[6],
payload[7],
payload[8],
payload[9],
payload[10],
payload[11],
]);
let timeout_ms = i32::from_be_bytes([payload[12], payload[13], payload[14], payload[15]]);
let session_id = i64::from_be_bytes([
payload[16],
payload[17],
payload[18],
payload[19],
payload[20],
payload[21],
payload[22],
payload[23],
]);
let passwd_len = i32::from_be_bytes([payload[24], payload[25], payload[26], payload[27]]);
if passwd_len != SESSION_PASSWD_LEN as i32 {
return Err(anyhow::anyhow!(
"ConnectRequest password length is {} (expected {})",
passwd_len,
SESSION_PASSWD_LEN
));
}
let passwd = payload[28..CONNECT_REQUEST_LEN].to_vec();
let read_only = payload.get(CONNECT_REQUEST_LEN).map(|b| *b != 0);
Ok(ZookeeperConnectRequest {
last_zxid_seen,
timeout_ms,
session_id,
passwd,
read_only,
})
}
fn parse_request(payload: &[u8]) -> Result<ZookeeperRequest> {
if payload.len() < 8 {
return Ok(ZookeeperRequest {
xid: 0,
op_code: 0,
operation: "unknown".to_string(),
path: String::new(),
});
}
let xid = i32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
let op_type = i32::from_be_bytes([payload[4], payload[5], payload[6], payload[7]]);
let operation = match op_type {
OP_CLOSE_SESSION => "closeSession",
1 => "create",
2 => "delete",
3 => "exists",
4 => "getData",
5 => "setData",
6 => "getACL",
7 => "setACL",
8 => "getChildren",
9 => "sync",
11 => "ping",
12 => "getChildren2",
13 => "check",
14 => "multi",
_ => "unknown",
};
let path = if payload.len() >= 12 {
let declared = i32::from_be_bytes([payload[8], payload[9], payload[10], payload[11]]);
let available = payload.len() - 12;
if declared > 0 && (declared as usize) <= available {
String::from_utf8_lossy(&payload[12..12 + declared as usize]).to_string()
} else {
String::new()
}
} else {
String::new()
};
Ok(ZookeeperRequest {
xid,
op_code: op_type,
operation: operation.to_string(),
path,
})
}
}
const MIN_REQUEST_BYTES: i32 = 8;
pub const MAX_REQUEST_BYTES: i32 = 1024 * 1024;
const INJECTED_DEFAULT_XID: i32 = -1;
const ZSYSTEMERROR: i32 = -1;
const ZOPERATIONTIMEOUT: i32 = -7;
const OP_PING: i32 = 11;
const OP_CLOSE_SESSION: i32 = -11;
const CONNECT_REQUEST_LEN: usize = 44;
const SESSION_PASSWD_LEN: usize = 16;
const MIN_SESSION_TIMEOUT_MS: i32 = 4_000;
const MAX_SESSION_TIMEOUT_MS: i32 = 40_000;
struct ZookeeperRequest {
xid: i32,
op_code: i32,
operation: String,
path: String,
}
struct ZookeeperConnectRequest {
#[allow(dead_code)]
last_zxid_seen: i64,
timeout_ms: i32,
session_id: i64,
passwd: Vec<u8>,
read_only: Option<bool>,
}
struct ZookeeperSession {
session_id: i64,
timeout_ms: i32,
passwd: Vec<u8>,
read_only: bool,
echo_read_only: bool,
}
impl ZookeeperSession {
fn negotiate(req: &ZookeeperConnectRequest, connection_id: ConnectionId) -> Self {
let timeout_ms = if req.timeout_ms <= 0 {
MIN_SESSION_TIMEOUT_MS
} else {
req.timeout_ms
.clamp(MIN_SESSION_TIMEOUT_MS, MAX_SESSION_TIMEOUT_MS)
};
let (session_id, passwd) = if req.session_id != 0 {
(req.session_id, req.passwd.clone())
} else {
let id = Self::mint_session_id(connection_id);
(id, Self::derive_passwd(id))
};
Self {
session_id,
timeout_ms,
passwd,
read_only: false,
echo_read_only: req.read_only.is_some(),
}
}
fn mint_session_id(connection_id: ConnectionId) -> i64 {
let nanos = crate::utils::clock::SystemTime::now()
.duration_since(crate::utils::clock::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0);
let id = ((nanos << 16) ^ u64::from(connection_id.as_u32())) & 0x7fff_ffff_ffff_ffff;
if id == 0 {
1
} else {
id as i64
}
}
fn derive_passwd(session_id: i64) -> Vec<u8> {
let base = session_id.to_be_bytes();
let mut passwd = Vec::with_capacity(SESSION_PASSWD_LEN);
passwd.extend_from_slice(&base);
passwd.extend(base.iter().map(|b| b ^ 0x5a));
passwd
}
fn connect_response(&self) -> Vec<u8> {
let mut body = Vec::with_capacity(4 + 4 + 8 + 4 + SESSION_PASSWD_LEN + 1);
body.extend_from_slice(&0i32.to_be_bytes()); body.extend_from_slice(&self.timeout_ms.to_be_bytes());
body.extend_from_slice(&self.session_id.to_be_bytes());
body.extend_from_slice(&(self.passwd.len() as i32).to_be_bytes());
body.extend_from_slice(&self.passwd);
if self.echo_read_only {
body.push(self.read_only as u8);
}
body
}
}