Add Windows named-pipe local control

This commit is contained in:
Eric Wendland 2026-07-18 17:05:38 +02:00
commit 3c84713472
9 changed files with 338 additions and 72 deletions

View file

@ -87,7 +87,23 @@ impl GethPaths {
#[must_use]
pub fn socket_path(&self) -> PathBuf {
self.run_dir().join("geth.sock")
PathBuf::from(self.control_endpoint())
}
#[must_use]
pub fn control_endpoint(&self) -> String {
#[cfg(unix)]
{
self.run_dir().join("geth.sock").display().to_string()
}
#[cfg(windows)]
{
let home = self.home.display().to_string().to_ascii_lowercase();
format!(
r"\\.\pipe\geth-{:016x}",
stable_control_endpoint_suffix(home.as_bytes())
)
}
}
pub fn ensure_base_dirs(&self) -> Result<(), ConfigError> {
@ -98,6 +114,16 @@ impl GethPaths {
}
}
#[cfg(any(windows, test))]
fn stable_control_endpoint_suffix(bytes: &[u8]) -> u64 {
let mut hash = 0xcbf2_9ce4_8422_2325_u64;
for byte in bytes {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
}
hash
}
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("could not determine OS data directory and GETH_HOME is unset")]
@ -603,6 +629,18 @@ mod tests {
assert!(matches!(error, ConfigError::InvalidLocalDiscovery(_)));
}
#[test]
fn control_endpoint_suffix_is_stable_and_home_scoped() {
assert_eq!(
stable_control_endpoint_suffix(b"C:/Users/Eric/geth"),
stable_control_endpoint_suffix(b"C:/Users/Eric/geth")
);
assert_ne!(
stable_control_endpoint_suffix(b"C:/Users/Eric/geth"),
stable_control_endpoint_suffix(b"C:/Users/Eric/other")
);
}
#[test]
fn config_set_preserves_comments_and_validates_the_result() {
let home = tempfile::tempdir().expect("tempdir");

View file

@ -1,20 +1,20 @@
//! Daemon task orchestration helpers.
use crate::{
LocalNode, NodeError, handle_stream, init_node, mirror_all_kv_stores_to_iroh_docs,
LocalNode, NodeError, handle_stream, init_node, local_transport,
local_transport::LocalListener, mirror_all_kv_stores_to_iroh_docs,
mirror_local_cas_to_iroh_blobs, start_peer_card_lan_discovery, sync::run_live_sync_once,
};
use geth_config::{GethConfig, GethPaths, RelayMode};
use geth_iroh::{EndpointStatus, GethIrohConfig, GethIrohEndpoint, GethRelayMode};
use geth_store::Store;
use std::time::Duration;
use tokio::net::{UnixListener, UnixStream};
use tokio::task::JoinHandle;
pub async fn run_daemon(paths: GethPaths) -> Result<(), NodeError> {
let mut node = init_node(&paths)?;
let listener = bind_control_socket(&paths).await?;
let _socket_guard = ControlSocketGuard {
let _control_guard = ControlEndpointGuard {
paths: paths.clone(),
};
let config = GethConfig::load(&node.paths.config_file())?;
@ -32,7 +32,7 @@ pub async fn run_daemon(paths: GethPaths) -> Result<(), NodeError> {
let serve_result = async {
let _peer_card_lan_discovery = start_peer_card_lan_discovery(&node).await;
tracing::info!(socket = %paths.socket_path().display(), "geth daemon listening");
tracing::info!(endpoint = %paths.control_endpoint(), "geth daemon listening");
serve_local_control(node.clone(), listener).await
}
.await;
@ -47,27 +47,20 @@ pub async fn run_daemon(paths: GethPaths) -> Result<(), NodeError> {
Ok(())
}
struct ControlSocketGuard {
struct ControlEndpointGuard {
paths: GethPaths,
}
impl Drop for ControlSocketGuard {
impl Drop for ControlEndpointGuard {
fn drop(&mut self) {
if let Err(error) = remove_control_socket(&self.paths) {
tracing::warn!(%error, "failed to clean up daemon control socket");
tracing::warn!(%error, "failed to clean up daemon control endpoint");
}
}
}
async fn bind_control_socket(paths: &GethPaths) -> Result<UnixListener, NodeError> {
let socket_path = paths.socket_path();
if socket_path.exists() {
match UnixStream::connect(&socket_path).await {
Ok(_) => return Err(NodeError::DaemonAlreadyRunning(paths.home().to_path_buf())),
Err(_) => remove_control_socket(paths)?,
}
}
match UnixListener::bind(&socket_path) {
async fn bind_control_socket(paths: &GethPaths) -> Result<LocalListener, NodeError> {
match local_transport::bind(paths).await {
Ok(listener) => Ok(listener),
Err(error) if error.kind() == std::io::ErrorKind::AddrInUse => {
Err(NodeError::DaemonAlreadyRunning(paths.home().to_path_buf()))
@ -77,11 +70,7 @@ async fn bind_control_socket(paths: &GethPaths) -> Result<UnixListener, NodeErro
}
fn remove_control_socket(paths: &GethPaths) -> Result<(), NodeError> {
match std::fs::remove_file(paths.socket_path()) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error.into()),
}
local_transport::remove(paths).map_err(NodeError::from)
}
async fn shutdown_iroh_runtime(
@ -115,11 +104,14 @@ fn clear_iroh_runtime_handles(node: &LocalNode) -> Result<(), NodeError> {
Ok(())
}
async fn serve_local_control(node: LocalNode, listener: UnixListener) -> Result<(), NodeError> {
async fn serve_local_control(
node: LocalNode,
mut listener: LocalListener,
) -> Result<(), NodeError> {
loop {
tokio::select! {
accepted = listener.accept() => {
let (stream, _) = accepted?;
let stream = accepted?;
let node = node.clone();
tokio::spawn(async move {
if let Err(error) = handle_stream(node, stream).await {
@ -313,7 +305,45 @@ fn config_relay_mode_to_iroh(
mod tests {
use super::*;
#[tokio::test]
async fn local_control_roundtrip_uses_the_platform_transport() {
let dir = tempfile::tempdir().expect("tempdir");
let paths = GethPaths::from_home(dir.path());
let node = init_node(&paths).expect("init node");
let mut listener = match bind_control_socket(&paths).await {
Ok(listener) => listener,
#[cfg(unix)]
Err(NodeError::Io(error)) if error.kind() == std::io::ErrorKind::PermissionDenied => {
eprintln!("skipping local control roundtrip; Unix sockets unavailable: {error}");
return;
}
Err(error) => panic!("bind control: {error}"),
};
let server = tokio::spawn(async move {
for _ in 0..2 {
let stream = listener.accept().await.expect("accept control");
if handle_stream(node.clone(), stream).await.is_ok() {
return;
}
}
panic!("local control server did not receive a valid request");
});
let second = bind_control_socket(&paths)
.await
.expect_err("second daemon must not replace the live endpoint");
assert!(matches!(second, NodeError::DaemonAlreadyRunning(_)));
let response = crate::send_control(&paths, geth_control::ControlRequest::Status)
.await
.expect("send status");
assert!(matches!(response, geth_control::ControlResponse::Status(_)));
server.await.expect("join control server");
remove_control_socket(&paths).expect("cleanup control endpoint");
}
#[test]
#[cfg(unix)]
fn control_socket_cleanup_is_idempotent() {
let dir = tempfile::tempdir().expect("tempdir");
let paths = GethPaths::from_home(dir.path());
@ -410,6 +440,7 @@ mod tests {
}
#[tokio::test]
#[cfg(unix)]
async fn control_socket_binding_rejects_a_second_daemon_and_recovers_stale_paths() {
let dir = tempfile::tempdir().expect("tempdir");
let paths = GethPaths::from_home(dir.path());

View file

@ -56,15 +56,6 @@ fn check_config(paths: &GethPaths, checks: &mut Vec<DoctorCheck>) {
}
async fn check_daemon(paths: &GethPaths, checks: &mut Vec<DoctorCheck>) {
if !paths.socket_path().exists() {
checks.push(fail(
"daemon-not-running",
format!("daemon socket is absent: {}", paths.socket_path().display()),
"start the daemon with `geth daemon run` or the user service command for your platform",
));
return;
}
match send_control(paths, ControlRequest::Status).await {
Ok(ControlResponse::Status(status)) => checks.push(ok(
"daemon-ok",
@ -79,13 +70,21 @@ async fn check_daemon(paths: &GethPaths, checks: &mut Vec<DoctorCheck>) {
format!("daemon returned unexpected response: {other:?}"),
"restart the daemon and rerun `geth doctor`",
)),
Err(error) => checks.push(fail(
"daemon-stale-socket",
Err(error) if crate::local_transport::endpoint_file_exists(paths) => checks.push(fail(
"daemon-stale-control-endpoint",
format!(
"daemon socket exists but control connection failed: {}",
"daemon control endpoint exists but the connection failed: {}",
error
),
"stop any stale daemon, remove the socket if no daemon is running, then start `geth daemon run`",
"restart the user service or foreground daemon; on Unix, remove the stale socket only after confirming no daemon is running",
)),
Err(error) => checks.push(fail(
"daemon-not-running",
format!(
"daemon control endpoint did not answer at {}: {error}",
paths.control_endpoint()
),
"start the daemon with `geth daemon run` or `geth daemon install`",
)),
}
}
@ -320,6 +319,7 @@ mod tests {
}
#[tokio::test]
#[cfg(unix)]
async fn doctor_reports_bad_config_and_stale_socket() {
let home = tempfile::tempdir().expect("home");
let paths = GethPaths::from_home(home.path());
@ -329,7 +329,7 @@ mod tests {
let report = run_doctor(&paths).await.expect("doctor");
assert!(!report.ok);
assert!(has_code(&report, "config-invalid"));
assert!(has_code(&report, "daemon-stale-socket"));
assert!(has_code(&report, "daemon-stale-control-endpoint"));
}
fn has_code(report: &DoctorReport, code: &str) -> bool {

View file

@ -2,6 +2,7 @@ pub mod backup;
mod daemon;
pub mod doctor;
mod local_control;
mod local_transport;
mod peer_client;
mod peer_control;
mod resource_contracts;
@ -68,6 +69,7 @@ use geth_types::{
use iroh::protocol::ProtocolHandler;
use iroh_docs::api::protocol::{AddrInfoOptions, ShareMode};
pub use local_control::handle_request_async;
use local_transport::LocalStream;
use peer_client::{request_overlay_wire, request_peer_control, request_pipe_wire};
use peer_control::{
IrohAlpnRoute, PeerControlCaller, authenticate_peer_control_caller, classify_iroh_alpn,
@ -85,7 +87,9 @@ use sync::{
KvDocsState, load_live_sync_cursor, store_live_sync_cursor, sync_now, sync_status_local,
};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream, UnixListener, UnixStream};
use tokio::net::{TcpListener, TcpStream};
#[cfg(unix)]
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::{mpsc, oneshot};
use tun_rs::{DeviceBuilder, Layer};
use wire::{
@ -429,7 +433,7 @@ pub async fn send_control(
paths: &GethPaths,
request: ControlRequest,
) -> Result<ControlResponse, NodeError> {
let mut stream = UnixStream::connect(paths.socket_path()).await?;
let mut stream = local_transport::connect(paths).await?;
stream
.write_all(geth_control::encode_request(&request)?.as_bytes())
.await?;
@ -445,7 +449,7 @@ pub async fn stream_ssh_proxy(
peer_node: String,
bearer_secret: Option<String>,
) -> Result<(), NodeError> {
let mut stream = UnixStream::connect(paths.socket_path()).await?;
let mut stream = local_transport::connect(paths).await?;
stream
.write_all(
geth_control::encode_request(&ControlRequest::SshProxyStream {
@ -461,7 +465,7 @@ pub async fn stream_ssh_proxy(
match geth_control::decode_response(&line)? {
ControlResponse::SshProxyConnected { allowed: true, .. } => {
let stream = reader.into_inner();
let (mut local_read, mut local_write) = stream.into_split();
let (mut local_read, mut local_write) = tokio::io::split(stream);
let mut stdin = tokio::io::stdin();
let mut stdout = tokio::io::stdout();
let upload = async {
@ -526,7 +530,7 @@ async fn stream_pipe_tcp(
bearer_secret: Option<String>,
client: TcpStream,
) -> Result<(), NodeError> {
let mut stream = UnixStream::connect(paths.socket_path()).await?;
let mut stream = local_transport::connect(paths).await?;
stream
.write_all(
geth_control::encode_request(&ControlRequest::PipeTcpStream {
@ -543,7 +547,7 @@ async fn stream_pipe_tcp(
match geth_control::decode_response(&line)? {
ControlResponse::PipeRemoteConnected { allowed: true, .. } => {
let daemon_stream = reader.into_inner();
let (mut daemon_read, mut daemon_write) = daemon_stream.into_split();
let (mut daemon_read, mut daemon_write) = tokio::io::split(daemon_stream);
let (mut client_read, mut client_write) = client.into_split();
let upload = async {
tokio::io::copy(&mut client_read, &mut daemon_write).await?;
@ -568,6 +572,7 @@ async fn stream_pipe_tcp(
}
}
#[cfg(unix)]
pub async fn run_unix_forward(
paths: &GethPaths,
listen_path: PathBuf,
@ -605,6 +610,20 @@ pub async fn run_unix_forward(
}
}
#[cfg(not(unix))]
pub async fn run_unix_forward(
_paths: &GethPaths,
_listen_path: PathBuf,
_peer_node: String,
_target_path: PathBuf,
_bearer_secret: Option<String>,
) -> Result<(), NodeError> {
Err(NodeError::UnsupportedPlatform(
geth_pipe::unix_forward_unsupported_message().to_owned(),
))
}
#[cfg(unix)]
async fn stream_pipe_unix(
paths: &GethPaths,
peer_node: String,
@ -612,7 +631,7 @@ async fn stream_pipe_unix(
bearer_secret: Option<String>,
client: UnixStream,
) -> Result<(), NodeError> {
let mut stream = UnixStream::connect(paths.socket_path()).await?;
let mut stream = local_transport::connect(paths).await?;
stream
.write_all(
geth_control::encode_request(&ControlRequest::PipeUnixStream {
@ -629,7 +648,7 @@ async fn stream_pipe_unix(
match geth_control::decode_response(&line)? {
ControlResponse::PipeRemoteConnected { allowed: true, .. } => {
let daemon_stream = reader.into_inner();
let (mut daemon_read, mut daemon_write) = daemon_stream.into_split();
let (mut daemon_read, mut daemon_write) = tokio::io::split(daemon_stream);
let (mut client_read, mut client_write) = client.into_split();
let upload = async {
tokio::io::copy(&mut client_read, &mut daemon_write).await?;
@ -654,7 +673,7 @@ async fn stream_pipe_unix(
}
}
async fn handle_stream(node: LocalNode, stream: UnixStream) -> Result<(), NodeError> {
async fn handle_stream(node: LocalNode, stream: LocalStream) -> Result<(), NodeError> {
let mut reader = BufReader::new(stream);
let mut line = String::new();
reader.read_line(&mut line).await?;
@ -737,6 +756,7 @@ async fn handle_stream(node: LocalNode, stream: UnixStream) -> Result<(), NodeEr
}
return result;
}
#[cfg(unix)]
if let ControlRequest::PipeUnixStream {
node: peer_node,
target_path,
@ -2661,7 +2681,7 @@ async fn handle_local_pipe_tcp_stream(
peer_node: &str,
target_addr: String,
bearer_secret: Option<String>,
local_stream: UnixStream,
local_stream: LocalStream,
) -> Result<(), NodeError> {
geth_pipe::validate_tcp_forward_target_addr(&target_addr)?;
let store = Store::open(&node.paths.metadata_db())?;
@ -2753,7 +2773,7 @@ async fn handle_local_pipe_tcp_stream(
let ControlResponse::PipeRemoteConnected { allowed: true, .. } = local_response else {
return Ok(());
};
let (mut local_read, mut local_write) = local_stream.into_split();
let (mut local_read, mut local_write) = tokio::io::split(local_stream);
let upload = async {
tokio::io::copy(&mut local_read, &mut remote_send)
.await
@ -2773,12 +2793,13 @@ async fn handle_local_pipe_tcp_stream(
Ok(())
}
#[cfg(unix)]
async fn handle_local_pipe_unix_stream(
node: LocalNode,
peer_node: &str,
target_path: PathBuf,
bearer_secret: Option<String>,
local_stream: UnixStream,
local_stream: LocalStream,
) -> Result<(), NodeError> {
let target_path = geth_pipe::validate_unix_forward_path(&target_path)?;
let target_display = target_path.display().to_string();
@ -2871,7 +2892,7 @@ async fn handle_local_pipe_unix_stream(
let ControlResponse::PipeRemoteConnected { allowed: true, .. } = local_response else {
return Ok(());
};
let (mut local_read, mut local_write) = local_stream.into_split();
let (mut local_read, mut local_write) = tokio::io::split(local_stream);
let upload = async {
tokio::io::copy(&mut local_read, &mut remote_send)
.await
@ -2988,7 +3009,7 @@ async fn handle_local_ssh_proxy_stream(
node: LocalNode,
peer_node: &str,
bearer_secret: Option<String>,
local_stream: UnixStream,
local_stream: LocalStream,
) -> Result<(), NodeError> {
let store = Store::open(&node.paths.metadata_db())?;
let stored = store
@ -3076,7 +3097,7 @@ async fn handle_local_ssh_proxy_stream(
let ControlResponse::SshProxyConnected { allowed: true, .. } = local_response else {
return Ok(());
};
let (mut local_read, mut local_write) = local_stream.into_split();
let (mut local_read, mut local_write) = tokio::io::split(local_stream);
let upload = async {
tokio::io::copy(&mut local_read, &mut remote_send)
.await
@ -5519,6 +5540,7 @@ async fn handle_pipe_tcp_wire_connection(
Ok(())
}
#[cfg(unix)]
async fn handle_pipe_unix_wire_connection(
node: LocalNode,
remote_endpoint_id: &str,
@ -5620,6 +5642,24 @@ async fn handle_pipe_unix_wire_connection(
Ok(())
}
#[cfg(not(unix))]
async fn handle_pipe_unix_wire_connection(
_node: LocalNode,
_remote_endpoint_id: &str,
_request: PipeUnixConnectWire,
mut send: iroh::endpoint::SendStream,
_recv: iroh::endpoint::RecvStream,
) -> Result<(), NodeError> {
let response = PipeWireResponse::Error {
message: geth_pipe::unix_forward_unsupported_message().to_owned(),
};
send.write_all(geth_control::encode_pipe_wire_response(&response)?.as_bytes())
.await
.map_err(|error| NodeError::IrohPeer(error.to_string()))?;
send.finish()
.map_err(|error| NodeError::IrohPeer(error.to_string()))
}
fn peer_gossip_endpoint_id(
node: &LocalNode,
peer_node: &str,
@ -12926,7 +12966,7 @@ mod tests {
socket.write_all(b"pong").await.expect("write tcp echo");
socket.shutdown().await.expect("shutdown tcp echo");
});
let (forward_client, forward_daemon) = UnixStream::pair().expect("unix stream pair");
let (forward_client, forward_daemon) = tokio::io::duplex(64 * 1024);
let right_node_id = right_card.node_id.to_string();
let left_for_forward = left.clone();
let tcp_target_for_forward = tcp_target.clone();
@ -12936,7 +12976,7 @@ mod tests {
&right_node_id,
tcp_target_for_forward,
None,
forward_daemon,
Box::new(forward_daemon),
)
.await
});
@ -12958,7 +12998,7 @@ mod tests {
other => panic!("unexpected pipe TCP stream response: {other:?}"),
}
let stream = forward_reader.into_inner();
let (mut forward_read, mut forward_write) = stream.into_split();
let (mut forward_read, mut forward_write) = tokio::io::split(stream);
forward_write
.write_all(b"ping")
.await
@ -13015,7 +13055,7 @@ mod tests {
&right_node_id,
unix_target_for_forward,
None,
unix_forward_daemon,
Box::new(unix_forward_daemon),
)
.await
});

View file

@ -0,0 +1,140 @@
use geth_config::GethPaths;
use std::io;
use tokio::io::{AsyncRead, AsyncWrite};
pub(crate) trait LocalIo: AsyncRead + AsyncWrite + Unpin + Send {}
impl<T> LocalIo for T where T: AsyncRead + AsyncWrite + Unpin + Send {}
pub(crate) type LocalStream = Box<dyn LocalIo>;
#[cfg(unix)]
#[derive(Debug)]
pub(crate) struct LocalListener(tokio::net::UnixListener);
#[cfg(unix)]
pub(crate) async fn bind(paths: &GethPaths) -> io::Result<LocalListener> {
let socket_path = paths.socket_path();
if socket_path.exists() {
match tokio::net::UnixStream::connect(&socket_path).await {
Ok(_) => {
return Err(io::Error::new(
io::ErrorKind::AddrInUse,
"geth local control socket is already accepting connections",
));
}
Err(_) => remove(paths)?,
}
}
tokio::net::UnixListener::bind(socket_path).map(LocalListener)
}
#[cfg(unix)]
pub(crate) async fn connect(paths: &GethPaths) -> io::Result<LocalStream> {
Ok(Box::new(
tokio::net::UnixStream::connect(paths.socket_path()).await?,
))
}
#[cfg(unix)]
impl LocalListener {
pub(crate) async fn accept(&mut self) -> io::Result<LocalStream> {
let (stream, _) = self.0.accept().await?;
Ok(Box::new(stream))
}
}
#[cfg(unix)]
pub(crate) fn remove(paths: &GethPaths) -> io::Result<()> {
match std::fs::remove_file(paths.socket_path()) {
Ok(()) => Ok(()),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error),
}
}
#[cfg(unix)]
pub(crate) fn endpoint_file_exists(paths: &GethPaths) -> bool {
paths.socket_path().exists()
}
#[cfg(windows)]
#[derive(Debug)]
pub(crate) struct LocalListener {
pipe_name: String,
pending: Option<tokio::net::windows::named_pipe::NamedPipeServer>,
}
#[cfg(windows)]
pub(crate) async fn bind(paths: &GethPaths) -> io::Result<LocalListener> {
use tokio::net::windows::named_pipe::{ClientOptions, ServerOptions};
let pipe_name = paths.control_endpoint();
if ClientOptions::new().open(&pipe_name).is_ok() {
return Err(io::Error::new(
io::ErrorKind::AddrInUse,
"geth local control named pipe is already accepting connections",
));
}
let pending = ServerOptions::new()
.first_pipe_instance(true)
.create(&pipe_name)
.map_err(map_windows_bind_error)?;
Ok(LocalListener {
pipe_name,
pending: Some(pending),
})
}
#[cfg(windows)]
fn map_windows_bind_error(error: io::Error) -> io::Error {
if matches!(
error.kind(),
io::ErrorKind::PermissionDenied | io::ErrorKind::AlreadyExists
) {
io::Error::new(io::ErrorKind::AddrInUse, error)
} else {
error
}
}
#[cfg(windows)]
pub(crate) async fn connect(paths: &GethPaths) -> io::Result<LocalStream> {
use tokio::net::windows::named_pipe::ClientOptions;
let pipe_name = paths.control_endpoint();
let mut last_error = None;
for _ in 0..20 {
match ClientOptions::new().open(&pipe_name) {
Ok(client) => return Ok(Box::new(client)),
Err(error) => last_error = Some(error),
}
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
}
Err(last_error.unwrap_or_else(|| {
io::Error::new(io::ErrorKind::NotFound, "geth local control pipe is absent")
}))
}
#[cfg(windows)]
impl LocalListener {
pub(crate) async fn accept(&mut self) -> io::Result<LocalStream> {
use tokio::net::windows::named_pipe::ServerOptions;
let connected = self.pending.as_mut().expect("pending pipe instance");
connected.connect().await?;
let connected = self.pending.take().expect("connected pipe instance");
self.pending = Some(ServerOptions::new().create(&self.pipe_name)?);
Ok(Box::new(connected))
}
}
#[cfg(windows)]
pub(crate) fn remove(_paths: &GethPaths) -> io::Result<()> {
Ok(())
}
#[cfg(windows)]
pub(crate) fn endpoint_file_exists(_paths: &GethPaths) -> bool {
false
}