|
5 | 5 | //! whose stdin/stdout become the protocol channel. |
6 | 6 | use std::fmt; |
7 | 7 | use std::future::Future; |
8 | | -use std::path::PathBuf; |
| 8 | +use std::path::{Path, PathBuf}; |
9 | 9 | use std::pin::Pin; |
10 | 10 | use std::sync::Arc; |
11 | 11 |
|
12 | | -use anyhow::Result; |
13 | | -use warpui::r#async::executor; |
| 12 | +use anyhow::{anyhow, Result}; |
| 13 | +use warpui::r#async::{executor, FutureExt as _}; |
14 | 14 |
|
15 | 15 | use remote_server::auth::RemoteServerAuthContext; |
16 | 16 | use remote_server::client::RemoteServerClient; |
@@ -74,30 +74,174 @@ impl SshTransport { |
74 | 74 | } |
75 | 75 | } |
76 | 76 |
|
| 77 | +#[derive(Debug)] |
| 78 | +enum InstallError { |
| 79 | + ScriptFailed { exit_code: i32, stderr: String }, |
| 80 | + Other(anyhow::Error), |
| 81 | +} |
| 82 | + |
| 83 | +impl fmt::Display for InstallError { |
| 84 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 85 | + match self { |
| 86 | + Self::ScriptFailed { exit_code, stderr } => { |
| 87 | + write!(f, "install script failed (exit {exit_code}): {stderr}") |
| 88 | + } |
| 89 | + Self::Other(error) => write!(f, "{error:#}"), |
| 90 | + } |
| 91 | + } |
| 92 | +} |
| 93 | + |
| 94 | +impl From<anyhow::Error> for InstallError { |
| 95 | + fn from(error: anyhow::Error) -> Self { |
| 96 | + Self::Other(error) |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +async fn detect_remote_platform(socket_path: &Path) -> Result<RemotePlatform> { |
| 101 | + let output = remote_server::ssh::run_ssh_command( |
| 102 | + socket_path, |
| 103 | + "uname -sm", |
| 104 | + remote_server::setup::CHECK_TIMEOUT, |
| 105 | + ) |
| 106 | + .await?; |
| 107 | + |
| 108 | + if output.status.success() { |
| 109 | + let stdout = String::from_utf8_lossy(&output.stdout); |
| 110 | + return parse_uname_output(&stdout); |
| 111 | + } |
| 112 | + |
| 113 | + let code = output.status.code().unwrap_or(-1); |
| 114 | + let stderr = String::from_utf8_lossy(&output.stderr); |
| 115 | + Err(anyhow!("uname -sm exited with code {code}: {stderr}")) |
| 116 | +} |
| 117 | + |
| 118 | +async fn verify_installed_binary(socket_path: &Path) -> Result<()> { |
| 119 | + let output = remote_server::ssh::run_ssh_command( |
| 120 | + socket_path, |
| 121 | + &remote_server::setup::binary_check_command(), |
| 122 | + remote_server::setup::CHECK_TIMEOUT, |
| 123 | + ) |
| 124 | + .await?; |
| 125 | + |
| 126 | + if output.status.success() { |
| 127 | + return Ok(()); |
| 128 | + } |
| 129 | + |
| 130 | + let code = output.status.code().unwrap_or(-1); |
| 131 | + let stderr = String::from_utf8_lossy(&output.stderr); |
| 132 | + Err(anyhow!( |
| 133 | + "installed binary check failed with code {code}: {stderr}" |
| 134 | + )) |
| 135 | +} |
| 136 | + |
| 137 | +async fn run_install_script( |
| 138 | + socket_path: &Path, |
| 139 | + staging_tarball_path: Option<&str>, |
| 140 | + timeout: std::time::Duration, |
| 141 | +) -> core::result::Result<(), InstallError> { |
| 142 | + let script = remote_server::setup::install_script(staging_tarball_path); |
| 143 | + match remote_server::ssh::run_ssh_script(socket_path, &script, timeout).await { |
| 144 | + Ok(output) if output.status.success() => Ok(()), |
| 145 | + Ok(output) => { |
| 146 | + let exit_code = output.status.code().unwrap_or(-1); |
| 147 | + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); |
| 148 | + Err(InstallError::ScriptFailed { exit_code, stderr }) |
| 149 | + } |
| 150 | + Err(error) => Err(InstallError::Other(error)), |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +fn should_skip_scp_fallback(error: &InstallError) -> bool { |
| 155 | + matches!(error, InstallError::ScriptFailed { exit_code: 2, .. }) |
| 156 | +} |
| 157 | + |
| 158 | +async fn download_remote_server_tarball(download_url: &str, tarball_path: &Path) -> Result<()> { |
| 159 | + let output = async { |
| 160 | + command::r#async::Command::new("curl") |
| 161 | + .arg("-fSL") |
| 162 | + .arg("--connect-timeout") |
| 163 | + .arg("15") |
| 164 | + .arg(download_url) |
| 165 | + .arg("-o") |
| 166 | + .arg(tarball_path.as_os_str()) |
| 167 | + .kill_on_drop(true) |
| 168 | + .output() |
| 169 | + .await |
| 170 | + } |
| 171 | + .with_timeout(remote_server::setup::SCP_INSTALL_TIMEOUT) |
| 172 | + .await |
| 173 | + .map_err(|_| { |
| 174 | + anyhow!( |
| 175 | + "local tarball download timed out after {:?}", |
| 176 | + remote_server::setup::SCP_INSTALL_TIMEOUT |
| 177 | + ) |
| 178 | + })? |
| 179 | + .map_err(|e| anyhow!("local curl failed to execute: {e}"))?; |
| 180 | + |
| 181 | + if output.status.success() { |
| 182 | + return Ok(()); |
| 183 | + } |
| 184 | + |
| 185 | + let code = output.status.code().unwrap_or(-1); |
| 186 | + let stderr = String::from_utf8_lossy(&output.stderr); |
| 187 | + Err(anyhow!( |
| 188 | + "local tarball download failed with code {code}: {stderr}" |
| 189 | + )) |
| 190 | +} |
| 191 | + |
| 192 | +async fn scp_install_fallback(socket_path: &Path) -> Result<()> { |
| 193 | + let platform = detect_remote_platform(socket_path).await?; |
| 194 | + let download_url = remote_server::setup::download_tarball_url(&platform); |
| 195 | + let remote_server_dir = remote_server::setup::remote_server_dir(); |
| 196 | + let mkdir_cmd = format!("mkdir -p {remote_server_dir}"); |
| 197 | + let mkdir_output = remote_server::ssh::run_ssh_command( |
| 198 | + socket_path, |
| 199 | + &mkdir_cmd, |
| 200 | + remote_server::setup::CHECK_TIMEOUT, |
| 201 | + ) |
| 202 | + .await?; |
| 203 | + |
| 204 | + if !mkdir_output.status.success() { |
| 205 | + let code = mkdir_output.status.code().unwrap_or(-1); |
| 206 | + let stderr = String::from_utf8_lossy(&mkdir_output.stderr); |
| 207 | + return Err(anyhow!( |
| 208 | + "remote-server dir creation failed with code {code}: {stderr}" |
| 209 | + )); |
| 210 | + } |
| 211 | + |
| 212 | + let tempdir = tempfile::tempdir()?; |
| 213 | + let tarball_path = tempdir.path().join("openwarp.tar.gz"); |
| 214 | + download_remote_server_tarball(&download_url, &tarball_path).await?; |
| 215 | + |
| 216 | + let remote_tarball_path = format!("{remote_server_dir}/openwarp-upload.tar.gz"); |
| 217 | + remote_server::ssh::scp_upload( |
| 218 | + socket_path, |
| 219 | + &tarball_path, |
| 220 | + &remote_tarball_path, |
| 221 | + remote_server::setup::SCP_INSTALL_TIMEOUT, |
| 222 | + ) |
| 223 | + .await?; |
| 224 | + |
| 225 | + run_install_script( |
| 226 | + socket_path, |
| 227 | + Some(&remote_tarball_path), |
| 228 | + remote_server::setup::SCP_INSTALL_TIMEOUT, |
| 229 | + ) |
| 230 | + .await |
| 231 | + .map_err(|error| anyhow!("staged install failed: {error}"))?; |
| 232 | + |
| 233 | + verify_installed_binary(socket_path).await |
| 234 | +} |
| 235 | + |
77 | 236 | impl RemoteTransport for SshTransport { |
78 | 237 | fn detect_platform( |
79 | 238 | &self, |
80 | 239 | ) -> Pin<Box<dyn Future<Output = Result<RemotePlatform, String>> + Send>> { |
81 | 240 | let socket_path = self.socket_path.clone(); |
82 | 241 | Box::pin(async move { |
83 | | - match remote_server::ssh::run_ssh_command( |
84 | | - &socket_path, |
85 | | - "uname -sm", |
86 | | - remote_server::setup::CHECK_TIMEOUT, |
87 | | - ) |
88 | | - .await |
89 | | - { |
90 | | - Ok(output) if output.status.success() => { |
91 | | - let stdout = String::from_utf8_lossy(&output.stdout); |
92 | | - parse_uname_output(&stdout).map_err(|e| format!("{e:#}")) |
93 | | - } |
94 | | - Ok(output) => { |
95 | | - let code = output.status.code().unwrap_or(-1); |
96 | | - let stderr = String::from_utf8_lossy(&output.stderr); |
97 | | - Err(format!("uname -sm exited with code {code}: {stderr}")) |
98 | | - } |
99 | | - Err(e) => Err(format!("{e:#}")), |
100 | | - } |
| 242 | + detect_remote_platform(&socket_path) |
| 243 | + .await |
| 244 | + .map_err(|e| format!("{e:#}")) |
101 | 245 | }) |
102 | 246 | } |
103 | 247 |
|
@@ -141,11 +285,11 @@ impl RemoteTransport for SshTransport { |
141 | 285 | ) |
142 | 286 | .await |
143 | 287 | { |
144 | | - // `test -x` exits 0 when present, 1 when missing. |
145 | | - // Any other exit code (or None / signal) is treated as a check failure. |
| 288 | + // `{binary} --version` 退出 0 表示存在且可运行。 |
| 289 | + // 126/127 表示缺失或不可执行;其他非 0 退出视为真实检查失败。 |
146 | 290 | Ok(output) => match output.status.code() { |
147 | 291 | Some(0) => Ok(true), |
148 | | - Some(1) => Ok(false), |
| 292 | + Some(126) | Some(127) => Ok(false), |
149 | 293 | Some(code) => { |
150 | 294 | let stderr = String::from_utf8_lossy(&output.stderr); |
151 | 295 | Err(format!("binary check exited with code {code}: {stderr}")) |
@@ -193,25 +337,26 @@ impl RemoteTransport for SshTransport { |
193 | 337 | fn install_binary(&self) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send>> { |
194 | 338 | let socket_path = self.socket_path.clone(); |
195 | 339 | Box::pin(async move { |
196 | | - let script = remote_server::setup::install_script(); |
197 | 340 | log::info!( |
198 | 341 | "Installing remote server binary to {}", |
199 | 342 | remote_server::setup::remote_server_binary() |
200 | 343 | ); |
201 | | - match remote_server::ssh::run_ssh_script( |
202 | | - &socket_path, |
203 | | - &script, |
204 | | - remote_server::setup::INSTALL_TIMEOUT, |
205 | | - ) |
206 | | - .await |
| 344 | + match run_install_script(&socket_path, None, remote_server::setup::INSTALL_TIMEOUT) |
| 345 | + .await |
207 | 346 | { |
208 | | - Ok(output) if output.status.success() => Ok(()), |
209 | | - Ok(output) => { |
210 | | - let code = output.status.code().unwrap_or(-1); |
211 | | - let stderr = String::from_utf8_lossy(&output.stderr); |
212 | | - Err(format!("install script failed (exit {code}): {stderr}")) |
| 347 | + Ok(()) => verify_installed_binary(&socket_path) |
| 348 | + .await |
| 349 | + .map_err(|error| format!("{error:#}")), |
| 350 | + Err(error) if should_skip_scp_fallback(&error) => Err(error.to_string()), |
| 351 | + Err(error) => { |
| 352 | + log::warn!("remote-server install failed, trying SCP fallback: {error}"); |
| 353 | + match scp_install_fallback(&socket_path).await { |
| 354 | + Ok(()) => Ok(()), |
| 355 | + Err(fallback_error) => { |
| 356 | + Err(format!("{error}; SCP fallback failed: {fallback_error:#}")) |
| 357 | + } |
| 358 | + } |
213 | 359 | } |
214 | | - Err(e) => Err(format!("{e:#}")), |
215 | 360 | } |
216 | 361 | }) |
217 | 362 | } |
|
0 commit comments