From 90fe49239a5ce5cf120211a130c014834b847031 Mon Sep 17 00:00:00 2001 From: whackur Date: Tue, 1 Sep 2026 23:06:02 +0900 Subject: [PATCH] feat(status): report web and attach endpoints --- README.md | 2 +- src/application/session_terminals_tests.rs | 9 ++- src/cli/daemon.rs | 7 ++- src/cli/status.rs | 7 ++- src/cli/status_render.rs | 13 +++- src/cli/status_render_tests.rs | 14 +++-- src/cli/status_tests.rs | 44 +++++++++++++- src/daemon/client_tests.rs | 9 ++- src/daemon/one_shot.rs | 51 +++++++++++++++- src/daemon/one_shot_tests.rs | 71 +++++++++++++++++++++- src/daemon/protocol/status.rs | 7 ++- src/daemon/protocol_tests.rs | 25 +++++++- src/daemon/serve.rs | 6 +- src/daemon/serve_tests/harness.rs | 9 ++- src/daemon/serve_tests/status.rs | 3 +- src/daemon/status.rs | 12 ++-- src/daemon/status_tests.rs | 25 ++++++-- 17 files changed, 278 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index 904631c..5d95f5e 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ nightcrow update # reinstall the binary; restart the session afterwards For foreground operation, use `nightcrow`; `nightcrow -d` starts the session in the background and writes its output to `~/.nightcrow/daemon.out`. See [Getting started](docs/getting-started.md) for installation variants, startup panes, disconnects, updates, and build verification. -To inspect a running daemon without attaching, run `nightcrow status [--socket PATH]`. It performs a read-only one-shot query and reports the PID, version, start time, uptime, endpoint, attached clients, repositories, and panes. It exits non-zero when no daemon is running. +To inspect a running daemon without attaching, run `nightcrow status [--socket PATH]`. It performs a read-only one-shot query and reports the PID, version, start time, uptime, web and attach endpoints, attached clients, repositories, and panes. It exits non-zero when no daemon is running. ## Features diff --git a/src/application/session_terminals_tests.rs b/src/application/session_terminals_tests.rs index df4a1d0..e8d7a57 100644 --- a/src/application/session_terminals_tests.rs +++ b/src/application/session_terminals_tests.rs @@ -28,8 +28,13 @@ fn attached(dir: &tempfile::TempDir, repos: &[String]) -> (DaemonSocket, DaemonC let listener = socket.listener().try_clone().expect("clones"); let state = crate::test_util::session_state(repos, dir.path()); let (shutdown_tx, _shutdown_rx) = std::sync::mpsc::sync_channel(1); - let session = - crate::daemon::serve::start(state, socket.path(), shutdown_tx).expect("starts the watcher"); + let session = crate::daemon::serve::start( + state, + socket.path(), + "127.0.0.1:4321".parse().unwrap(), + shutdown_tx, + ) + .expect("starts the watcher"); std::thread::spawn(move || crate::daemon::serve::serve(listener, session)); let client = DaemonClient::connect(socket.path()).expect("attaches"); (socket, client) diff --git a/src/cli/daemon.rs b/src/cli/daemon.rs index 48550c6..7e8be3e 100644 --- a/src/cli/daemon.rs +++ b/src/cli/daemon.rs @@ -114,7 +114,12 @@ pub(crate) fn run_daemon( .listener() .try_clone() .context("cloning the daemon listener")?; - let session = crate::daemon::serve::start(server.session_state(), socket.path(), shutdown_tx)?; + let session = crate::daemon::serve::start( + server.session_state(), + socket.path(), + server.addr(), + shutdown_tx, + )?; std::thread::Builder::new() .name("nightcrow-daemon-accept".into()) .spawn(move || crate::daemon::serve::serve(listener, session)) diff --git a/src/cli/status.rs b/src/cli/status.rs index 06714da..9926346 100644 --- a/src/cli/status.rs +++ b/src/cli/status.rs @@ -81,10 +81,13 @@ fn validate_status(status: &DaemonStatus) -> Result<()> { if status.pid == 0 { bail!("protocol error: malformed status response: PID is zero"); } - if let Ok(endpoint) = &status.endpoint + if status.web_endpoint.is_empty() { + bail!("protocol error: malformed status response: web endpoint is empty"); + } + if let Ok(endpoint) = &status.attach_endpoint && endpoint.is_empty() { - bail!("protocol error: malformed status response: endpoint is empty"); + bail!("protocol error: malformed status response: attach endpoint is empty"); } let mut client_ids = status.attached_clients.clone(); client_ids.sort_unstable(); diff --git a/src/cli/status_render.rs b/src/cli/status_render.rs index 1e0ef5d..e6dabdb 100644 --- a/src/cli/status_render.rs +++ b/src/cli/status_render.rs @@ -14,7 +14,18 @@ pub(super) fn render_status(status: &DaemonStatus) -> String { ) .unwrap(); writeln!(output, "Uptime: {}", format_uptime(status.uptime_ms)).unwrap(); - writeln!(output, "Endpoint: {}", format_endpoint(&status.endpoint)).unwrap(); + writeln!( + output, + "Web endpoint: {}", + display_text(&status.web_endpoint) + ) + .unwrap(); + writeln!( + output, + "Attach endpoint: {}", + format_endpoint(&status.attach_endpoint) + ) + .unwrap(); let mut clients = status.attached_clients.clone(); clients.sort_unstable(); diff --git a/src/cli/status_render_tests.rs b/src/cli/status_render_tests.rs index d4edd03..da382c3 100644 --- a/src/cli/status_render_tests.rs +++ b/src/cli/status_render_tests.rs @@ -10,7 +10,8 @@ fn status() -> DaemonStatus { version: version(), started_at_unix_ms: Ok(1_735_689_723_004), uptime_ms: 90_061_000, - endpoint: Ok("custom.sock".into()), + web_endpoint: "http://127.0.0.1:4321/".into(), + attach_endpoint: Ok("custom.sock".into()), attached_clients: vec![9, 2], repositories: vec![ RepositoryStatus { @@ -35,6 +36,8 @@ fn status_output_sorts_ids_and_repositories_and_names_empty_values() { assert!(output.contains("Status: running")); assert!(output.contains("Started at: 2025-01-01T00:02:03.004Z")); assert!(output.contains("Uptime: 1d 1h 1m 1s")); + assert!(output.contains("Web endpoint: http://127.0.0.1:4321/")); + assert!(output.contains("Attach endpoint: custom.sock")); assert!(output.contains("Attached client IDs: 2, 9")); assert!(output.find("Repository: a") < output.find("Repository: b")); assert!(output.contains(" Pane IDs: 3, 8")); @@ -60,21 +63,22 @@ fn status_output_explains_empty_repository_set() { } #[test] -fn status_output_explains_unavailable_endpoint() { +fn status_output_explains_unavailable_attach_endpoint() { let mut status = status(); - status.endpoint = Err(StatusUnavailable { + status.attach_endpoint = Err(StatusUnavailable { reason: StatusUnavailableReason::EndpointNotUnicode, }); assert!( render_status(&status) - .contains("Endpoint: unavailable (endpoint path is not valid Unicode)") + .contains("Attach endpoint: unavailable (endpoint path is not valid Unicode)") ); } #[test] fn status_output_escapes_control_characters_and_preserves_unicode() { let mut status = status(); - status.endpoint = Ok("sock\u{1b}]0;evil\u{7}\n\u{9b}".into()); + status.web_endpoint = "http://sock\u{1b}]0;evil\u{7}\n\u{9b}/".into(); + status.attach_endpoint = Ok("sock\u{1b}]0;evil\u{7}\n\u{9b}".into()); status.repositories[0].id = "repo-한글\u{1b}".into(); status.repositories[0].path = "C:\\work\n\u{80}".into(); let output = render_status(&status); diff --git a/src/cli/status_tests.rs b/src/cli/status_tests.rs index 24e00d4..203732e 100644 --- a/src/cli/status_tests.rs +++ b/src/cli/status_tests.rs @@ -52,7 +52,8 @@ fn a_version_mismatch_is_reported_as_a_version_error() { version: "old".into(), started_at_unix_ms: Ok(0), uptime_ms: 0, - endpoint: Ok("sock".into()), + web_endpoint: "http://127.0.0.1:4321/".into(), + attach_endpoint: Ok("sock".into()), repositories: vec![], attached_clients: vec![], }; @@ -77,7 +78,8 @@ fn malformed_status_facts_are_rejected_before_rendering() { version: version(), started_at_unix_ms: Ok(0), uptime_ms: 0, - endpoint: Ok("sock".into()), + web_endpoint: "http://127.0.0.1:4321/".into(), + attach_endpoint: Ok("sock".into()), repositories: vec![RepositoryStatus { id: "repo".into(), path: "/repo".into(), @@ -89,3 +91,41 @@ fn malformed_status_facts_are_rejected_before_rendering() { let error = validate_status(&status).unwrap_err(); assert!(error.to_string().contains("malformed status"), "{error:#}"); } + +#[test] +fn an_empty_web_endpoint_is_rejected_before_rendering() { + let status = DaemonStatus { + pid: 1, + version: version(), + started_at_unix_ms: Ok(0), + uptime_ms: 0, + web_endpoint: String::new(), + attach_endpoint: Ok("sock".into()), + repositories: vec![], + attached_clients: vec![], + }; + let error = validate_status(&status).unwrap_err(); + assert!( + error.to_string().contains("web endpoint is empty"), + "{error:#}" + ); +} + +#[test] +fn an_empty_attach_endpoint_is_rejected_before_rendering() { + let status = DaemonStatus { + pid: 1, + version: version(), + started_at_unix_ms: Ok(0), + uptime_ms: 0, + web_endpoint: "http://127.0.0.1:4321/".into(), + attach_endpoint: Ok(String::new()), + repositories: vec![], + attached_clients: vec![], + }; + let error = validate_status(&status).unwrap_err(); + assert!( + error.to_string().contains("attach endpoint is empty"), + "{error:#}" + ); +} diff --git a/src/daemon/client_tests.rs b/src/daemon/client_tests.rs index b86d53e..c89a49f 100644 --- a/src/daemon/client_tests.rs +++ b/src/daemon/client_tests.rs @@ -35,8 +35,13 @@ fn daemon(dir: &tempfile::TempDir, repos: &[String]) -> TestDaemon { }, )); let (shutdown_tx, _shutdown_rx) = std::sync::mpsc::sync_channel(1); - let session = - crate::daemon::serve::start(state, socket.path(), shutdown_tx).expect("starts the watcher"); + let session = crate::daemon::serve::start( + state, + socket.path(), + "127.0.0.1:4321".parse().unwrap(), + shutdown_tx, + ) + .expect("starts the watcher"); std::thread::spawn(move || crate::daemon::serve::serve(listener, session)); TestDaemon { socket } } diff --git a/src/daemon/one_shot.rs b/src/daemon/one_shot.rs index 552788e..5b6b074 100644 --- a/src/daemon/one_shot.rs +++ b/src/daemon/one_shot.rs @@ -9,10 +9,40 @@ use super::frame::{Frame, FrameKind, read_frame, write_frame}; use super::protocol::{ClientMessage, ServerMessage}; use super::transport::UnixStream; use anyhow::{Context, Result, bail}; +use serde::Deserialize; use std::io::Write; use std::path::Path; use std::time::Duration; +/// The status shape emitted before the web and attach endpoints were split. +/// It is intentionally private and only used to turn a precise, same-version +/// compatibility failure into an actionable error at the one-shot boundary. +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct LegacyStatusResponse { + #[serde(rename = "type")] + message_type: String, + status: LegacyDaemonStatus, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct LegacyDaemonStatus { + #[serde(rename = "pid")] + _pid: u32, + version: String, + #[serde(rename = "started_at_unix_ms")] + _started_at_unix_ms: Result, + #[serde(rename = "uptime_ms")] + _uptime_ms: u64, + #[serde(rename = "endpoint")] + _endpoint: Result, + #[serde(rename = "repositories")] + _repositories: Vec, + #[serde(rename = "attached_clients")] + _attached_clients: Vec, +} + pub(crate) fn connect(path: &Path) -> std::io::Result { UnixStream::connect(path) } @@ -44,7 +74,26 @@ pub(crate) fn request( frame.kind ); } - serde_json::from_slice(&frame.payload).context("protocol error: malformed daemon response JSON") + match serde_json::from_slice(&frame.payload) { + Ok(response) => Ok(response), + Err(error) => { + if matches!(request, ClientMessage::Status {}) + && is_legacy_status_response(&frame.payload) + { + bail!( + "protocol incompatibility: daemon status response uses the legacy endpoint field; restart the daemon after updating nightcrow" + ); + } + Err(error).context("protocol error: malformed daemon response JSON") + } + } +} + +fn is_legacy_status_response(payload: &[u8]) -> bool { + let Ok(response) = serde_json::from_slice::(payload) else { + return false; + }; + response.message_type == "status" && response.status.version == super::protocol::version() } #[cfg(test)] diff --git a/src/daemon/one_shot_tests.rs b/src/daemon/one_shot_tests.rs index e30bd9f..c8168a8 100644 --- a/src/daemon/one_shot_tests.rs +++ b/src/daemon/one_shot_tests.rs @@ -20,7 +20,8 @@ fn one_shot_request_uses_the_configured_endpoint_and_reads_one_typed_response() version: "test".into(), started_at_unix_ms: Ok(1), uptime_ms: 2, - endpoint: Ok("status.sock".into()), + web_endpoint: "http://127.0.0.1:4321/".into(), + attach_endpoint: Ok("status.sock".into()), repositories: vec![], attached_clients: vec![], }; @@ -43,6 +44,74 @@ fn one_shot_request_uses_the_configured_endpoint_and_reads_one_typed_response() server.join().unwrap(); } +#[test] +fn a_legacy_status_response_reports_that_the_daemon_must_be_restarted() { + let dir = tempfile::TempDir::new().unwrap(); + let path = dir.path().join("status.sock"); + let socket = DaemonSocket::bind(&path).unwrap(); + let listener = socket.listener().try_clone().unwrap(); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let _ = read_frame(&mut stream).unwrap(); + let response = format!( + r#"{{"type":"status","status":{{"pid":7,"version":"{}","started_at_unix_ms":{{"Ok":1}},"uptime_ms":2,"endpoint":{{"Ok":"status.sock"}},"repositories":[],"attached_clients":[]}}}}"#, + crate::daemon::protocol::version() + ); + write_frame(&mut stream, &Frame::control(response.into_bytes())).unwrap(); + stream.flush().unwrap(); + }); + + let error = request( + &path, + &ClientMessage::Status {}, + std::time::Duration::from_secs(1), + ) + .unwrap_err(); + + assert!( + error.to_string().contains("protocol incompatibility"), + "{error:#}" + ); + assert!(error.to_string().contains("legacy endpoint"), "{error:#}"); + assert!( + error.to_string().contains("restart the daemon"), + "{error:#}" + ); + server.join().unwrap(); +} + +#[test] +fn an_incomplete_legacy_status_marker_remains_a_malformed_response() { + let dir = tempfile::TempDir::new().unwrap(); + let path = dir.path().join("status.sock"); + let socket = DaemonSocket::bind(&path).unwrap(); + let listener = socket.listener().try_clone().unwrap(); + let server = thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let _ = read_frame(&mut stream).unwrap(); + let response = format!( + r#"{{"type":"status","status":{{"pid":7,"version":"{}","started_at_unix_ms":{{"Ok":1}},"uptime_ms":2,"endpoint":7,"repositories":[],"attached_clients":[]}}}}"#, + crate::daemon::protocol::version() + ); + write_frame(&mut stream, &Frame::control(response.into_bytes())).unwrap(); + stream.flush().unwrap(); + }); + + let error = request( + &path, + &ClientMessage::Status {}, + std::time::Duration::from_secs(1), + ) + .unwrap_err(); + + assert!( + error.to_string().contains("malformed daemon response JSON"), + "{error:#}" + ); + assert!(!error.to_string().contains("protocol incompatibility")); + server.join().unwrap(); +} + #[test] fn a_terminal_response_is_a_wire_error() { let dir = tempfile::TempDir::new().unwrap(); diff --git a/src/daemon/protocol/status.rs b/src/daemon/protocol/status.rs index 455ab57..8515a5e 100644 --- a/src/daemon/protocol/status.rs +++ b/src/daemon/protocol/status.rs @@ -9,8 +9,11 @@ pub struct DaemonStatus { pub version: String, pub started_at_unix_ms: Result, pub uptime_ms: u64, - /// The socket endpoint, or why it could not be represented as text. - pub endpoint: Result, + /// The HTTP endpoint the viewer listener bound at runtime. + pub web_endpoint: String, + /// The attach socket endpoint, or why its path could not be represented as + /// text. + pub attach_endpoint: Result, pub repositories: Vec, /// Attach protocol client ids only. Terminal-hub connection ids are a /// different namespace and deliberately do not appear here. diff --git a/src/daemon/protocol_tests.rs b/src/daemon/protocol_tests.rs index 3555b4e..de527c5 100644 --- a/src/daemon/protocol_tests.rs +++ b/src/daemon/protocol_tests.rs @@ -59,7 +59,8 @@ fn every_server_message_survives_the_round_trip() { version: "0.1.0".into(), started_at_unix_ms: Ok(123), uptime_ms: 7, - endpoint: Ok("/tmp/nightcrow.sock".into()), + web_endpoint: "http://127.0.0.1:4321/".into(), + attach_endpoint: Ok("/tmp/nightcrow.sock".into()), repositories: vec![RepositoryStatus { id: "r1".into(), path: "/w/repo".into(), @@ -115,12 +116,29 @@ fn status_rejects_unknown_request_fields_and_missing_response_fields() { assert!(serde_json::from_str::(r#"{"type":"status","pid":1}"#).is_err()); let missing_uptime = r#"{ "type":"status","status":{"pid":1,"version":"0.1.0", - "started_at_unix_ms":{"Ok":1},"endpoint":"/tmp/d.sock", + "started_at_unix_ms":{"Ok":1},"web_endpoint":"http://127.0.0.1:4321/", + "attach_endpoint":"/tmp/d.sock", "repositories":[],"attached_clients":[]}} "#; assert!(serde_json::from_str::(missing_uptime).is_err()); } +#[test] +fn an_old_status_shape_is_rejected_even_when_the_build_version_matches() { + // Status fields are required by the current protocol, so a same-version + // daemon from before this field split fails closed rather than losing the + // distinction between its web and attach endpoints. + let old_shape = format!( + r#"{{ + "type":"status","status":{{"pid":1,"version":"{}", + "started_at_unix_ms":{{"Ok":1}},"uptime_ms":0,"endpoint":{{"Ok":"/tmp/d.sock"}}, + "repositories":[],"attached_clients":[]}} + }}"#, + version() + ); + assert!(serde_json::from_str::(&old_shape).is_err()); +} + #[test] fn an_unavailable_endpoint_reason_survives_the_status_round_trip() { let status = DaemonStatus { @@ -128,7 +146,8 @@ fn an_unavailable_endpoint_reason_survives_the_status_round_trip() { version: version(), started_at_unix_ms: Ok(0), uptime_ms: 0, - endpoint: Err(super::StatusUnavailable { + web_endpoint: "http://127.0.0.1:4321/".into(), + attach_endpoint: Err(super::StatusUnavailable { reason: super::StatusUnavailableReason::EndpointNotUnicode, }), repositories: vec![], diff --git a/src/daemon/serve.rs b/src/daemon/serve.rs index 43de5d8..d8de48b 100644 --- a/src/daemon/serve.rs +++ b/src/daemon/serve.rs @@ -15,6 +15,7 @@ use crate::platform::signals::Shutdown; use crate::session; use crate::session::SessionState; use std::collections::HashMap; +use std::net::SocketAddr; use std::path::Path; use std::sync::mpsc::SyncSender; use std::sync::{Arc, Mutex}; @@ -90,7 +91,8 @@ impl Session { /// [`DaemonSocket`]: super::socket::DaemonSocket pub fn start( state: Arc, - endpoint: &Path, + attach_endpoint: &Path, + web_addr: SocketAddr, shutdown_tx: SyncSender, ) -> anyhow::Result> { let session = Arc::new(Session { @@ -99,7 +101,7 @@ pub fn start( bridges: Mutex::new(HashMap::new()), nudge: Arc::new(super::watch::Nudge::default()), shutdown_tx, - metadata: super::status::DaemonMetadata::capture(endpoint), + metadata: super::status::DaemonMetadata::capture(attach_endpoint, web_addr), admission: Arc::new(admission::PreAttachAdmission::new( MAX_PRE_ATTACH_CONNECTIONS, )), diff --git a/src/daemon/serve_tests/harness.rs b/src/daemon/serve_tests/harness.rs index eb9614d..35123a6 100644 --- a/src/daemon/serve_tests/harness.rs +++ b/src/daemon/serve_tests/harness.rs @@ -39,8 +39,13 @@ pub(super) fn daemon(dir: &tempfile::TempDir, repos: &[String]) -> TestDaemon { let state = crate::test_util::session_state(repos, dir.path()); let served = std::sync::Arc::clone(&state); let (shutdown_tx, shutdown_rx) = std::sync::mpsc::sync_channel(1); - let session = crate::daemon::serve::start(served, socket.path(), shutdown_tx) - .expect("starts the watcher"); + let session = crate::daemon::serve::start( + served, + socket.path(), + "127.0.0.1:4321".parse().unwrap(), + shutdown_tx, + ) + .expect("starts the watcher"); let serving = std::sync::Arc::clone(&session); std::thread::spawn(move || crate::daemon::serve::serve(listener, serving)); TestDaemon { diff --git a/src/daemon/serve_tests/status.rs b/src/daemon/serve_tests/status.rs index 5aa4415..09546fa 100644 --- a/src/daemon/serve_tests/status.rs +++ b/src/daemon/serve_tests/status.rs @@ -36,9 +36,10 @@ fn status_is_authoritative_and_does_not_attach_or_mutate_the_session() { assert_eq!(status.pid, std::process::id()); assert_eq!(status.version, version()); assert_eq!( - status.endpoint.as_deref(), + status.attach_endpoint.as_deref(), Ok(daemon.path().to_str().expect("test path is Unicode")) ); + assert_eq!(status.web_endpoint, "http://127.0.0.1:4321/"); assert_eq!(status.attached_clients, vec![attached_id]); assert_eq!(status.repositories.len(), 1); assert_eq!(status.repositories[0].path, resolved(&path)); diff --git a/src/daemon/status.rs b/src/daemon/status.rs index d726dfa..ced2e01 100644 --- a/src/daemon/status.rs +++ b/src/daemon/status.rs @@ -1,5 +1,6 @@ use super::protocol::{DaemonStatus, RepositoryStatus, StatusUnavailable, StatusUnavailableReason}; use super::serve::Session; +use std::net::SocketAddr; use std::path::Path; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -9,17 +10,19 @@ pub(super) struct DaemonMetadata { version: String, started_at: SystemTime, started_mono: Instant, - endpoint: Result, + web_endpoint: String, + attach_endpoint: Result, } impl DaemonMetadata { - pub(super) fn capture(endpoint: &Path) -> Self { + pub(super) fn capture(attach_endpoint: &Path, web_addr: SocketAddr) -> Self { Self { pid: std::process::id(), version: super::protocol::version(), started_at: SystemTime::now(), started_mono: Instant::now(), - endpoint: endpoint + web_endpoint: format!("http://{web_addr}/"), + attach_endpoint: attach_endpoint .to_str() .map(str::to_owned) .ok_or(StatusUnavailable { @@ -45,7 +48,8 @@ impl DaemonMetadata { version: self.version.clone(), started_at_unix_ms: unix_millis(self.started_at), uptime_ms: millis(self.started_mono.elapsed()), - endpoint: self.endpoint.clone(), + web_endpoint: self.web_endpoint.clone(), + attach_endpoint: self.attach_endpoint.clone(), repositories, attached_clients: session.clients.ids(), } diff --git a/src/daemon/status_tests.rs b/src/daemon/status_tests.rs index 583da69..745243c 100644 --- a/src/daemon/status_tests.rs +++ b/src/daemon/status_tests.rs @@ -7,19 +7,36 @@ fn a_non_unicode_endpoint_is_reported_as_unavailable() { use std::os::unix::ffi::OsStringExt; let path = std::path::PathBuf::from(OsString::from_vec(vec![b'd', b'.', 0xff])); - let metadata = DaemonMetadata::capture(&path); + let metadata = + DaemonMetadata::capture(&path, std::net::SocketAddr::from(([127, 0, 0, 1], 4321))); assert_eq!( - metadata.endpoint, + metadata.attach_endpoint, Err(StatusUnavailable { reason: StatusUnavailableReason::EndpointNotUnicode, }) ); + assert_eq!(metadata.web_endpoint, "http://127.0.0.1:4321/"); +} + +#[test] +fn an_ipv6_web_endpoint_is_rendered_with_url_brackets() { + let metadata = DaemonMetadata::capture( + std::path::Path::new("d.sock"), + std::net::SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 4321)), + ); + + assert_eq!(metadata.web_endpoint, "http://[::1]:4321/"); } #[cfg(windows)] #[test] fn a_unicode_endpoint_is_preserved_exactly() { let path = std::path::Path::new(r"C:\nightcrow\한글.sock"); - let metadata = DaemonMetadata::capture(path); - assert_eq!(metadata.endpoint, Ok(path.to_str().unwrap().to_owned())); + let metadata = + DaemonMetadata::capture(path, std::net::SocketAddr::from(([127, 0, 0, 1], 4321))); + assert_eq!( + metadata.attach_endpoint, + Ok(path.to_str().unwrap().to_owned()) + ); + assert_eq!(metadata.web_endpoint, "http://127.0.0.1:4321/"); }