commit 4210455995ea516c84cd6788851b1ad8830a7be1
parent 255f5d50707f732590b1813efa247e0a9e896588
Author: triesap <tyson@radroots.org>
Date: Sat, 25 Apr 2026 09:49:55 +0000
signer: add session lifecycle commands
Diffstat:
10 files changed, 972 insertions(+), 64 deletions(-)
diff --git a/src/cli.rs b/src/cli.rs
@@ -535,6 +535,7 @@ impl Command {
},
Self::Signer(signer) => match signer.command {
SignerCommand::Status => "signer status",
+ SignerCommand::Session(_) => "signer session",
},
Self::Status => "status",
Self::Sync(sync) => match sync.command {
@@ -698,6 +699,43 @@ pub struct SignerArgs {
#[derive(Debug, Clone, Subcommand)]
pub enum SignerCommand {
Status,
+ Session(SignerSessionArgs),
+}
+
+#[derive(Debug, Clone, Args)]
+pub struct SignerSessionArgs {
+ #[command(subcommand)]
+ pub command: SignerSessionCommand,
+}
+
+#[derive(Debug, Clone, Subcommand)]
+pub enum SignerSessionCommand {
+ List,
+ Show {
+ session_id: String,
+ },
+ ConnectBunker {
+ url: String,
+ },
+ ConnectNostrconnect {
+ url: String,
+ #[arg(long)]
+ client_secret_key: String,
+ },
+ PublicKey {
+ session_id: String,
+ },
+ Authorize {
+ session_id: String,
+ },
+ RequireAuth {
+ session_id: String,
+ #[arg(long)]
+ auth_url: String,
+ },
+ Close {
+ session_id: String,
+ },
}
#[derive(Debug, Clone, Args)]
@@ -1232,7 +1270,7 @@ mod tests {
JobCommand, JobWatchArgs, ListingCommand, LocalCommand, LocalExportFormatArg,
MarketCommand, MycCommand, NetCommand, OrderCommand, OrderWatchArgs, OutputFormatArg,
RelayCommand, RpcCommand, RuntimeCommand, RuntimeConfigCommand, SellCommand, SetupRoleArg,
- SignerCommand, SyncCommand, SyncWatchArgs,
+ SignerCommand, SignerSessionCommand, SyncCommand, SyncWatchArgs,
};
use crate::runtime::config::OutputFormat;
#[test]
@@ -1616,6 +1654,36 @@ mod tests {
match parsed.command {
Command::Signer(signer) => match signer.command {
SignerCommand::Status => {}
+ SignerCommand::Session(_) => panic!("unexpected signer session command"),
+ },
+ _ => panic!("unexpected command variant"),
+ }
+ }
+
+ #[test]
+ fn parses_signer_session_lifecycle_commands() {
+ let parsed = CliArgs::parse_from([
+ "radroots",
+ "signer",
+ "session",
+ "require-auth",
+ "sess_123",
+ "--auth-url",
+ "https://auth.example",
+ ]);
+ match parsed.command {
+ Command::Signer(signer) => match signer.command {
+ SignerCommand::Session(session) => match session.command {
+ SignerSessionCommand::RequireAuth {
+ session_id,
+ auth_url,
+ } => {
+ assert_eq!(session_id, "sess_123");
+ assert_eq!(auth_url, "https://auth.example");
+ }
+ _ => panic!("unexpected signer session subcommand"),
+ },
+ SignerCommand::Status => panic!("unexpected signer status command"),
},
_ => panic!("unexpected command variant"),
}
diff --git a/src/commands/mod.rs b/src/commands/mod.rs
@@ -20,7 +20,7 @@ pub mod workflow;
use crate::cli::{
AccountCommand, Command, ConfigCommand, FarmCommand, JobCommand, ListingCommand, LocalCommand,
MarketCommand, MycCommand, NetCommand, OrderCommand, RelayCommand, RpcCommand, RuntimeCommand,
- RuntimeConfigCommand, SellCommand, SignerCommand, SyncCommand,
+ RuntimeConfigCommand, SellCommand, SignerCommand, SignerSessionCommand, SyncCommand,
};
use crate::domain::runtime::{CommandOutput, CommandView};
use crate::runtime::RuntimeError;
@@ -62,6 +62,40 @@ pub fn dispatch(
},
Command::Signer(signer) => match &signer.command {
SignerCommand::Status => Ok(signer::status(config)),
+ SignerCommand::Session(session) => match &session.command {
+ SignerSessionCommand::List => Ok(signer::session_list(config)),
+ SignerSessionCommand::Show { session_id } => {
+ Ok(signer::session_show(config, session_id.as_str()))
+ }
+ SignerSessionCommand::ConnectBunker { url } => {
+ Ok(signer::session_connect_bunker(config, url.as_str()))
+ }
+ SignerSessionCommand::ConnectNostrconnect {
+ url,
+ client_secret_key,
+ } => Ok(signer::session_connect_nostrconnect(
+ config,
+ url.as_str(),
+ client_secret_key.as_str(),
+ )),
+ SignerSessionCommand::PublicKey { session_id } => {
+ Ok(signer::session_public_key(config, session_id.as_str()))
+ }
+ SignerSessionCommand::Authorize { session_id } => {
+ Ok(signer::session_authorize(config, session_id.as_str()))
+ }
+ SignerSessionCommand::RequireAuth {
+ session_id,
+ auth_url,
+ } => Ok(signer::session_require_auth(
+ config,
+ session_id.as_str(),
+ auth_url.as_str(),
+ )),
+ SignerSessionCommand::Close { session_id } => {
+ Ok(signer::session_close(config, session_id.as_str()))
+ }
+ },
},
Command::Doctor => doctor::report(config, logging),
Command::Farm(farm_command) => match &farm_command.command {
diff --git a/src/commands/signer.rs b/src/commands/signer.rs
@@ -1,5 +1,8 @@
-use crate::domain::runtime::{CommandDisposition, CommandOutput, CommandView, SignerStatusView};
+use crate::domain::runtime::{
+ CommandDisposition, CommandOutput, CommandView, SignerSessionActionView, SignerStatusView,
+};
use crate::runtime::config::RuntimeConfig;
+use crate::runtime::daemon::DaemonRpcError;
use crate::runtime::signer::resolve_signer_status;
pub fn status(config: &RuntimeConfig) -> CommandOutput {
@@ -20,3 +23,141 @@ pub fn status(config: &RuntimeConfig) -> CommandOutput {
}
}
}
+
+pub fn session_list(config: &RuntimeConfig) -> CommandOutput {
+ crate::runtime::daemon::signer_sessions(config)
+}
+
+pub fn session_show(config: &RuntimeConfig, session_id: &str) -> CommandOutput {
+ session_action_output(
+ "show",
+ crate::runtime::daemon::signer_session_show(config, session_id),
+ )
+}
+
+pub fn session_connect_bunker(config: &RuntimeConfig, url: &str) -> CommandOutput {
+ session_action_output(
+ "connect_bunker",
+ crate::runtime::daemon::signer_session_connect_bunker(config, url),
+ )
+}
+
+pub fn session_connect_nostrconnect(
+ config: &RuntimeConfig,
+ url: &str,
+ client_secret_key: &str,
+) -> CommandOutput {
+ session_action_output(
+ "connect_nostrconnect",
+ crate::runtime::daemon::signer_session_connect_nostrconnect(config, url, client_secret_key),
+ )
+}
+
+pub fn session_public_key(config: &RuntimeConfig, session_id: &str) -> CommandOutput {
+ session_action_output(
+ "public_key",
+ crate::runtime::daemon::signer_session_public_key(config, session_id),
+ )
+}
+
+pub fn session_authorize(config: &RuntimeConfig, session_id: &str) -> CommandOutput {
+ session_action_output(
+ "authorize",
+ crate::runtime::daemon::signer_session_authorize(config, session_id),
+ )
+}
+
+pub fn session_require_auth(
+ config: &RuntimeConfig,
+ session_id: &str,
+ auth_url: &str,
+) -> CommandOutput {
+ session_action_output(
+ "require_auth",
+ crate::runtime::daemon::signer_session_require_auth(config, session_id, auth_url),
+ )
+}
+
+pub fn session_close(config: &RuntimeConfig, session_id: &str) -> CommandOutput {
+ session_action_output(
+ "close",
+ crate::runtime::daemon::signer_session_close(config, session_id),
+ )
+}
+
+fn session_action_output(
+ action: &str,
+ result: Result<SignerSessionActionView, DaemonRpcError>,
+) -> CommandOutput {
+ match result {
+ Ok(view) => CommandOutput::success(CommandView::SignerSessionAction(view)),
+ Err(error) => {
+ let (disposition, view) = session_action_error_view(action, error);
+ match disposition {
+ CommandDisposition::Unconfigured => {
+ CommandOutput::unconfigured(CommandView::SignerSessionAction(view))
+ }
+ CommandDisposition::ExternalUnavailable => {
+ CommandOutput::external_unavailable(CommandView::SignerSessionAction(view))
+ }
+ CommandDisposition::Unsupported => {
+ CommandOutput::unsupported(CommandView::SignerSessionAction(view))
+ }
+ CommandDisposition::InternalError => {
+ CommandOutput::internal_error(CommandView::SignerSessionAction(view))
+ }
+ CommandDisposition::Success => {
+ CommandOutput::success(CommandView::SignerSessionAction(view))
+ }
+ }
+ }
+ }
+}
+
+fn session_action_error_view(
+ action: &str,
+ error: DaemonRpcError,
+) -> (CommandDisposition, SignerSessionActionView) {
+ let (disposition, state, reason) = match error {
+ DaemonRpcError::Unconfigured(reason)
+ | DaemonRpcError::Unauthorized(reason)
+ | DaemonRpcError::MethodUnavailable(reason) => {
+ (CommandDisposition::Unconfigured, "unconfigured", reason)
+ }
+ DaemonRpcError::External(reason) => (
+ CommandDisposition::ExternalUnavailable,
+ "unavailable",
+ reason,
+ ),
+ DaemonRpcError::InvalidResponse(reason)
+ | DaemonRpcError::Remote(reason)
+ | DaemonRpcError::UnknownJob(reason) => {
+ (CommandDisposition::InternalError, "error", reason)
+ }
+ };
+ (
+ disposition,
+ SignerSessionActionView {
+ action: action.to_owned(),
+ state: state.to_owned(),
+ source: "daemon signer session rpc · durable write plane".to_owned(),
+ session_id: None,
+ mode: None,
+ remote_signer_pubkey: None,
+ client_pubkey: None,
+ signer_pubkey: None,
+ user_pubkey: None,
+ relays: Vec::new(),
+ permissions: Vec::new(),
+ auth_required: None,
+ authorized: None,
+ auth_url: None,
+ expires_in_secs: None,
+ pubkey: None,
+ replayed: None,
+ required: None,
+ closed: None,
+ reason: Some(reason),
+ },
+ )
+}
diff --git a/src/domain/runtime.rs b/src/domain/runtime.rs
@@ -132,6 +132,7 @@ pub enum CommandView {
SellMutation(SellMutationView),
SellShow(SellShowView),
Setup(SetupView),
+ SignerSessionAction(SignerSessionActionView),
SignerStatus(SignerStatusView),
Status(StatusView),
SyncPull(SyncActionView),
@@ -2153,6 +2154,47 @@ pub struct SignerStatusView {
pub myc: Option<MycStatusView>,
}
+#[derive(Debug, Clone, Serialize)]
+pub struct SignerSessionActionView {
+ pub action: String,
+ pub state: String,
+ pub source: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub session_id: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub mode: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub remote_signer_pubkey: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub client_pubkey: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub signer_pubkey: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub user_pubkey: Option<String>,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub relays: Vec<String>,
+ #[serde(default, skip_serializing_if = "Vec::is_empty")]
+ pub permissions: Vec<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub auth_required: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub authorized: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub auth_url: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub expires_in_secs: Option<u64>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub pubkey: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub replayed: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub required: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub closed: Option<bool>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub reason: Option<String>,
+}
+
impl SignerStatusView {
pub fn disposition(&self) -> CommandDisposition {
match self.state.as_str() {
diff --git a/src/render/mod.rs b/src/render/mod.rs
@@ -116,6 +116,9 @@ fn render_human_view_to(
CommandView::RpcStatus(view) => {
render_rpc_status(stdout, view)?;
}
+ CommandView::SignerSessionAction(view) => {
+ render_signer_session_action(stdout, view)?;
+ }
CommandView::ConfigShow(view) => {
render_config_show(stdout, view)?;
}
@@ -485,6 +488,10 @@ fn render_json_to(stdout: &mut dyn Write, output: &CommandOutput) -> Result<(),
serde_json::to_writer_pretty(&mut *stdout, view)?;
writeln!(stdout)?;
}
+ CommandView::SignerSessionAction(view) => {
+ serde_json::to_writer_pretty(&mut *stdout, view)?;
+ writeln!(stdout)?;
+ }
CommandView::SignerStatus(view) => {
serde_json::to_writer_pretty(&mut *stdout, view)?;
writeln!(stdout)?;
@@ -3158,7 +3165,15 @@ fn render_rpc_sessions(stdout: &mut dyn Write, view: &RpcSessionsView) -> Result
}
} else {
let table = Table {
- headers: &["session", "role", "auth", "authorized", "relays", "expires"],
+ headers: &[
+ "session",
+ "role",
+ "user",
+ "auth",
+ "authorized",
+ "relays",
+ "expires",
+ ],
rows: view
.sessions
.iter()
@@ -3166,6 +3181,10 @@ fn render_rpc_sessions(stdout: &mut dyn Write, view: &RpcSessionsView) -> Result
vec![
session.session_id.clone(),
session.role.clone(),
+ session
+ .user_pubkey
+ .clone()
+ .unwrap_or_else(|| "n/a".to_owned()),
yes_no(session.auth_required).to_owned(),
yes_no(session.authorized).to_owned(),
session.relay_count.to_string(),
@@ -3186,6 +3205,74 @@ fn render_rpc_sessions(stdout: &mut dyn Write, view: &RpcSessionsView) -> Result
Ok(())
}
+fn render_signer_session_action(
+ stdout: &mut dyn Write,
+ view: &crate::domain::runtime::SignerSessionActionView,
+) -> Result<(), RuntimeError> {
+ write_context(
+ stdout,
+ format!("signer session · {} · {}", view.action, view.state).as_str(),
+ )?;
+ let relays = view.relays.join(", ");
+ let permissions = view.permissions.join(", ");
+ let auth_required = view.auth_required.map(yes_no).unwrap_or("n/a");
+ let authorized = view.authorized.map(yes_no).unwrap_or("n/a");
+ let replayed = view.replayed.map(yes_no).unwrap_or("n/a");
+ let required = view.required.map(yes_no).unwrap_or("n/a");
+ let closed = view.closed.map(yes_no).unwrap_or("n/a");
+ let expires = view
+ .expires_in_secs
+ .map(|secs| format!("{secs}s"))
+ .unwrap_or_else(|| "n/a".to_owned());
+ render_pairs(
+ stdout,
+ "session",
+ &[
+ ("id", view.session_id.as_deref().unwrap_or("n/a")),
+ ("mode", view.mode.as_deref().unwrap_or("n/a")),
+ (
+ "remote signer",
+ view.remote_signer_pubkey.as_deref().unwrap_or("n/a"),
+ ),
+ ("client", view.client_pubkey.as_deref().unwrap_or("n/a")),
+ (
+ "signer pubkey",
+ view.signer_pubkey.as_deref().unwrap_or("n/a"),
+ ),
+ ("user pubkey", view.user_pubkey.as_deref().unwrap_or("n/a")),
+ ("pubkey", view.pubkey.as_deref().unwrap_or("n/a")),
+ ("auth required", auth_required),
+ ("authorized", authorized),
+ ("auth url", view.auth_url.as_deref().unwrap_or("n/a")),
+ ("expires", expires.as_str()),
+ ("replayed", replayed),
+ ("required", required),
+ ("closed", closed),
+ (
+ "relays",
+ if relays.is_empty() {
+ "n/a"
+ } else {
+ relays.as_str()
+ },
+ ),
+ (
+ "permissions",
+ if permissions.is_empty() {
+ "n/a"
+ } else {
+ permissions.as_str()
+ },
+ ),
+ ],
+ )?;
+ if let Some(reason) = &view.reason {
+ writeln!(stdout, "reason: {reason}")?;
+ }
+ writeln!(stdout, "source: {}", view.source)?;
+ Ok(())
+}
+
fn render_sync_status(stdout: &mut dyn Write, view: &SyncStatusView) -> Result<(), RuntimeError> {
write_context(
stdout,
@@ -4278,6 +4365,7 @@ fn human_command_name(view: &CommandView) -> &'static str {
},
CommandView::SellShow(_) => "sell show",
CommandView::Setup(_) => "setup",
+ CommandView::SignerSessionAction(_) => "signer session",
CommandView::SignerStatus(_) => "signer status",
CommandView::Status(_) => "status",
CommandView::SyncPull(_) => "sync pull",
diff --git a/src/runtime/daemon.rs b/src/runtime/daemon.rs
@@ -7,7 +7,9 @@ use radroots_events::trade::RadrootsTradeOrder;
use radroots_sdk::{
RadrootsSdkConfig, RadrootsdAuth, SdkPublishError, SdkRadrootsdFarmPublishOptions,
SdkRadrootsdListingPublishOptions, SdkRadrootsdProfilePublishOptions,
- SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionRef, SdkTransportMode, SignerConfig,
+ SdkRadrootsdSignerAuthority, SdkRadrootsdSignerSessionConnectRequest,
+ SdkRadrootsdSignerSessionHandle, SdkRadrootsdSignerSessionRef, SdkRadrootsdSignerSessionRole,
+ SdkRadrootsdSignerSessionView, SdkTransportMode, SignerConfig,
};
use reqwest::blocking::Client;
use serde::de::DeserializeOwned;
@@ -16,14 +18,15 @@ use serde_json::Value;
use crate::domain::runtime::{
CommandOutput, CommandView, JobDetailView, JobSummaryView, RpcSessionView, RpcSessionsView,
- RpcStatusView,
+ RpcStatusView, SignerSessionActionView,
};
use crate::runtime::config::RuntimeConfig;
use crate::runtime::provider;
-use crate::runtime::signer::ActorWriteSignerAuthority;
+use crate::runtime::signer::{ActorWriteSignerAuthority, configured_myc_signer_authority};
const RPC_SOURCE: &str = "daemon rpc · durable write plane";
const BRIDGE_SOURCE: &str = "daemon bridge · durable write plane";
+const SIGNER_SESSION_SOURCE: &str = "daemon signer session rpc · durable write plane";
const RPC_TIMEOUT_SECS: u64 = 2;
#[derive(Debug)]
@@ -378,6 +381,170 @@ pub fn sessions(config: &RuntimeConfig) -> CommandOutput {
}
}
+pub fn signer_sessions(config: &RuntimeConfig) -> CommandOutput {
+ match signer_session_views(config) {
+ Ok((url, sessions)) => {
+ let entries = sessions
+ .into_iter()
+ .map(map_sdk_session_view)
+ .collect::<Vec<_>>();
+ let state = if entries.is_empty() { "empty" } else { "ready" };
+ CommandOutput::success(CommandView::RpcSessions(RpcSessionsView {
+ state: state.to_owned(),
+ source: SIGNER_SESSION_SOURCE.to_owned(),
+ url,
+ count: entries.len(),
+ reason: None,
+ sessions: entries,
+ actions: Vec::new(),
+ }))
+ }
+ Err(DaemonRpcError::External(reason)) => {
+ CommandOutput::external_unavailable(CommandView::RpcSessions(RpcSessionsView {
+ state: "unavailable".to_owned(),
+ source: SIGNER_SESSION_SOURCE.to_owned(),
+ url: config.rpc.url.clone(),
+ count: 0,
+ reason: Some(reason),
+ sessions: Vec::new(),
+ actions: vec![
+ "start radrootsd and verify the actor write-plane endpoint".to_owned(),
+ ],
+ }))
+ }
+ Err(DaemonRpcError::Unconfigured(reason))
+ | Err(DaemonRpcError::Unauthorized(reason))
+ | Err(DaemonRpcError::MethodUnavailable(reason)) => {
+ CommandOutput::unconfigured(CommandView::RpcSessions(RpcSessionsView {
+ state: "unconfigured".to_owned(),
+ source: SIGNER_SESSION_SOURCE.to_owned(),
+ url: config.rpc.url.clone(),
+ count: 0,
+ reason: Some(reason),
+ sessions: Vec::new(),
+ actions: vec!["configure the radrootsd actor write-plane binding".to_owned()],
+ }))
+ }
+ Err(DaemonRpcError::InvalidResponse(reason))
+ | Err(DaemonRpcError::Remote(reason))
+ | Err(DaemonRpcError::UnknownJob(reason)) => {
+ CommandOutput::internal_error(CommandView::RpcSessions(RpcSessionsView {
+ state: "error".to_owned(),
+ source: SIGNER_SESSION_SOURCE.to_owned(),
+ url: config.rpc.url.clone(),
+ count: 0,
+ reason: Some(reason),
+ sessions: Vec::new(),
+ actions: Vec::new(),
+ }))
+ }
+ }
+}
+
+pub fn signer_session_connect_bunker(
+ config: &RuntimeConfig,
+ url: &str,
+) -> Result<SignerSessionActionView, DaemonRpcError> {
+ let mut request = SdkRadrootsdSignerSessionConnectRequest::bunker(url.to_owned());
+ if let Some(authority) = configured_myc_signer_authority(config) {
+ request = request.with_signer_authority(sdk_signer_authority(&authority));
+ }
+ signer_session_connect(config, "connect_bunker", &request)
+}
+
+pub fn signer_session_connect_nostrconnect(
+ config: &RuntimeConfig,
+ url: &str,
+ client_secret_key: &str,
+) -> Result<SignerSessionActionView, DaemonRpcError> {
+ let mut request = SdkRadrootsdSignerSessionConnectRequest::nostrconnect(
+ url.to_owned(),
+ client_secret_key.to_owned(),
+ );
+ if let Some(authority) = configured_myc_signer_authority(config) {
+ request = request.with_signer_authority(sdk_signer_authority(&authority));
+ }
+ signer_session_connect(config, "connect_nostrconnect", &request)
+}
+
+pub fn signer_session_show(
+ config: &RuntimeConfig,
+ session_id: &str,
+) -> Result<SignerSessionActionView, DaemonRpcError> {
+ let sdk = actor_write_sdk_client(config)?;
+ let session_ref = SdkRadrootsdSignerSessionRef::from_session_id(session_id.to_owned());
+ let session = block_on_sdk(sdk.radrootsd().signer_sessions().status(&session_ref))?
+ .map_err(|error| DaemonRpcError::Remote(error.to_string()))?;
+ Ok(signer_session_view_action("show", session))
+}
+
+pub fn signer_session_public_key(
+ config: &RuntimeConfig,
+ session_id: &str,
+) -> Result<SignerSessionActionView, DaemonRpcError> {
+ let sdk = actor_write_sdk_client(config)?;
+ let session_ref = SdkRadrootsdSignerSessionRef::from_session_id(session_id.to_owned());
+ let public_key = block_on_sdk(
+ sdk.radrootsd()
+ .signer_sessions()
+ .get_public_key(&session_ref),
+ )?
+ .map_err(|error| DaemonRpcError::Remote(error.to_string()))?;
+ let mut view = signer_session_action("public_key", "ready");
+ view.session_id = Some(session_id.to_owned());
+ view.pubkey = Some(public_key.pubkey);
+ Ok(view)
+}
+
+pub fn signer_session_authorize(
+ config: &RuntimeConfig,
+ session_id: &str,
+) -> Result<SignerSessionActionView, DaemonRpcError> {
+ let sdk = actor_write_sdk_client(config)?;
+ let session_ref = SdkRadrootsdSignerSessionRef::from_session_id(session_id.to_owned());
+ let result = block_on_sdk(sdk.radrootsd().signer_sessions().authorize(&session_ref))?
+ .map_err(|error| DaemonRpcError::Remote(error.to_string()))?;
+ let mut view = signer_session_action("authorize", "ready");
+ view.session_id = Some(session_id.to_owned());
+ view.authorized = Some(result.authorized);
+ view.replayed = Some(result.replayed);
+ Ok(view)
+}
+
+pub fn signer_session_require_auth(
+ config: &RuntimeConfig,
+ session_id: &str,
+ auth_url: &str,
+) -> Result<SignerSessionActionView, DaemonRpcError> {
+ let sdk = actor_write_sdk_client(config)?;
+ let session_ref = SdkRadrootsdSignerSessionRef::from_session_id(session_id.to_owned());
+ let result = block_on_sdk(
+ sdk.radrootsd()
+ .signer_sessions()
+ .require_auth(&session_ref, auth_url),
+ )?
+ .map_err(|error| DaemonRpcError::Remote(error.to_string()))?;
+ let mut view = signer_session_action("require_auth", "ready");
+ view.session_id = Some(session_id.to_owned());
+ view.required = Some(result.required);
+ view.auth_url = Some(auth_url.to_owned());
+ Ok(view)
+}
+
+pub fn signer_session_close(
+ config: &RuntimeConfig,
+ session_id: &str,
+) -> Result<SignerSessionActionView, DaemonRpcError> {
+ let sdk = actor_write_sdk_client(config)?;
+ let session_ref = SdkRadrootsdSignerSessionRef::from_session_id(session_id.to_owned());
+ let result = block_on_sdk(sdk.radrootsd().signer_sessions().close(&session_ref))?
+ .map_err(|error| DaemonRpcError::Remote(error.to_string()))?;
+ let mut view = signer_session_action("close", "ready");
+ view.session_id = Some(session_id.to_owned());
+ view.closed = Some(result.closed);
+ Ok(view)
+}
+
pub fn bridge_job_list(config: &RuntimeConfig) -> Result<Vec<JobSummaryView>, DaemonRpcError> {
bridge_jobs(config).map(|jobs| jobs.into_iter().map(map_job_summary_view).collect())
}
@@ -659,6 +826,83 @@ fn sdk_signer_authority(value: &ActorWriteSignerAuthority) -> SdkRadrootsdSigner
}
}
+fn signer_session_views(
+ config: &RuntimeConfig,
+) -> Result<(String, Vec<SdkRadrootsdSignerSessionView>), DaemonRpcError> {
+ let target = actor_write_target(config)?;
+ let sdk = radrootsd_sdk_client(&target)?;
+ let sessions = block_on_sdk(sdk.radrootsd().signer_sessions().list())?
+ .map_err(|error| DaemonRpcError::Remote(error.to_string()))?;
+ Ok((target.url, sessions))
+}
+
+fn signer_session_connect(
+ config: &RuntimeConfig,
+ action: &str,
+ request: &SdkRadrootsdSignerSessionConnectRequest,
+) -> Result<SignerSessionActionView, DaemonRpcError> {
+ let sdk = actor_write_sdk_client(config)?;
+ let handle = block_on_sdk(sdk.radrootsd().signer_sessions().connect(request))?
+ .map_err(|error| DaemonRpcError::Remote(error.to_string()))?;
+ Ok(signer_session_handle_action(action, handle))
+}
+
+fn signer_session_action(action: &str, state: &str) -> SignerSessionActionView {
+ SignerSessionActionView {
+ action: action.to_owned(),
+ state: state.to_owned(),
+ source: SIGNER_SESSION_SOURCE.to_owned(),
+ session_id: None,
+ mode: None,
+ remote_signer_pubkey: None,
+ client_pubkey: None,
+ signer_pubkey: None,
+ user_pubkey: None,
+ relays: Vec::new(),
+ permissions: Vec::new(),
+ auth_required: None,
+ authorized: None,
+ auth_url: None,
+ expires_in_secs: None,
+ pubkey: None,
+ replayed: None,
+ required: None,
+ closed: None,
+ reason: None,
+ }
+}
+
+fn signer_session_handle_action(
+ action: &str,
+ handle: SdkRadrootsdSignerSessionHandle,
+) -> SignerSessionActionView {
+ let mut view = signer_session_action(action, "ready");
+ view.session_id = Some(handle.session().session_id().to_owned());
+ view.mode = Some(format!("{:?}", handle.mode()));
+ view.remote_signer_pubkey = Some(handle.remote_signer_pubkey().to_owned());
+ view.client_pubkey = Some(handle.client_pubkey().to_owned());
+ view.relays = handle.relays().to_vec();
+ view
+}
+
+fn signer_session_view_action(
+ action: &str,
+ session: SdkRadrootsdSignerSessionView,
+) -> SignerSessionActionView {
+ let mut view = signer_session_action(action, "ready");
+ view.session_id = Some(session.session().session_id().to_owned());
+ view.signer_pubkey = Some(session.signer_pubkey);
+ view.user_pubkey = session.user_pubkey;
+ view.client_pubkey = Some(session.client_pubkey);
+ view.relays = session.relays;
+ view.permissions = session.permissions;
+ view.auth_required = Some(session.auth_required);
+ view.authorized = Some(session.authorized);
+ view.auth_url = session.auth_url;
+ view.expires_in_secs = session.expires_in_secs;
+ view
+}
+
fn map_sdk_publish_error(error: SdkPublishError) -> DaemonRpcError {
match error {
SdkPublishError::Config(err) => DaemonRpcError::Unconfigured(err.to_string()),
@@ -1093,6 +1337,28 @@ fn map_session_view(session: Nip46SessionRemote) -> RpcSessionView {
}
}
+fn map_sdk_session_view(session: SdkRadrootsdSignerSessionView) -> RpcSessionView {
+ RpcSessionView {
+ session_id: session.session().session_id().to_owned(),
+ role: sdk_session_role(session.role).to_owned(),
+ client_pubkey: session.client_pubkey,
+ signer_pubkey: session.signer_pubkey,
+ user_pubkey: session.user_pubkey,
+ relay_count: session.relays.len(),
+ permissions_count: session.permissions.len(),
+ auth_required: session.auth_required,
+ authorized: session.authorized,
+ expires_in_secs: session.expires_in_secs,
+ }
+}
+
+fn sdk_session_role(role: SdkRadrootsdSignerSessionRole) -> &'static str {
+ match role {
+ SdkRadrootsdSignerSessionRole::InboundLocalSigner => "inbound_local_signer",
+ SdkRadrootsdSignerSessionRole::OutboundRemoteSigner => "outbound_remote_signer",
+ }
+}
+
pub fn bridge_source() -> &'static str {
BRIDGE_SOURCE
}
diff --git a/src/runtime/signer.rs b/src/runtime/signer.rs
@@ -99,6 +99,27 @@ pub fn resolve_actor_write_authority(
}))
}
+pub fn configured_myc_signer_authority(
+ config: &RuntimeConfig,
+) -> Option<ActorWriteSignerAuthority> {
+ let binding = config.capability_binding(SIGNER_REMOTE_NIP46_CAPABILITY)?;
+ if binding.provider_runtime_id != SIGNER_BINDING_PROVIDER_RUNTIME_ID {
+ return None;
+ }
+ if !matches!(
+ binding.target_kind,
+ CapabilityBindingTargetKind::ManagedInstance
+ ) || binding.target != "default"
+ {
+ return None;
+ }
+ Some(ActorWriteSignerAuthority {
+ provider_runtime_id: SIGNER_BINDING_PROVIDER_RUNTIME_ID.to_owned(),
+ account_identity_id: binding.managed_account_ref.clone()?,
+ provider_signer_session_id: binding.signer_session_ref.clone(),
+ })
+}
+
fn resolve_local_signer_status(config: &RuntimeConfig) -> SignerStatusView {
let (account_resolution, resolved_account_id) =
match crate::runtime::accounts::resolve_account_resolution(config) {
diff --git a/tests/job_rpc.rs b/tests/job_rpc.rs
@@ -1,3 +1,4 @@
+use std::fs;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::path::Path;
@@ -50,10 +51,78 @@ fn job_rpc_test_guard() -> MutexGuard<'static, ()> {
.expect("job rpc test lock")
}
+fn write_user_config(workdir: &Path, contents: &str) {
+ let config_dir = workdir.join("home/.radroots/config/apps/cli");
+ fs::create_dir_all(&config_dir).expect("user config dir");
+ fs::write(config_dir.join("config.toml"), contents).expect("write user config");
+}
+
+fn signer_session_config(url: &str) -> String {
+ format!(
+ r#"
+[[capability_binding]]
+capability = "write_plane.trade_jsonrpc"
+provider = "radrootsd"
+target_kind = "explicit_endpoint"
+target = "{url}"
+
+[[capability_binding]]
+capability = "signer.remote_nip46"
+provider = "myc"
+target_kind = "managed_instance"
+target = "default"
+managed_account_ref = "acct_user"
+signer_session_ref = "myc_conn_1"
+"#
+ )
+}
+
+fn run_signer_session_command<F>(
+ workdir: &Path,
+ args: &[&str],
+ handler: F,
+) -> (Value, MockRpcRequest)
+where
+ F: Fn(&MockRpcRequest) -> MockRpcResponse + Send + Sync + 'static,
+{
+ let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
+ let recorded = Arc::clone(&requests);
+ let server = MockRpcServer::start(move |request| {
+ let response = handler(&request);
+ recorded.lock().expect("record requests").push(request);
+ response
+ });
+ write_user_config(
+ workdir,
+ signer_session_config(server.url().as_str()).as_str(),
+ );
+ let output = job_rpc_command_in(workdir)
+ .env("RADROOTS_RPC_BEARER_TOKEN", "secret")
+ .args(args)
+ .output()
+ .expect("run signer session command");
+ assert!(
+ output.status.success(),
+ "command {:?} stdout: {}\nstderr: {}",
+ args,
+ String::from_utf8_lossy(&output.stdout),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ let json: Value = serde_json::from_slice(output.stdout.as_slice()).expect("json output");
+ let request = requests
+ .lock()
+ .expect("recorded requests")
+ .first()
+ .expect("one recorded request")
+ .clone();
+ (json, request)
+}
+
#[derive(Debug, Clone)]
struct MockRpcRequest {
method: String,
auth_header: Option<String>,
+ body: Value,
}
#[derive(Debug, Clone)]
@@ -98,7 +167,7 @@ struct MockRpcServer {
impl MockRpcServer {
fn start<F>(handler: F) -> Self
where
- F: Fn(String, Option<String>) -> MockRpcResponse + Send + Sync + 'static,
+ F: Fn(MockRpcRequest) -> MockRpcResponse + Send + Sync + 'static,
{
let listener = TcpListener::bind("127.0.0.1:0").expect("bind mock rpc listener");
listener
@@ -110,22 +179,27 @@ impl MockRpcServer {
.to_string();
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_flag = Arc::clone(&shutdown);
- let handler: Arc<dyn Fn(String, Option<String>) -> MockRpcResponse + Send + Sync> =
+ let handler: Arc<dyn Fn(MockRpcRequest) -> MockRpcResponse + Send + Sync> =
Arc::new(handler);
let handle = thread::spawn(move || {
while !shutdown_flag.load(Ordering::SeqCst) {
match listener.accept() {
Ok((mut stream, _)) => {
- if let Ok(request) = read_request(&mut stream) {
- let response =
- handler(request.method.clone(), request.auth_header.clone());
- let _ = write_response(&mut stream, &response);
+ let _ = stream.set_nonblocking(false);
+ match read_request(&mut stream) {
+ Ok(request) => {
+ let response = handler(request);
+ let _ = write_response(&mut stream, &response);
+ }
+ Err(_) => {}
}
}
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
}
- Err(_) => break,
+ Err(_) => {
+ thread::sleep(Duration::from_millis(10));
+ }
}
}
});
@@ -201,6 +275,7 @@ fn read_request(stream: &mut TcpStream) -> Result<MockRpcRequest, String> {
Ok(MockRpcRequest {
method,
auth_header,
+ body: envelope,
})
}
@@ -282,6 +357,27 @@ fn sample_bridge_status() -> Value {
})
}
+fn sample_signer_session() -> Value {
+ json!({
+ "session_id": "sess_1",
+ "role": "outbound_remote_signer",
+ "client_pubkey": "client_pubkey",
+ "signer_pubkey": "myc_signer_pubkey",
+ "user_pubkey": "user_pubkey",
+ "relays": ["wss://relay.one"],
+ "permissions": ["sign_event", "nip44_encrypt"],
+ "auth_required": false,
+ "authorized": true,
+ "auth_url": null,
+ "expires_in_secs": 60,
+ "signer_authority": {
+ "provider_runtime_id": "myc",
+ "account_identity_id": "acct_user",
+ "provider_signer_session_id": "myc_conn_1"
+ }
+ })
+}
+
fn sample_job(job_id: &str, state: &str, terminal: bool, completed_at_unix: Option<u64>) -> Value {
json!({
"job_id": job_id,
@@ -315,14 +411,9 @@ fn rpc_status_reports_bridge_ready_via_daemon_rpc() {
let _guard = job_rpc_test_guard();
let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |method, auth_header| {
- recorded
- .lock()
- .expect("record requests")
- .push(MockRpcRequest {
- method: method.clone(),
- auth_header: auth_header.clone(),
- });
+ let server = MockRpcServer::start(move |request| {
+ let method = request.method.clone();
+ recorded.lock().expect("record requests").push(request);
match method.as_str() {
"bridge.status" => MockRpcResponse::success(sample_bridge_status()),
_ => MockRpcResponse::rpc_error(-32601, "method not found"),
@@ -356,14 +447,9 @@ fn rpc_sessions_ndjson_emits_public_session_entries() {
let _guard = job_rpc_test_guard();
let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |method, auth_header| {
- recorded
- .lock()
- .expect("record requests")
- .push(MockRpcRequest {
- method: method.clone(),
- auth_header: auth_header.clone(),
- });
+ let server = MockRpcServer::start(move |request| {
+ let method = request.method.clone();
+ recorded.lock().expect("record requests").push(request);
match method.as_str() {
"nip46.session.list" => MockRpcResponse::success(json!([
{
@@ -416,18 +502,189 @@ fn rpc_sessions_ndjson_emits_public_session_entries() {
}
#[test]
+fn signer_session_connect_preserves_configured_myc_authority() {
+ let _guard = job_rpc_test_guard();
+ let dir = tempdir().expect("tempdir");
+
+ let (bunker_json, bunker_request) = run_signer_session_command(
+ dir.path(),
+ &[
+ "--json",
+ "signer",
+ "session",
+ "connect-bunker",
+ "bunker://remote",
+ ],
+ |_| {
+ MockRpcResponse::success(json!({
+ "session_id": "sess_bunker",
+ "mode": "Bunker",
+ "remote_signer_pubkey": "myc_signer_pubkey",
+ "client_pubkey": "client_pubkey",
+ "relays": ["wss://relay.one"]
+ }))
+ },
+ );
+ assert_eq!(bunker_json["action"], "connect_bunker");
+ assert_eq!(bunker_json["session_id"], "sess_bunker");
+ assert_eq!(bunker_json["remote_signer_pubkey"], "myc_signer_pubkey");
+
+ let nostr_dir = tempdir().expect("tempdir");
+ let (nostr_json, nostr_request) = run_signer_session_command(
+ nostr_dir.path(),
+ &[
+ "--json",
+ "signer",
+ "session",
+ "connect-nostrconnect",
+ "nostrconnect://remote",
+ "--client-secret-key",
+ "client-secret",
+ ],
+ |_| {
+ MockRpcResponse::success(json!({
+ "session_id": "sess_nostrconnect",
+ "mode": "Nostrconnect",
+ "remote_signer_pubkey": "myc_signer_pubkey",
+ "client_pubkey": "client_pubkey",
+ "relays": ["wss://relay.two"]
+ }))
+ },
+ );
+ assert_eq!(nostr_json["action"], "connect_nostrconnect");
+ assert_eq!(nostr_json["session_id"], "sess_nostrconnect");
+
+ for request in [&bunker_request, &nostr_request] {
+ assert_eq!(request.method, "nip46.connect");
+ assert_eq!(request.auth_header.as_deref(), Some("Bearer secret"));
+ assert_eq!(
+ request.body["params"]["signer_authority"]["provider_runtime_id"],
+ "myc"
+ );
+ assert_eq!(
+ request.body["params"]["signer_authority"]["account_identity_id"],
+ "acct_user"
+ );
+ assert_eq!(
+ request.body["params"]["signer_authority"]["provider_signer_session_id"],
+ "myc_conn_1"
+ );
+ }
+ assert_eq!(bunker_request.body["params"]["url"], "bunker://remote");
+ assert_eq!(nostr_request.body["params"]["url"], "nostrconnect://remote");
+ assert_eq!(
+ nostr_request.body["params"]["client_secret_key"],
+ "client-secret"
+ );
+}
+
+#[test]
+fn signer_session_commands_cover_inspect_hydrate_authorize_require_auth_and_close() {
+ let _guard = job_rpc_test_guard();
+ let dir = tempdir().expect("tempdir");
+ let mut recorded = Vec::<MockRpcRequest>::new();
+
+ let (list_json, request) =
+ run_signer_session_command(dir.path(), &["--json", "signer", "session", "list"], |_| {
+ MockRpcResponse::success(json!([sample_signer_session()]))
+ });
+ recorded.push(request);
+ assert_eq!(list_json["state"], "ready");
+ assert_eq!(list_json["sessions"][0]["session_id"], "sess_1");
+ assert_eq!(list_json["sessions"][0]["user_pubkey"], "user_pubkey");
+
+ let (show_json, request) = run_signer_session_command(
+ dir.path(),
+ &["--json", "signer", "session", "show", "sess_1"],
+ |_| MockRpcResponse::success(sample_signer_session()),
+ );
+ recorded.push(request);
+ assert_eq!(show_json["action"], "show");
+ assert_eq!(show_json["session_id"], "sess_1");
+ assert_eq!(show_json["user_pubkey"], "user_pubkey");
+
+ let (public_key_json, request) = run_signer_session_command(
+ dir.path(),
+ &["--json", "signer", "session", "public-key", "sess_1"],
+ |_| MockRpcResponse::success(json!({ "pubkey": "user_pubkey" })),
+ );
+ recorded.push(request);
+ assert_eq!(public_key_json["action"], "public_key");
+ assert_eq!(public_key_json["pubkey"], "user_pubkey");
+
+ let (authorize_json, request) = run_signer_session_command(
+ dir.path(),
+ &["--json", "signer", "session", "authorize", "sess_1"],
+ |_| MockRpcResponse::success(json!({ "authorized": true, "replayed": false })),
+ );
+ recorded.push(request);
+ assert_eq!(authorize_json["action"], "authorize");
+ assert_eq!(authorize_json["authorized"], true);
+ assert_eq!(authorize_json["replayed"], false);
+
+ let (require_auth_json, request) = run_signer_session_command(
+ dir.path(),
+ &[
+ "--json",
+ "signer",
+ "session",
+ "require-auth",
+ "sess_1",
+ "--auth-url",
+ "https://auth.example",
+ ],
+ |_| MockRpcResponse::success(json!({ "required": true })),
+ );
+ recorded.push(request);
+ assert_eq!(require_auth_json["action"], "require_auth");
+ assert_eq!(require_auth_json["required"], true);
+ assert_eq!(require_auth_json["auth_url"], "https://auth.example");
+
+ let (close_json, request) = run_signer_session_command(
+ dir.path(),
+ &["--json", "signer", "session", "close", "sess_1"],
+ |_| MockRpcResponse::success(json!({ "closed": true })),
+ );
+ recorded.push(request);
+ assert_eq!(close_json["action"], "close");
+ assert_eq!(close_json["closed"], true);
+
+ let methods = recorded
+ .iter()
+ .map(|request| request.method.as_str())
+ .collect::<Vec<_>>();
+ assert_eq!(
+ methods,
+ [
+ "nip46.session.list",
+ "nip46.session.status",
+ "nip46.get_public_key",
+ "nip46.session.authorize",
+ "nip46.session.require_auth",
+ "nip46.session.close",
+ ]
+ );
+ assert!(
+ recorded
+ .iter()
+ .all(|request| request.auth_header.as_deref() == Some("Bearer secret"))
+ );
+ for request in recorded.iter().skip(1) {
+ assert_eq!(request.body["params"]["session_id"], "sess_1");
+ }
+ assert_eq!(
+ recorded[4].body["params"]["auth_url"],
+ "https://auth.example"
+ );
+}
+
+#[test]
fn job_commands_require_bridge_bearer_token() {
let _guard = job_rpc_test_guard();
let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |method, auth_header| {
- recorded
- .lock()
- .expect("record requests")
- .push(MockRpcRequest {
- method,
- auth_header,
- });
+ let server = MockRpcServer::start(move |request| {
+ recorded.lock().expect("record requests").push(request);
MockRpcResponse::rpc_error(-32601, "method not found")
});
@@ -455,14 +712,9 @@ fn job_ls_and_get_report_retained_bridge_jobs() {
let _guard = job_rpc_test_guard();
let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
let recorded = Arc::clone(&requests);
- let server = MockRpcServer::start(move |method, auth_header| {
- recorded
- .lock()
- .expect("record requests")
- .push(MockRpcRequest {
- method: method.clone(),
- auth_header: auth_header.clone(),
- });
+ let server = MockRpcServer::start(move |request| {
+ let method = request.method.clone();
+ recorded.lock().expect("record requests").push(request);
match method.as_str() {
"bridge.job.list" => {
MockRpcResponse::success(json!([sample_job("job-123", "publishing", false, None)]))
@@ -526,14 +778,9 @@ fn job_watch_ndjson_emits_one_frame_per_poll_until_terminal() {
let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
let observed = Arc::clone(&requests);
let counter = Arc::clone(&sequence);
- let server = MockRpcServer::start(move |method, auth_header| {
- observed
- .lock()
- .expect("record requests")
- .push(MockRpcRequest {
- method: method.clone(),
- auth_header,
- });
+ let server = MockRpcServer::start(move |request| {
+ let method = request.method.clone();
+ observed.lock().expect("record requests").push(request);
match method.as_str() {
"bridge.job.status" => {
let mut count = counter.lock().expect("watch count");
@@ -590,14 +837,9 @@ fn job_watch_human_appends_snapshots_without_screen_clear() {
let requests = Arc::new(Mutex::new(Vec::<MockRpcRequest>::new()));
let observed = Arc::clone(&requests);
let counter = Arc::clone(&sequence);
- let server = MockRpcServer::start(move |method, auth_header| {
- observed
- .lock()
- .expect("record requests")
- .push(MockRpcRequest {
- method: method.clone(),
- auth_header,
- });
+ let server = MockRpcServer::start(move |request| {
+ let method = request.method.clone();
+ observed.lock().expect("record requests").push(request);
match method.as_str() {
"bridge.job.status" => {
let mut count = counter.lock().expect("watch count");
diff --git a/tests/listing.rs b/tests/listing.rs
@@ -1537,6 +1537,7 @@ impl MockRpcServer {
while !shutdown_flag.load(Ordering::SeqCst) {
match listener.accept() {
Ok((mut stream, _)) => {
+ let _ = stream.set_nonblocking(false);
if let Ok(request) = read_request(&mut stream) {
let response =
handler(request.body.clone(), request.auth_header.clone());
@@ -1546,7 +1547,9 @@ impl MockRpcServer {
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
}
- Err(_) => break,
+ Err(_) => {
+ thread::sleep(Duration::from_millis(10));
+ }
}
}
});
diff --git a/tests/order.rs b/tests/order.rs
@@ -283,6 +283,7 @@ impl MockRpcServer {
while !shutdown_flag.load(Ordering::SeqCst) {
match listener.accept() {
Ok((mut stream, _)) => {
+ let _ = stream.set_nonblocking(false);
if let Ok(request) = read_request(&mut stream) {
let response =
handler(request.body.clone(), request.auth_header.clone());
@@ -292,7 +293,9 @@ impl MockRpcServer {
Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => {
thread::sleep(Duration::from_millis(10));
}
- Err(_) => break,
+ Err(_) => {
+ thread::sleep(Duration::from_millis(10));
+ }
}
}
});