Skip to content

Commit 72868ea

Browse files
authored
fix(cli): show startup feedback for foreground forwards (#296)
1 parent 885fdc3 commit 72868ea

File tree

2 files changed

+42
-7
lines changed

2 files changed

+42
-7
lines changed

architecture/sandbox-connect.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,9 @@ on the host are forwarded to `127.0.0.1:<port>` inside the sandbox.
163163
#### CLI
164164

165165
- Reuses the same `ProxyCommand` path as `sandbox connect`.
166-
- Invokes OpenSSH with `-N -L <port>:127.0.0.1:<port> sandbox`.
167-
- By default stays attached in foreground until interrupted (Ctrl+C).
166+
- Invokes OpenSSH with `-N -o ExitOnForwardFailure=yes -L <port>:127.0.0.1:<port> sandbox`.
167+
- By default stays attached in foreground until interrupted (Ctrl+C), and prints an early startup
168+
confirmation after SSH stays up through its initial forward-setup checks.
168169
- With `-d`/`--background`, SSH forks after auth and the CLI exits. The PID is
169170
tracked in `~/.config/openshell/forwards/<name>-<port>.pid` along with sandbox id metadata.
170171
- `openshell forward stop <port> <name>` validates PID ownership and then kills a background forward.

crates/openshell-cli/src/ssh.rs

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@ use std::os::unix::process::CommandExt;
2020
use std::path::{Path, PathBuf};
2121
use std::process::{Command, Stdio};
2222
use std::sync::Arc;
23+
use std::time::Duration;
2324
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader};
2425
use tokio::net::TcpStream;
26+
use tokio::process::Command as TokioCommand;
2527
use tokio_rustls::TlsConnector;
2628

29+
const FOREGROUND_FORWARD_STARTUP_GRACE_PERIOD: Duration = Duration::from_secs(2);
30+
2731
#[derive(Clone, Copy, Debug)]
2832
pub enum Editor {
2933
Vscode,
@@ -309,9 +313,11 @@ pub async fn sandbox_forward(
309313
) -> Result<()> {
310314
let session = ssh_session_config(server, name, tls).await?;
311315

312-
let mut command = ssh_base_command(&session.proxy_command);
316+
let mut command = TokioCommand::from(ssh_base_command(&session.proxy_command));
313317
command
314318
.arg("-N")
319+
.arg("-o")
320+
.arg("ExitOnForwardFailure=yes")
315321
.arg("-L")
316322
.arg(format!("{port}:127.0.0.1:{port}"));
317323

@@ -326,10 +332,18 @@ pub async fn sandbox_forward(
326332
.stdout(Stdio::inherit())
327333
.stderr(Stdio::inherit());
328334

329-
let status = tokio::task::spawn_blocking(move || command.status())
330-
.await
331-
.into_diagnostic()?
332-
.into_diagnostic()?;
335+
let status = if background {
336+
command.status().await.into_diagnostic()?
337+
} else {
338+
let mut child = command.spawn().into_diagnostic()?;
339+
match tokio::time::timeout(FOREGROUND_FORWARD_STARTUP_GRACE_PERIOD, child.wait()).await {
340+
Ok(status) => status.into_diagnostic()?,
341+
Err(_) => {
342+
eprintln!("{}", foreground_forward_started_message(name, port));
343+
child.wait().await.into_diagnostic()?
344+
}
345+
}
346+
};
333347

334348
if !status.success() {
335349
return Err(miette::miette!("ssh exited with status {status}"));
@@ -351,6 +365,14 @@ pub async fn sandbox_forward(
351365
Ok(())
352366
}
353367

368+
fn foreground_forward_started_message(name: &str, port: u16) -> String {
369+
format!(
370+
"{} Forwarding port {port} to sandbox {name}\n Access at: http://127.0.0.1:{port}/\n Press Ctrl+C to stop\n {}",
371+
"✓".green().bold(),
372+
"Hint: pass --background to start forwarding without blocking your terminal".dimmed(),
373+
)
374+
}
375+
354376
async fn sandbox_exec_with_mode(
355377
server: &str,
356378
name: &str,
@@ -1105,4 +1127,16 @@ mod tests {
11051127
let text = format!("{err}");
11061128
assert!(text.contains("openshell-test-missing-binary is not installed or not on PATH"));
11071129
}
1130+
1131+
#[test]
1132+
fn foreground_forward_started_message_includes_port_and_stop_hint() {
1133+
let message = foreground_forward_started_message("demo", 8080);
1134+
assert!(message.contains("Forwarding port 8080 to sandbox demo"));
1135+
assert!(message.contains("Access at: http://127.0.0.1:8080/"));
1136+
assert!(message.contains("sandbox demo"));
1137+
assert!(message.contains("Press Ctrl+C to stop"));
1138+
assert!(message.contains(
1139+
"Hint: pass --background to start forwarding without blocking your terminal"
1140+
));
1141+
}
11081142
}

0 commit comments

Comments
 (0)