Public dry run dryrun-33643196cbdf

This commit is contained in:
Michel Paulissen 2026-07-03 19:29:17 +02:00
parent 666360b98d
commit 17e52b5a2f
4 changed files with 172 additions and 4 deletions

View file

@ -1,5 +1,5 @@
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::net::{TcpListener, TcpStream, ToSocketAddrs};
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
@ -19,6 +19,7 @@ const DEFAULT_OIDC_ISSUER_URL: &str = "https://auth.michelpaulissen.com";
const BROWSER_CALLBACK_ADDR: &str = "127.0.0.1:45173";
const BROWSER_CALLBACK_PATH: &str = "/callback";
const DEFAULT_BROWSER_LOGIN_CALLBACK_TIMEOUT_SECONDS: u64 = 300;
const DOCTOR_COORDINATOR_TIMEOUT: Duration = Duration::from_millis(500);
#[derive(Clone, Debug, Parser)]
#[command(name = "disasmer", version, arg_required_else_help = true)]
@ -639,10 +640,17 @@ struct ProjectConfig {
fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
let config = read_project_config(&cwd)?;
let coordinator = args.scope.coordinator.or_else(|| {
config
.as_ref()
.and_then(|config| config.coordinator.clone())
});
let coordinator_reachability = coordinator_reachability(coordinator.as_deref());
Ok(json!({
"command": "doctor",
"cwd": cwd,
"coordinator": args.scope.coordinator.or_else(|| config.as_ref().and_then(|config| config.coordinator.clone())),
"coordinator": coordinator,
"coordinator_reachability": coordinator_reachability,
"auth": auth_state_value(),
"project": config,
"dependencies": {
@ -663,6 +671,73 @@ fn doctor_report(args: DoctorArgs, cwd: PathBuf) -> Result<Value> {
}))
}
fn coordinator_reachability(coordinator: Option<&str>) -> Value {
let Some(coordinator) = coordinator else {
return json!({
"checked": false,
"status": "not_configured",
"next_action": "run disasmer login --browser or pass --coordinator"
});
};
match ping_coordinator(coordinator, DOCTOR_COORDINATOR_TIMEOUT) {
Ok(response) => json!({
"checked": true,
"status": "reachable",
"coordinator": coordinator,
"response": response
}),
Err(err) => json!({
"checked": true,
"status": "unreachable",
"coordinator": coordinator,
"error": err.to_string(),
"next_action": "check the coordinator URL, network, and service status"
}),
}
}
fn ping_coordinator(coordinator: &str, timeout: Duration) -> Result<Value> {
let transport_addr = json_line_transport_addr(coordinator);
let socket_addrs = transport_addr
.to_socket_addrs()
.with_context(|| format!("failed to resolve {coordinator} via {transport_addr}"))?;
let mut saw_addr = false;
let mut last_error = None;
for socket_addr in socket_addrs {
saw_addr = true;
match TcpStream::connect_timeout(&socket_addr, timeout) {
Ok(stream) => {
stream
.set_read_timeout(Some(timeout))
.with_context(|| format!("failed to set read timeout for {socket_addr}"))?;
stream
.set_write_timeout(Some(timeout))
.with_context(|| format!("failed to set write timeout for {socket_addr}"))?;
let mut session = JsonLineSession::from_stream(stream)?;
return session.request(json!({ "type": "ping" })).with_context(|| {
format!("coordinator ping failed for {coordinator} via {socket_addr}")
});
}
Err(err) => last_error = Some(err),
}
}
if !saw_addr {
anyhow::bail!(
"failed to resolve any socket address for {coordinator} via {transport_addr}"
);
}
anyhow::bail!(
"failed to connect to {coordinator} via {transport_addr}: {}",
last_error
.map(|err| err.to_string())
.unwrap_or_else(|| "no socket addresses attempted".to_owned())
);
}
fn auth_status_report(args: AuthStatusArgs, cwd: PathBuf) -> Result<Value> {
let config = read_project_config(&cwd)?;
Ok(json!({
@ -1363,6 +1438,20 @@ fn human_report(value: &Value) -> String {
));
}
}
if let Some(reachability) = value.get("coordinator_reachability") {
if let Some(status) = reachability.get("status").and_then(Value::as_str) {
lines.push(format!("coordinator reachability: {status}"));
}
if let Some(error) = reachability.get("error").and_then(Value::as_str) {
lines.push(format!("coordinator error: {error}"));
}
if let Some(response_type) = reachability
.pointer("/response/type")
.and_then(Value::as_str)
{
lines.push(format!("coordinator ping: {response_type}"));
}
}
if let Some(response) = value
.get("response")
.or_else(|| value.get("coordinator_response"))
@ -2724,6 +2813,10 @@ impl JsonLineSession {
let transport_addr = json_line_transport_addr(addr);
let writer = TcpStream::connect(&transport_addr)
.with_context(|| format!("failed to connect to {addr} via {transport_addr}"))?;
Self::from_stream(writer)
}
fn from_stream(writer: TcpStream) -> Result<Self> {
let reader = BufReader::new(writer.try_clone()?);
Ok(Self {
writer,
@ -3198,6 +3291,73 @@ mod tests {
assert_eq!(json_line_transport_addr("127.0.0.1:7999"), "127.0.0.1:7999");
}
#[test]
fn doctor_reports_unchecked_coordinator_reachability_without_config() {
let temp = tempfile::tempdir().unwrap();
let report = doctor_report(
DoctorArgs {
scope: CliScopeArgs {
coordinator: None,
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
},
temp.path().to_path_buf(),
)
.unwrap();
assert_eq!(report["command"], "doctor");
assert!(report["coordinator"].is_null());
assert_eq!(report["coordinator_reachability"]["checked"], false);
assert_eq!(
report["coordinator_reachability"]["status"],
"not_configured"
);
}
#[test]
fn doctor_pings_configured_coordinator() {
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap().to_string();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut reader = BufReader::new(stream.try_clone().unwrap());
let mut line = String::new();
reader.read_line(&mut line).unwrap();
assert!(line.contains("\"type\":\"ping\""));
stream
.write_all(b"{\"type\":\"pong\",\"epoch\":42}\n")
.unwrap();
});
let temp = tempfile::tempdir().unwrap();
let report = doctor_report(
DoctorArgs {
scope: CliScopeArgs {
coordinator: Some(addr.clone()),
tenant: "tenant".to_owned(),
project: "project".to_owned(),
user: "user".to_owned(),
json: false,
},
},
temp.path().to_path_buf(),
)
.unwrap();
server.join().unwrap();
assert_eq!(report["coordinator"], addr);
assert_eq!(report["coordinator_reachability"]["checked"], true);
assert_eq!(report["coordinator_reachability"]["status"], "reachable");
assert_eq!(
report["coordinator_reachability"]["response"]["type"],
"pong"
);
assert_eq!(report["coordinator_reachability"]["response"]["epoch"], 42);
}
#[test]
fn cli_first_mvp_command_surface_parses() {
for args in [