//! IPC server: accepts greeter connections over a Unix socket, drives the
//! PAM auth conversation through the IPC channel, then forks the user
//! session and holds the VT switch until crustace signals "first frame ready"
//! on a per-session socket — so the foreground VT only changes when the
//! target session is genuinely about to paint.

use anyhow::{ anyhow, bail, Result };
use loginmanager_ipc::
{
	proto::{ Request, Response },
	wire::{ read_message, write_message },
};
use std::os::unix::fs::PermissionsExt;
use std::path::{ Path, PathBuf };
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{ AsyncReadExt, AsyncWriteExt };
use tokio::net::{ UnixListener, UnixStream };
use tokio::sync::{ Mutex, Notify, mpsc };
use tokio::task;

use crate::config::DaemonConfig;
use crate::greeter;
use crate::pam_session::{ ConversationHandler, Transaction };
use crate::session;
use crate::vt;

const PAM_SERVICE:    &str     = "loginmanager-daemon";
const READY_ENV_VAR:  &str     = "LOGINMANAGER_SESSION_SOCK";
const READY_TIMEOUT:       Duration = Duration::from_secs( 30 );
const VT_SWITCH_TIMEOUT:   Duration = Duration::from_secs( 15 );

#[ derive( Clone, Copy, Debug ) ]
pub(crate) enum SessionState
{
	Idle,
	Authenticating,
	Starting { pid: libc::pid_t, vt: u32 },
	Active   { pid: libc::pid_t, vt: u32 },
}

pub(crate) type SharedSessionState = Arc<Mutex<SessionState>>;

/// Drop the session state back to `Idle` and wake `greeter::supervise` if it
/// is parked waiting on the user session to finish. Notify is fire-and-forget:
/// supervise re-checks the state after every wake, so a stale permit is
/// consumed by an extra (no-op) iteration of its wait loop rather than a
/// spurious respawn.
async fn reset_to_idle( state: &SharedSessionState, respawn: &Arc<Notify> )
{
	*state.lock().await = SessionState::Idle;
	respawn.notify_one();
}

pub fn run( socket_path: &str, cfg: DaemonConfig ) -> Result<()>
{
	let runtime = tokio::runtime::Builder::new_multi_thread()
		.enable_all()
		.build()?;
	runtime.block_on( serve( socket_path.to_string(), cfg ) )
}

async fn serve( socket_path: String, cfg: DaemonConfig ) -> Result<()>
{
	let path = Path::new( &socket_path );
	let runtime_dir: PathBuf = path.parent()
		.filter( |p| !p.as_os_str().is_empty() )
		.map( |p| p.to_path_buf() )
		.unwrap_or_else( || PathBuf::from( "." ) );
	std::fs::create_dir_all( &runtime_dir )?;
	set_dir_perms( &runtime_dir, 0o711 )?;
	if path.exists() { std::fs::remove_file( path ).ok(); }

	let listener = UnixListener::bind( &socket_path )?;
	set_socket_perms( &socket_path, 0o666 )?;
	tracing::info!( "server: listening on {socket_path}" );

	let session_state = Arc::new( Mutex::new( SessionState::Idle ) );

	// Gate the greeter's respawn on the daemon switching the user's VT back
	// to the greeter's VT. Otherwise the greeter forge starts on a
	// non-foreground VT, libseat keeps its session paused, and the
	// "session activate" resume on logout never fires cleanly — leaving the
	// user on a black screen instead of the greeter.
	let respawn_notify = Arc::new( Notify::new() );

	let greeter_cfg     = cfg.greeter.clone();
	let greeter_state   = session_state.clone();
	let greeter_respawn = respawn_notify.clone();
	tokio::spawn( greeter::supervise( greeter_cfg, greeter_state, greeter_respawn ) );

	loop
	{
		let ( stream, _ ) = listener.accept().await?;
		tracing::info!( "server: accepted connection" );
		let cfg = cfg.clone();
		let runtime_dir = runtime_dir.clone();
		let state = session_state.clone();
		let respawn = respawn_notify.clone();
		tokio::spawn( async move
		{
			if let Err( e ) = handle_connection( stream, cfg, runtime_dir, state, respawn ).await
			{
				tracing::error!( "server: connection failed: {e}" );
			}
		} );
	}
}

fn set_socket_perms( path: &str, mode: u32 ) -> Result<()>
{
	let mut perms = std::fs::metadata( path )?.permissions();
	perms.set_mode( mode );
	std::fs::set_permissions( path, perms )?;
	Ok( () )
}

fn set_dir_perms( path: &Path, mode: u32 ) -> Result<()>
{
	let mut perms = std::fs::metadata( path )?.permissions();
	perms.set_mode( mode );
	std::fs::set_permissions( path, perms )?;
	Ok( () )
}

enum AuthStatus
{
	Ok( libc::pid_t ),
	Failed( String ),
}

async fn handle_connection(
	stream:      UnixStream,
	cfg:         DaemonConfig,
	runtime_dir: PathBuf,
	state:       SharedSessionState,
	respawn:     Arc<Notify>,
) -> Result<()>
{
	let ( mut reader, mut writer ) = stream.into_split();

	let req: Request = read_message( &mut reader ).await
		.map_err( |e| anyhow!( "read first message: {e}" ) )?;
	match &req
	{
		Request::PowerOff =>
		{
			tracing::info!( "server: PowerOff requested" );
			let _ = std::process::Command::new( "systemctl" ).arg( "poweroff" ).spawn();
			return Ok( () );
		}
		Request::Reboot =>
		{
			tracing::info!( "server: Reboot requested" );
			let _ = std::process::Command::new( "systemctl" ).arg( "reboot" ).spawn();
			return Ok( () );
		}
		_ => {}
	}
	let username = match req
	{
		Request::BeginAuth { username } => username,
		Request::Cancel                 => return Ok( () ),
		_ => bail!( "expected BeginAuth as first message" ),
	};
	tracing::info!( "server: BeginAuth for {username}" );

	let busy = {
		let mut current = state.lock().await;
		match *current
		{
			SessionState::Idle =>
			{
				*current = SessionState::Authenticating;
				None
			}
			SessionState::Authenticating =>
			{
				Some( "authentication already in progress".to_string() )
			}
			SessionState::Starting { pid, vt } | SessionState::Active { pid, vt } =>
			{
				Some( format!( "session already active on VT {vt} (pid {pid})" ) )
			}
		}
	};
	if let Some( description ) = busy
	{
		write_message( &mut writer, &Response::Error { description } ).await
			.map_err( |e| anyhow!( "write busy Error: {e}" ) )?;
		return Ok( () );
	}

	let user = match session::lookup_user( &username )
	{
		Ok( u ) => u,
		Err( e ) =>
		{
			reset_to_idle( &state, &respawn ).await;
			write_message( &mut writer, &Response::AuthRejected { reason: e.to_string() } ).await
				.map_err( |e| anyhow!( "write AuthRejected: {e}" ) )?;
			return Ok( () );
		}
	};

	let target_vt = match vt::open_query()
	{
		Ok( vt ) => vt,
		Err( e ) =>
		{
			reset_to_idle( &state, &respawn ).await;
			return Err( e );
		}
	};
	let tty_path  = format!( "/dev/tty{}", target_vt );

	let ready_path = runtime_dir
		.join( format!( "session-ready-vt{}.sock", target_vt ) )
		.to_string_lossy()
		.into_owned();
	let _ = std::fs::remove_file( &ready_path );
	let ready_listener = match UnixListener::bind( &ready_path )
	{
		Ok( listener ) => listener,
		Err( e ) =>
		{
			reset_to_idle( &state, &respawn ).await;
			return Err( e.into() );
		}
	};
	if let Err( e ) = set_socket_perms( &ready_path, 0o600 )
	{
		reset_to_idle( &state, &respawn ).await;
		return Err( e );
	}
	if let Err( e ) = std::os::unix::fs::chown( &ready_path, Some( user.uid ), Some( user.gid ) )
	{
		reset_to_idle( &state, &respawn ).await;
		return Err( e.into() );
	}
	tracing::info!(
		"server: ready socket {ready_path} owner {}:{} mode 0600",
		user.uid,
		user.gid,
	);

	let mut session_env = cfg.session.environment.clone();
	session_env.push( ( READY_ENV_VAR.to_string(), ready_path.clone() ) );
	tracing::info!( "server: exported {READY_ENV_VAR}={ready_path}" );

	let ( challenge_tx, mut challenge_rx ) = mpsc::channel::<( String, bool )>( 1 );
	let ( response_tx,  response_rx )      = mpsc::channel::<Option<String>>( 1 );
	let ( status_tx,    mut status_rx )    = mpsc::channel::<AuthStatus>( 1 );

	// Decide whether to wrap the user session in `loginmanager-splash`.
	// The splash:
	//   * signals `ready` immediately on the LOGINMANAGER_SESSION_SOCK,
	//     so the daemon's `accept()` unblocks within milliseconds instead
	//     of hitting the 30-second timeout;
	//   * blocks on `VT_WAITACTIVE` until the daemon issues SwitchTo;
	//   * draws a fade-in + pulse of the brand logo while forge spins up;
	//   * `execv`s the real session command (`cfg.session.command`) on top.
	// Falls back to direct exec if disabled in config or the binary is
	// missing on disk.
	let ( exec_binary, exec_extra_args ): ( PathBuf, Vec<String> ) = if cfg.splash.enabled
		&& cfg.splash.binary.exists()
	{
		(
			cfg.splash.binary.clone(),
			vec![
				cfg.splash.logo.to_string_lossy().into_owned(),
				cfg.session.command.to_string_lossy().into_owned(),
			],
		)
	} else {
		( cfg.session.command.clone(), Vec::new() )
	};
	tracing::info!(
		"server: session exec target {} (extra args: {:?})",
		exec_binary.display(),
		exec_extra_args,
	);

	let username_for_pam = username.clone();
	let tty_path_for_pam = tty_path.clone();
	let user_for_pam     = user;
	let command_for_pam  = exec_binary;
	let extra_args_for_pam = exec_extra_args;
	let env_for_pam      = session_env;
	let greeter_vt       = cfg.greeter.vt;
	let pam_task = task::spawn_blocking( move ||
	{
		let result = ( || -> Result<libc::pid_t>
		{
			let handler = Box::new( IpcConv { challenge_tx, response_rx } );
			let mut tx = Transaction::start( PAM_SERVICE, &username_for_pam, handler )?;
			tx.set_tty( &tty_path_for_pam )?;
			tx.putenv( "XDG_SESSION_TYPE=wayland" )?;
			tx.authenticate()?;
			session::fork_session( tx, user_for_pam, target_vt, &command_for_pam, &extra_args_for_pam, &env_for_pam )
		} )();

		let status = match &result
		{
			Ok( pid )  => AuthStatus::Ok( *pid ),
			Err( e )   => AuthStatus::Failed( e.to_string() ),
		};
		let _ = status_tx.blocking_send( status );
		result.map( |_| () )
	} );

	let pid = match pump_auth( &mut reader, &mut writer, &mut challenge_rx, &response_tx, &mut status_rx, &username, target_vt ).await
	{
		Ok( Some( pid ) ) => pid,
		Ok( None ) =>
		{
			let _ = std::fs::remove_file( &ready_path );
			// Drop the sender so the PAM task's blocking_recv unblocks.
			drop( response_tx );
			let _ = pam_task.await;
			reset_to_idle( &state, &respawn ).await;
			return Ok( () );
		}
		Err( e ) =>
		{
			let _ = std::fs::remove_file( &ready_path );
			// Drop the sender so the PAM task's blocking_recv unblocks.
			drop( response_tx );
			let _ = pam_task.await;
			reset_to_idle( &state, &respawn ).await;
			return Err( e );
		}
	};
	*state.lock().await = SessionState::Starting { pid, vt: target_vt };

	// `splash_stream` is kept alive past the accept block so we can write
	// an ack byte back to the splash once `SwitchTo` and `VT_WAITACTIVE`
	// have confirmed the kernel really put our VT in the foreground. The
	// splash blocks on a `read(1)` on its end and only then opens DRM —
	// so the master slot is guaranteed empty and the implicit-master rule
	// on open() lands the splash as master without an ioctl.
	let mut splash_stream: Option<UnixStream> = None;
	match tokio::time::timeout( READY_TIMEOUT, ready_listener.accept() ).await
	{
		Ok( Ok( ( mut stream, _ ) ) ) =>
		{
			let mut ready = [0u8; 1];
			match stream.read_exact( &mut ready ).await
			{
				Ok( _ ) if ready[0] == 1 =>
				{
					tracing::info!(
						"server: session (pid {pid}) signaled first frame ready on VT {target_vt}",
					);
					splash_stream = Some( stream );
				}
				_ =>
				{
					tracing::warn!(
						"server: session (pid {pid}) ready signal invalid \
						 on {ready_path}, switching VT anyway",
					);
				}
			}
		}
		Ok( Err( e ) ) => tracing::warn!(
			"server: ready accept failed on {ready_path}: {e}, switching VT anyway",
		),
		Err( _ )       => tracing::warn!(
			"server: session (pid {pid}) did not connect to {ready_path} \
			 after {} s — crustace may not be implementing the ready protocol; \
			 switching VT anyway",
			READY_TIMEOUT.as_secs(),
		),
	}
	let _ = std::fs::remove_file( &ready_path );

	// Put the destination VT in `KD_GRAPHICS` *before* the switch so the
	// kernel's fbcon doesn't paint the text console on it the instant
	// `SwitchTo` lands. Without this the splash's last frame is overwritten
	// by fbcon as soon as the splash drops master, and the user sees a
	// flash of console output between the splash exiting and forge taking
	// over the scanout.
	match task::spawn_blocking( move || vt::set_graphics_mode( target_vt ) ).await
	{
		Ok( Ok( () ) ) => tracing::info!( "server: tty{target_vt} → KD_GRAPHICS" ),
		Ok( Err( e ) ) => tracing::warn!( "server: KDSETMODE on tty{target_vt}: {e}" ),
		Err( e )       => tracing::warn!( "server: KDSETMODE task join: {e}" ),
	}

	let switch = tokio::time::timeout(
		VT_SWITCH_TIMEOUT,
		task::spawn_blocking( move || -> Result<()>
		{
			vt::switch_via_logind( "seat0", target_vt )
		} ),
	).await;
	let switch_ok = match switch
	{
		Ok( Ok( Ok( () ) ) ) => true,
		Ok( Ok( Err( e ) ) ) =>
		{
			tracing::error!( "server: VT switch failed: {e}" );
			false
		}
		Ok( Err( e ) ) =>
		{
			tracing::error!( "server: VT switch task join: {e}" );
			false
		}
		Err( _ ) =>
		{
			tracing::warn!(
				"server: VT switch via logind timed out after {} s, continuing",
				VT_SWITCH_TIMEOUT.as_secs(),
			);
			false
		}
	};

	// Wait for the kernel to actually put target_vt in the foreground.
	// `SwitchTo` is supposed to be synchronous but in practice it can
	// return before the kernel commits the swap; the splash needs the
	// commit to land before it opens DRM (so the previous master has
	// dropped and implicit-master grants on open). The daemon holds
	// CAP_SYS_TTY_CONFIG so it can do `VT_WAITACTIVE` directly on
	// `/dev/tty0`, which the splash can't open from userland.
	if switch_ok
	{
		match task::spawn_blocking( move || vt::wait_active( target_vt ) ).await
		{
			Ok( Ok( () ) ) =>
			{
				tracing::info!( "server: VT_WAITACTIVE confirmed tty{target_vt} is foreground" );
			}
			Ok( Err( e ) ) => tracing::warn!( "server: VT_WAITACTIVE: {e}" ),
			Err( e )       => tracing::warn!( "server: VT_WAITACTIVE task join: {e}" ),
		}
	}

	// Ack the splash so it can proceed to its DRM setup. We send a byte
	// even if SwitchTo / VT_WAITACTIVE failed — the splash needs to
	// unblock either way; on failure it'll just see EACCES from set_crtc
	// and fall through to `execv` of the real session.
	if let Some( mut stream ) = splash_stream
	{
		if let Err( e ) = stream.write_all( &[ 1u8 ] ).await
		{
			tracing::warn!( "server: ack to splash on {ready_path}: {e}" );
		}
		// Drop closes the socket; the splash's `read` returns whatever bytes
		// landed plus EOF if we wrote less than expected.
	}

	match write_message( &mut writer, &Response::SessionReady { vt: target_vt } ).await
	{
		Ok( () ) => tracing::info!( "server: SessionReady sent, greeter remains alive on VT {greeter_vt}" ),
		Err( e ) => tracing::warn!( "server: write SessionReady failed: {e}" ),
	}
	*state.lock().await = SessionState::Active { pid, vt: target_vt };

	// Spawn session monitor BEFORE pam_task.await so there is no window where
	// the child exits before we start watching it.
	let mon_state   = state.clone();
	let mon_respawn = respawn.clone();
	let mon_user_vt = target_vt;
	tokio::spawn( async move { monitor_session( pid, mon_user_vt, greeter_vt, mon_state, mon_respawn ).await } );

	if let Err( e ) = pam_task.await
	{
		tracing::error!( "server: PAM task join: {e}" );
	}

	Ok( () )
}

async fn monitor_session(
	pid:        libc::pid_t,
	user_vt:    u32,
	greeter_vt: u32,
	state:      SharedSessionState,
	respawn:    Arc<Notify>,
)
{
	tracing::info!( "server: monitor_session for pid {pid} watching" );
	let wait = task::spawn_blocking( move || session::wait_session( pid ) ).await;
	match wait
	{
		Ok( Ok( status ) ) => tracing::info!( "server: session pid {pid} exited (raw status {status})" ),
		Ok( Err( e ) ) => tracing::error!( "server: wait session failed: {e}" ),
		Err( e ) => tracing::error!( "server: wait task join: {e}" ),
	}

	// Give logind a moment to mark the user session as Closed before we
	// ask the Seat to switch — SwitchTo can otherwise be a silent no-op
	// while logind still believes the previous tty's session is "active".
	tokio::time::sleep( std::time::Duration::from_millis( 100 ) ).await;

	// The dead compositor left tty{user_vt} in KD_GRAPHICS mode; the
	// kernel will refuse `set_console()` (returning -EINVAL silently) until
	// we put it back in KD_TEXT + VT_AUTO. Without this, neither logind's
	// SwitchTo nor a direct VT_ACTIVATE move the foreground console — only
	// a physical Ctrl-Alt-F<n> from the keyboard layer manages.
	match task::spawn_blocking( move || vt::reset_vt_for_switch( user_vt ) ).await
	{
		Ok( Ok( () ) ) => tracing::info!( "server: reset tty{user_vt} to KD_TEXT/VT_AUTO" ),
		Ok( Err( e ) ) => tracing::warn!( "server: reset tty{user_vt}: {e}" ),
		Err( e )       => tracing::warn!( "server: reset tty task join: {e}" ),
	}

	// Symmetric to the login path: put the greeter VT in KD_GRAPHICS before
	// the switch so fbcon doesn't repaint the text console in the window
	// between the user's compositor dropping master and the greeter's forge
	// taking it back. Without this the user sees a flash of console text
	// after logout while the new greeter forge spins up.
	match task::spawn_blocking( move || vt::set_graphics_mode( greeter_vt ) ).await
	{
		Ok( Ok( () ) ) => tracing::info!( "server: tty{greeter_vt} → KD_GRAPHICS" ),
		Ok( Err( e ) ) => tracing::warn!( "server: KDSETMODE on tty{greeter_vt}: {e}" ),
		Err( e )       => tracing::warn!( "server: KDSETMODE task join: {e}" ),
	}

	tracing::info!( "server: switching back to greeter VT {greeter_vt}" );
	let back = tokio::time::timeout(
		VT_SWITCH_TIMEOUT,
		task::spawn_blocking( move || -> Result<()>
		{
			vt::switch_via_logind( "seat0", greeter_vt )
		} ),
	).await;
	match back
	{
		Ok( Ok( Ok( () ) ) ) => tracing::info!( "server: SwitchTo seat0 vt={greeter_vt} OK" ),
		Ok( Ok( Err( e ) ) ) => tracing::error!( "server: switch back to greeter VT failed: {e}" ),
		Ok( Err( e ) ) => tracing::error!( "server: greeter VT switch task join: {e}" ),
		Err( _ ) => tracing::warn!(
			"server: switch back to greeter VT timed out after {} s, resetting state to Idle anyway",
			VT_SWITCH_TIMEOUT.as_secs(),
		),
	}

	// Sanity-check: logind's SwitchTo sometimes returns OK without the
	// kernel actually swapping (the dying user session can still be
	// "active" from logind's perspective for a tick). Read the kernel's
	// own view; if it disagrees with what we asked for, fall back to a
	// direct VT_ACTIVATE ioctl which doesn't go through logind at all.
	let mut active_vt = read_active_vt();
	tracing::info!( "server: after SwitchTo, /sys/class/tty/tty0/active = {}", active_vt.as_deref().unwrap_or( "?" ) );
	if active_vt.as_deref() != Some( &format!( "tty{greeter_vt}" )[..] )
	{
		tracing::warn!(
			"server: SwitchTo didn't land on tty{greeter_vt}; falling back to direct VT_ACTIVATE"
		);
		match task::spawn_blocking( move || -> Result<()>
		{
			vt::activate( greeter_vt )?;
			vt::wait_active( greeter_vt )
		} ).await
		{
			Ok( Ok( () ) ) =>
			{
				active_vt = read_active_vt();
				tracing::info!(
					"server: VT_ACTIVATE OK, /sys/class/tty/tty0/active = {}",
					active_vt.as_deref().unwrap_or( "?" ),
				);
			}
			Ok( Err( e ) ) => tracing::error!( "server: VT_ACTIVATE fallback failed: {e}" ),
			Err( e )       => tracing::error!( "server: VT_ACTIVATE fallback task join: {e}" ),
		}
	}

	reset_to_idle( &state, &respawn ).await;
}

fn read_active_vt() -> Option<String>
{
	std::fs::read_to_string( "/sys/class/tty/tty0/active" )
		.ok()
		.map( |s| s.trim().to_string() )
}

async fn pump_auth(
	reader:       &mut ( impl tokio::io::AsyncRead + Unpin ),
	writer:       &mut ( impl tokio::io::AsyncWrite + Unpin ),
	challenge_rx: &mut mpsc::Receiver<( String, bool )>,
	response_tx:  &mpsc::Sender<Option<String>>,
	status_rx:    &mut mpsc::Receiver<AuthStatus>,
	username:     &str,
	target_vt:    u32,
) -> Result<Option<libc::pid_t>>
{
	let mut challenges_open = true;
	loop
	{
		if !challenges_open
		{
			let status = status_rx.recv().await;
			return finalize_status( status, writer, username, target_vt ).await;
		}

		tokio::select!
		{
			maybe_challenge = challenge_rx.recv() =>
			{
				let Some( ( prompt, secret ) ) = maybe_challenge
				else
				{
					challenges_open = false;
					continue;
				};
				write_message( writer, &Response::AuthChallenge { prompt, secret } ).await
					.map_err( |e| anyhow!( "write AuthChallenge: {e}" ) )?;
				let resp: Request = read_message( reader ).await
					.map_err( |e| anyhow!( "read AuthResponse: {e}" ) )?;
				match resp
				{
					Request::AuthResponse { answer } =>
					{
						if response_tx.send( answer ).await.is_err() { challenges_open = false; }
					}
					Request::Cancel => return Ok( None ),
					other => bail!( "expected AuthResponse, got {other:?}" ),
				}
			}
			maybe_status = status_rx.recv() =>
			{
				return finalize_status( maybe_status, writer, username, target_vt ).await;
			}
		}
	}
}

async fn finalize_status(
	status:    Option<AuthStatus>,
	writer:    &mut ( impl tokio::io::AsyncWrite + Unpin ),
	username:  &str,
	target_vt: u32,
) -> Result<Option<libc::pid_t>>
{
	let Some( status ) = status else { return Ok( None ); };
	match status
	{
		AuthStatus::Ok( pid ) =>
		{
			write_message( writer, &Response::AuthAccepted ).await
				.map_err( |e| anyhow!( "write AuthAccepted: {e}" ) )?;
			tracing::info!( "server: auth ok for {username}, session forked on VT {target_vt} (pid {pid})" );
			Ok( Some( pid ) )
		}
		AuthStatus::Failed( reason ) =>
		{
			tracing::info!( "server: auth rejected for {username}: {reason}" );
			write_message( writer, &Response::AuthRejected { reason } ).await
				.map_err( |e| anyhow!( "write AuthRejected: {e}" ) )?;
			Ok( None )
		}
	}
}

struct IpcConv
{
	challenge_tx: mpsc::Sender<( String, bool )>,
	response_rx:  mpsc::Receiver<Option<String>>,
}

impl ConversationHandler for IpcConv
{
	fn prompt_blind( &mut self, prompt: &str ) -> Option<String>
	{
		self.challenge_tx.blocking_send( ( prompt.to_string(), true ) ).ok()?;
		self.response_rx.blocking_recv()?
	}

	fn prompt_echo( &mut self, prompt: &str ) -> Option<String>
	{
		self.challenge_tx.blocking_send( ( prompt.to_string(), false ) ).ok()?;
		self.response_rx.blocking_recv()?
	}
}
