8000 feat(minimald): implement RenameSession RPC by twitchyliquid64 · Pull Request #436 · gominimal/minimal · GitHub
[go: up one dir, main page]

Skip to content
Merged
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
20 changes: 20 additions & 0 deletions crates/minimald-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -190,3 +190,23 @@ impl OneshotSshRpc for CreateSession {
type Request<'a> = CreateSessionRequest;
type Response = Errorable<CreateSessionResponse>;
}

/// An RPC to rename an existing session.
pub struct RenameSession;

/// The request for a [`RenameSession`] RPC.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RenameSessionRequest {
pub id: SessionId,
pub new_name: String,
}

/// The response for a [`RenameSession`] RPC.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RenameSessionResponse;

impl OneshotSshRpc for RenameSession {
const NAME: &'static str = constcat::concat!(RPC_SUBSYSTEM_PREFIX, "RenameSession");
type Request<'a> = RenameSessionRequest;
type Response = Errorable<RenameSessionResponse>;
}
85 changes: 83 additions & 2 deletions crates/minimald/src/rpc.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use minimald_rpc::{
CreateSession, CreateSessionResponse, Errorable, GetSessionRecord, GetSessionRecordRequest,
GetSessionRecordResponse, GetVersion, GetVersionResponse, ListSessions, ListSessionsEntry,
ListSessionsResponse, OneshotSshRpc, RPC_SUBSYSTEM_PREFIX,
ListSessionsResponse, OneshotSshRpc, RPC_SUBSYSTEM_PREFIX, RenameSession,
RenameSessionResponse,
};
use russh::{
Channel as RuChannel, ChannelId,
Expand Down Expand Up @@ -151,6 +152,27 @@ async fn serve_create_session(s: ServerStateHandle, c: RuChannel<Msg>) {
}
}

async fn serve_rename_session(s: ServerStateHandle, c: RuChannel<Msg>) {
let res = RenameSession
.handle_channel(c, async |req| {
let res = s
.sessions_manager()
.await
.rename_session(req.id, req.new_name)
.await;
match res {
Ok(()) => Ok(Errorable::Ok(RenameSessionResponse)),
Err(e) => Ok(Errorable::Err {
error: e.to_string(),
}),
}
})
.await;
if let Err(e) = res {
tracing::warn!("RPC handler for {} failed: {}", RenameSession::NAME, e);
}
}

pub(crate) const STREAM_WORKSPACE_FILES: &str =
constcat::concat!(RPC_SUBSYSTEM_PREFIX, "WorkspaceFilesTarZst");

Expand Down Expand Up @@ -223,6 +245,7 @@ pub async fn handle_ssh_rpc(
| ListSessions::NAME
| GetSessionRecord::NAME
| CreateSession::NAME
| RenameSession::NAME
| STREAM_WORKSPACE_FILES => {
let mut conn_lock = c.lock().await;
let c_hnd = match conn_lock.take(id) {
Expand Down Expand Up @@ -252,6 +275,7 @@ pub async fn handle_ssh_rpc(
ListSessions::NAME => spawn(serve_list_sessions(s, channel)),
GetSessionRecord::NAME => spawn(serve_get_session_record(s, channel)),
CreateSession::NAME => spawn(serve_create_session(s, channel)),
RenameSession::NAME => spawn(serve_rename_session(s, channel)),
STREAM_WORKSPACE_FILES => spawn(serve_stream_workspace_files(s, config, channel)),
_ => unreachable!(),
};
Expand All @@ -261,7 +285,7 @@ pub async fn handle_ssh_rpc(

#[cfg(test)]
mod tests {
use minimald_rpc::{CreateSession, CreateSessionRequest};
use minimald_rpc::{CreateSession, CreateSessionRequest, RenameSessionRequest};
use paths::HostAbsPath;
use sessions::SessionId;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
Expand Down Expand Up @@ -531,4 +555,61 @@ mod tests {
}
);
}

#[tokio::test]
async fn rename_session_propagates_into_the_running_session() {
let server = TestServer::new().await;
let mut client = server.connect().await;
let session_id = fresh_session(&mut client).await;

// Bring the session up before renaming, so the rename has to reach the
// live actor's in-memory record rather than only touching disk.
let mngr = server.state.sessions_manager().await;
let handle = mngr
.get_session(SessionKeyPredicate::Id(session_id))
.await
.unwrap()
.expect("freshly-created session should be retrievable");

let resp = client
.call::<RenameSession>(&RenameSessionRequest {
id: session_id,
new_name: "renamed".to_string(),
})
.await;
assert_eq!(resp, Errorable::Ok(RenameSessionResponse));

// The record held by the running session reflects the new name...
let record = handle.record().await;
assert_eq!(record.name.as_deref(), Some("renamed"));
// ...while its id is untouched by the rename.
assert_eq!(record.id, session_id);

// The rename is reflected by GetRecord...
let get_session = client
.call::<GetSessionRecord>(&GetSessionRecordRequest::Id(record.id))
.await;
assert_eq!(
get_session.record.as_ref().unwrap().name,
Some("renamed".to_string())
);
}

#[tokio::test]
async fn rename_session_errors_for_unknown_id() {
let server = TestServer::new().await;
let mut client = server.connect().await;

let resp = client
.call::<RenameSession>(&RenameSessionRequest {
id: SessionId::nil(),
new_name: "renamed".to_string(),
})
.await;

assert!(
matches!(resp, Errorable::Err { error } if error.contains("no session with ID")),
"expected an unknown-id error",
);
}
}
33 changes: 32 additions & 1 deletion crates/minimald/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use crate::{
use mctx::ConfigBuilder;
use paths::DaemonAbsPath;
use russh::{Channel, server::Msg};
use sessions::store::SessionObject;
use sessions::{Record, store::SessionObject};
use std::fmt::{self};
use tokio::sync::{mpsc, oneshot};

Expand Down Expand Up @@ -41,6 +41,9 @@ enum SessionMessage {
ChannelConfig,
),
GetHostAttrs(oneshot::Sender<Option<HostAttrs>>),
RefreshRecord(Record),
#[cfg(test)]
GetRecord(oneshot::Sender<Record>),
}

/// Manages a running session.
Expand Down Expand Up @@ -86,6 +89,7 @@ impl<S: SessionObject> Session<S> {
self.handle_message(msg).await;
}
}

/// Handles a specific message recieved by the session.
async fn handle_message(&mut self, msg: SessionMessage) {
match msg {
Expand All @@ -105,6 +109,13 @@ impl<S: SessionObject> Session<S> {
Some(h) => h.get_attrs().await.ok(),
});
}
SessionMessage::RefreshRecord(r) => {
self.session.refresh_from_record(r);
}
#[cfg(test)]
SessionMessage::GetRecord(r) => {
let _ = r.send(self.session.record().clone());
}
}
}

Expand Down Expand Up @@ -213,6 +224,26 @@ impl SessionHandle {
recv.await.expect("corresponding session is dead")
}

/// Returns the session record currently held by the live session actor.
///
/// This reads the actor's in-memory copy, not the on-disk record, so
/// tests can assert that updates have propagated into the running session.
#[cfg(test)]
pub(crate) async fn record(&self) -> Record {
let (send, recv) = oneshot::channel();
// Ignore send errors - the recv will also fail.
let _ = self.0.send(SessionMessage::GetRecord(send)).await;
recv.await.expect("corresponding session is dead")
}

/// Tell the session that the underlying session record was updated.
pub(crate) async fn apply_record(&self, new_record: Record) {
self.0
.send(SessionMessage::RefreshRecord(new_record))
.await
.expect("corresponding session is dead");
}

pub async fn attach(
&self,
channel: Channel<Msg>,
Expand Down
37 changes: 36 additions & 1 deletion crates/minimald/src/sessions.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::collections::BTreeMap;
use std::{collections::BTreeMap, io::ErrorKind::NotFound};

use crate::{
session::{Session, SessionHandle},
Expand Down Expand Up @@ -53,6 +53,7 @@ enum ManagerMessage {
GetRecord(SessionKeyPredicate, Responder<Option<sessions::Record>>),
GetSession(SessionKeyPredicate, Responder<Option<SessionHandle>>),
CreateSession(sessions::Record, Responder<SessionId>),
RenameSession(SessionId, String, Responder<()>),
}

/// Manages session instances, and session state on disk.
Expand Down Expand Up @@ -185,6 +186,25 @@ impl<L: Loader> Manager<L> {
})
.await;
}
// Renames an existing session with the given ID.
ManagerMessage::RenameSession(id, new_name, r) => {
r.handle(async {
match self.store.find_by_id(&id)? {
None => Err(std::io::Error::new(
NotFound,
format!("no session with ID `{}`", id.as_ref()),
)),
Some(k) => {
self.store.rename(&k, new_name.clone())?;
if let Some(hnd) = self.running.get(&k) {
hnd.apply_record(self.store.get(&k)?.record().clone()).await;
}
Ok(())
}
}
})
.await
}
}
}
}
Expand Down Expand Up @@ -237,4 +257,19 @@ impl ManagerHandle {
let _ = self.0.send(ManagerMessage::GetSession(pred, send)).await;
recv.await.expect("corresponding sessions manager is dead")
}

/// Renames the given session to the given name.
pub async fn rename_session(
&self,
id: SessionId,
new_name: String,
) -> Result<(), SessionsError> {
let (send, recv) = Responder::channel();
// Ignore send errors - the recv will also fail.
let _ = self
.0
.send(ManagerMessage::RenameSession(id, new_name, send))
.await;
recv.await.expect("corresponding sessions manager is dead")
}
}
Loading
0