Skip to content

Commit e178fd3

Browse files
committed
修复 OpenWarp SSH CLI 安装资产
1 parent a8b72ee commit e178fd3

8 files changed

Lines changed: 418 additions & 178 deletions

File tree

.github/workflows/openwarp_release.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,30 @@ jobs:
150150
if-no-files-found: error
151151
retention-days: 7
152152

153+
- name: Build CLI
154+
id: bundle_cli
155+
run: |
156+
script/bundle --channel "$CHANNEL" --arch ${{ matrix.arch }} --artifact cli --nosign
157+
shell: bash
158+
env:
159+
GIT_RELEASE_TAG: ${{ needs.prepare_metadata.outputs.release_tag }}
160+
161+
- name: Package CLI tarball
162+
run: |
163+
cp "${{ steps.bundle_cli.outputs.binary_path }}" warp-oss
164+
tar czf openwarp-macos-${{ matrix.arch }}.tar.gz \
165+
warp-oss \
166+
-C "$(dirname "${{ steps.bundle_cli.outputs.bundled_resources_dir }}")" resources
167+
shell: bash
168+
169+
- name: Upload CLI artifact
170+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
171+
with:
172+
name: openwarp-cli-macos-${{ matrix.arch }}
173+
path: openwarp-macos-${{ matrix.arch }}.tar.gz
174+
if-no-files-found: error
175+
retention-days: 7
176+
153177
release_windows:
154178
name: Build Release (Windows x64)
155179
runs-on: windows-latest
@@ -250,6 +274,31 @@ jobs:
250274
if-no-files-found: error
251275
retention-days: 7
252276

277+
- name: Build CLI
278+
id: bundle_cli
279+
run: |
280+
script/bundle --channel "$CHANNEL" --arch x86_64 --artifact cli --packages none
281+
shell: bash
282+
env:
283+
GIT_RELEASE_TAG: ${{ needs.prepare_metadata.outputs.release_tag }}
284+
APPIMAGE_EXTRACT_AND_RUN: 1
285+
286+
- name: Package CLI tarball
287+
run: |
288+
cp "${{ steps.bundle_cli.outputs.executable_path }}" warp-oss
289+
tar czf openwarp-linux-x86_64.tar.gz \
290+
warp-oss \
291+
-C "$(dirname "${{ steps.bundle_cli.outputs.bundled_resources_dir }}")" resources
292+
shell: bash
293+
294+
- name: Upload CLI artifact
295+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
296+
with:
297+
name: openwarp-cli-linux-x86_64
298+
path: openwarp-linux-x86_64.tar.gz
299+
if-no-files-found: error
300+
retention-days: 7
301+
253302
publish_release:
254303
name: Publish GitHub Release
255304
runs-on: ubuntu-latest

app/src/remote_server/ssh_transport.rs

Lines changed: 182 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55
//! whose stdin/stdout become the protocol channel.
66
use std::fmt;
77
use std::future::Future;
8-
use std::path::PathBuf;
8+
use std::path::{Path, PathBuf};
99
use std::pin::Pin;
1010
use std::sync::Arc;
1111

12-
use anyhow::Result;
13-
use warpui::r#async::executor;
12+
use anyhow::{anyhow, Result};
13+
use warpui::r#async::{executor, FutureExt as _};
1414

1515
use remote_server::auth::RemoteServerAuthContext;
1616
use remote_server::client::RemoteServerClient;
@@ -74,30 +74,174 @@ impl SshTransport {
7474
}
7575
}
7676

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+
77236
impl RemoteTransport for SshTransport {
78237
fn detect_platform(
79238
&self,
80239
) -> Pin<Box<dyn Future<Output = Result<RemotePlatform, String>> + Send>> {
81240
let socket_path = self.socket_path.clone();
82241
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:#}"))
101245
})
102246
}
103247

@@ -141,11 +285,11 @@ impl RemoteTransport for SshTransport {
141285
)
142286
.await
143287
{
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 退出视为真实检查失败。
146290
Ok(output) => match output.status.code() {
147291
Some(0) => Ok(true),
148-
Some(1) => Ok(false),
292+
Some(126) | Some(127) => Ok(false),
149293
Some(code) => {
150294
let stderr = String::from_utf8_lossy(&output.stderr);
151295
Err(format!("binary check exited with code {code}: {stderr}"))
@@ -193,25 +337,26 @@ impl RemoteTransport for SshTransport {
193337
fn install_binary(&self) -> Pin<Box<dyn Future<Output = Result<(), String>> + Send>> {
194338
let socket_path = self.socket_path.clone();
195339
Box::pin(async move {
196-
let script = remote_server::setup::install_script();
197340
log::info!(
198341
"Installing remote server binary to {}",
199342
remote_server::setup::remote_server_binary()
200343
);
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
207346
{
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+
}
213359
}
214-
Err(e) => Err(format!("{e:#}")),
215360
}
216361
})
217362
}

0 commit comments

Comments
 (0)