Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions architecture/sandbox-connect.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,9 @@ on the host are forwarded to `127.0.0.1:<port>` inside the sandbox.
#### CLI

- Reuses the same `ProxyCommand` path as `sandbox connect`.
- Invokes OpenSSH with `-N -L <port>:127.0.0.1:<port> sandbox`.
- By default stays attached in foreground until interrupted (Ctrl+C).
- Invokes OpenSSH with `-N -o ExitOnForwardFailure=yes -L <port>:127.0.0.1:<port> sandbox`.
- By default stays attached in foreground until interrupted (Ctrl+C), and prints an early startup
confirmation after SSH stays up through its initial forward-setup checks.
- With `-d`/`--background`, SSH forks after auth and the CLI exits. The PID is
tracked in `~/.config/openshell/forwards/<name>-<port>.pid` along with sandbox id metadata.
- `openshell forward stop <port> <name>` validates PID ownership and then kills a background forward.
Expand Down
44 changes: 39 additions & 5 deletions crates/openshell-cli/src/ssh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use tokio::process::Command as TokioCommand;
use tokio_rustls::TlsConnector;

const FOREGROUND_FORWARD_STARTUP_GRACE_PERIOD: Duration = Duration::from_secs(2);

#[derive(Clone, Copy, Debug)]
pub enum Editor {
Vscode,
Expand Down Expand Up @@ -309,9 +313,11 @@ pub async fn sandbox_forward(
) -> Result<()> {
let session = ssh_session_config(server, name, tls).await?;

let mut command = ssh_base_command(&session.proxy_command);
let mut command = TokioCommand::from(ssh_base_command(&session.proxy_command));
command
.arg("-N")
.arg("-o")
.arg("ExitOnForwardFailure=yes")
Comment on lines +319 to +320
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With this, if the tunnel is not established for some reason (e.g. port is already used), it will exit. Currently it will hang after printing this:

openshell forward start 10001 peaceful-stinkbug
bind [::1]:10001: Address already in use
channel_setup_fwd_listener_tcpip: cannot listen to port: 10001
Could not request local forwarding.

# hangs

.arg("-L")
.arg(format!("{port}:127.0.0.1:{port}"));

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

let status = tokio::task::spawn_blocking(move || command.status())
.await
.into_diagnostic()?
.into_diagnostic()?;
let status = if background {
command.status().await.into_diagnostic()?
} else {
let mut child = command.spawn().into_diagnostic()?;
match tokio::time::timeout(FOREGROUND_FORWARD_STARTUP_GRACE_PERIOD, child.wait()).await {
Ok(status) => status.into_diagnostic()?,
Err(_) => {
eprintln!("{}", foreground_forward_started_message(name, port));
child.wait().await.into_diagnostic()?
}
}
};

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

fn foreground_forward_started_message(name: &str, port: u16) -> String {
format!(
"{} Forwarding port {port} to sandbox {name}\n Access at: http://127.0.0.1:{port}/\n Press Ctrl+C to stop\n {}",
"✓".green().bold(),
"Hint: pass --background to start forwarding without blocking your terminal".dimmed(),
)
}

async fn sandbox_exec_with_mode(
server: &str,
name: &str,
Expand Down Expand Up @@ -1105,4 +1127,16 @@ mod tests {
let text = format!("{err}");
assert!(text.contains("openshell-test-missing-binary is not installed or not on PATH"));
}

#[test]
fn foreground_forward_started_message_includes_port_and_stop_hint() {
let message = foreground_forward_started_message("demo", 8080);
assert!(message.contains("Forwarding port 8080 to sandbox demo"));
assert!(message.contains("Access at: http://127.0.0.1:8080/"));
assert!(message.contains("sandbox demo"));
assert!(message.contains("Press Ctrl+C to stop"));
assert!(message.contains(
"Hint: pass --background to start forwarding without blocking your terminal"
));
}
}
Loading