1use anyhow::Error as AnyhowError;
2use anyhow::Result;
3use thiserror::Error;
4
5pub const EXIT_NO_MASTER: i32 = 3;
6pub const EXIT_TIMEOUT: i32 = 4;
7pub const EXIT_ORPHAN: i32 = 5;
8pub const EXIT_DROPPED: i32 = 6;
9
10#[derive(Debug, Error, PartialEq, Eq)]
11pub enum CoopError {
12 #[error(
13 "no control master for {host}\n \
14 run: ssh -MNf -S {socket} -o ControlPersist=8h {target}\n \
15 a human may need to tap a hardware key; ask rather than retrying"
16 )]
17 NoMaster {
18 host: String,
19 socket: String,
20 target: String,
21 },
22 #[error(
23 "the master is down or its one slot is held\n run ssh -O check with coop's configured ControlPath to tell which"
24 )]
25 SessionChannelBusy,
26 #[error(
27 "the local ssh agent is unreachable; ssh-add -l cannot use SSH_AUTH_SOCK\n re-establish or re-attach the agent; an authentication prompt may be waiting where you cannot see it"
28 )]
29 SshAgentUnreachable,
30 #[error(
31 "the local ssh agent has no keys loaded\n add the required key with ssh-add; an authentication prompt may be waiting where you cannot see it"
32 )]
33 SshAgentHasNoKeys,
34 #[error(
35 "keyboard-interactive authentication failed; this is either a local ssh-agent problem or a busy control-master channel\n run ssh-add -l, then ssh -O check with coop's configured ControlPath"
36 )]
37 KeyboardInteractiveAmbiguous,
38 #[error("timed out waiting for job {id}; it is still running")]
39 Timeout { id: String },
40 #[error("job {id} is orphaned; no rc will ever arrive")]
41 Orphan { id: String },
42 #[error("lost contact while waiting; the job continues\n resume: coop tail {id}")]
43 Dropped { id: String },
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum AgentState {
48 Keys,
49 NoKeys,
50 Unreachable,
51 Unknown,
52}
53
54pub fn classify(stderr: &str, agent_state: impl FnOnce() -> AgentState) -> Option<CoopError> {
55 let stderr = stderr.to_ascii_lowercase();
56 if ["session request failed", "session open refused"]
57 .iter()
58 .any(|pattern| stderr.contains(pattern))
59 {
60 return Some(CoopError::SessionChannelBusy);
61 }
62 if !stderr.contains("permission denied (keyboard-interactive)") {
63 return None;
64 }
65 Some(match agent_state() {
66 AgentState::Keys => CoopError::SessionChannelBusy,
67 AgentState::NoKeys => CoopError::SshAgentHasNoKeys,
68 AgentState::Unreachable => CoopError::SshAgentUnreachable,
69 AgentState::Unknown => CoopError::KeyboardInteractiveAmbiguous,
70 })
71}
72
73pub fn exit_code(error: &AnyhowError) -> i32 {
74 match error.downcast_ref::<CoopError>() {
75 Some(CoopError::NoMaster { .. }) => EXIT_NO_MASTER,
76 Some(CoopError::Timeout { .. }) => EXIT_TIMEOUT,
77 Some(CoopError::Orphan { .. }) => EXIT_ORPHAN,
78 Some(CoopError::Dropped { .. }) => EXIT_DROPPED,
79 Some(
80 CoopError::SessionChannelBusy
81 | CoopError::SshAgentUnreachable
82 | CoopError::SshAgentHasNoKeys
83 | CoopError::KeyboardInteractiveAmbiguous,
84 )
85 | None => 1,
86 }
87}
88
89pub fn require_master(
98 t: &dyn crate::transport::Transport,
99 host: &crate::config::Host,
100) -> Result<()> {
101 if t.master_alive(host) {
102 return Ok(());
103 }
104 Err(no_master(host))
105}
106
107pub fn no_master(host: &crate::config::Host) -> AnyhowError {
120 let mut hint = None;
121 if let Some(parent) = host.socket.parent() {
122 if let Err(e) = create_private_dir(parent) {
125 hint = Some(format!("{}: {e}", parent.display()));
126 }
127 }
128 let error: AnyhowError = CoopError::NoMaster {
129 host: host.name.clone(),
130 socket: host.socket.display().to_string(),
131 target: host.target.clone(),
132 }
133 .into();
134 match hint {
135 Some(why) => error.context(format!("cannot prepare the socket directory {why}")),
137 None => error,
138 }
139}
140
141pub fn master_command(host: &crate::config::Host) -> String {
148 if let Some(parent) = host.socket.parent() {
149 let _ = create_private_dir(parent);
150 }
151 format!(
152 "run: ssh -MNf -S {} -o ControlPersist=8h {}",
153 host.socket.display(),
154 host.target
155 )
156}
157
158fn create_private_dir(dir: &std::path::Path) -> std::io::Result<()> {
159 if dir.is_dir() {
160 return Ok(());
161 }
162 std::fs::create_dir_all(dir)?;
163 #[cfg(unix)]
164 {
165 use std::os::unix::fs::PermissionsExt;
166 std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))?;
167 }
168 Ok(())
169}