-
Notifications
You must be signed in to change notification settings - Fork 1
Add detached processes and port forwarding to the provider traits #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
36ef6e9
492ab71
d502b00
11278f7
7ed7c12
959885c
f5176d9
1c9a83a
fad58a5
251d57d
5e4bad0
af46c0a
3211470
97576f8
c45cabe
1dce478
11a5f37
eaaea1e
0f1d50f
e7ed240
a7374a5
90b2fbc
23c1073
6a96012
2a5f3bb
e688699
2be62a1
d6113ff
35a284b
226c7b0
646497f
fd02130
ca4a688
fb4bbd4
02cbbbb
96dcf88
907bc8f
f6a4678
7e3edfe
32150df
6fdc6b1
369412c
d564c7e
c273d3b
dea6f2b
bff125a
5b80fdf
7e15a5d
3b004a9
7ddb093
74daccb
0be3c8c
98f23b6
aef145f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,7 +8,7 @@ use std::sync::Arc; | |
| use clap::{Parser, Subcommand, ValueEnum}; | ||
| use tinybox_core::{ | ||
| BoxId, BoxInfo, BoxSpec, Clock, Error, ExecRequest, Host, HostRef, NetworkPolicy, | ||
| PassthroughSandbox, Placement, PortMapping, Sandbox, SandboxRef, SnapshotId, Store, | ||
| PassthroughSandbox, Placement, PortMapping, ProcessId, Sandbox, SandboxRef, SnapshotId, Store, | ||
| SystemClock, TemplateName, Templates, WorkspaceSource, passthrough, | ||
| }; | ||
| use tinybox_docker::DockerSandbox; | ||
|
|
@@ -177,6 +177,47 @@ enum Command { | |
| #[arg(trailing_var_arg = true, required = true, value_name = "COMMAND")] | ||
| argv: Vec<String>, | ||
| }, | ||
| /// Start a command in a box and leave it running. | ||
| /// | ||
| /// Where `exec` waits, this returns a process id as soon as the command is | ||
| /// started. It is how a server gets into a box; `exec` would never return. | ||
| Spawn { | ||
| /// Which box to start it in. | ||
| id: String, | ||
| /// The command and its arguments. | ||
| #[arg(trailing_var_arg = true, required = true, value_name = "COMMAND")] | ||
| argv: Vec<String>, | ||
| }, | ||
| /// Report whether a spawned process is still running. | ||
| Ps { | ||
| /// Which box it was started in. | ||
| id: String, | ||
| /// The process id `spawn` printed. | ||
| process: String, | ||
| }, | ||
| /// Stop a spawned process. | ||
| /// | ||
| /// Succeeds when it has already exited: stopping something already stopped | ||
| /// is the outcome the caller wanted. | ||
| Kill { | ||
| /// Which box it was started in. | ||
| id: String, | ||
| /// The process id `spawn` printed. | ||
| process: String, | ||
| }, | ||
| /// Make a port on the box's machine reachable from this one. | ||
| /// | ||
| /// Publishing a port (`create -p`) puts it on the machine the box runs on. | ||
| /// When that is somewhere else, this is what closes the gap. The tunnel | ||
| /// lasts as long as the command runs, so it holds until interrupted. | ||
| Forward { | ||
| /// The port on the box's machine. | ||
| port: u16, | ||
| /// The address to reach it at over there. Defaults to loopback, which | ||
| /// is where a published port lands. | ||
| #[arg(long, value_name = "IP", default_value = "127.0.0.1")] | ||
| address: std::net::IpAddr, | ||
| }, | ||
| /// List every box. | ||
| #[command(alias = "list")] | ||
| Ls, | ||
|
|
@@ -350,12 +391,11 @@ impl Cli { | |
| )?; | ||
| announce(&sandbox.create(&spec).await?, sandbox.as_ref(), out, err) | ||
| } | ||
| Command::Exec { id, argv } => { | ||
| let id = BoxId::new(id)?; | ||
| let sandbox = build(sandbox_of(&store, &id)?)?; | ||
| let output = sandbox.exec(&id, &ExecRequest::new(argv)).await?; | ||
| report(&output, out, err) | ||
| } | ||
| Command::Exec { id, argv } => exec(&store, &backends, id, argv, out, err).await, | ||
| Command::Spawn { id, argv } => spawn(&store, &backends, id, argv, out).await, | ||
| Command::Ps { id, process } => probe(&store, &backends, id, &process, out).await, | ||
| Command::Kill { id, process } => kill(&store, &backends, id, &process, out).await, | ||
| Command::Forward { port, address } => forward(reach.as_ref(), address, port, out).await, | ||
| // Listing is the store's business, not the sandbox's: the store is | ||
| // what owns the set of records. | ||
| Command::Ls => text(out, &render_listing(&store.list()?)), | ||
|
|
@@ -570,6 +610,37 @@ fn line(out: &mut dyn Write, value: &str) -> tinybox_core::Result<u8> { | |
| text(out, &format!("{value}\n")) | ||
| } | ||
|
|
||
| /// Open a tunnel to `remote` and hold it until the process is interrupted. | ||
| /// | ||
| /// The forward is a guard, so it exists for exactly as long as this function | ||
| /// runs. There is no daemon to hand it to and no state file that could | ||
| /// describe a tunnel this process is no longer holding open, so blocking is | ||
| /// the honest shape: the command running *is* the forward existing. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns whatever the host reports when the tunnel cannot be opened — | ||
| /// [`Error::Unsupported`] from a host that cannot tunnel at all. | ||
| async fn forward( | ||
| reach: &dyn Host, | ||
| address: std::net::IpAddr, | ||
| port: u16, | ||
| out: &mut dyn Write, | ||
| ) -> tinybox_core::Result<u8> { | ||
| let forwarded = reach.forward((address, port).into()).await?; | ||
| line(out, &forwarded.local_addr().to_string())?; | ||
|
|
||
| if forwarded.is_direct() { | ||
| // Nothing is being held open, so there is nothing to hold *for*. | ||
| // Blocking here would look like a working tunnel and be a hang. | ||
| return Ok(0); | ||
| } | ||
| // Park until the terminal interrupts us; dropping `forwarded` on the way | ||
| // out closes the tunnel. | ||
| std::future::pending::<()>().await; | ||
|
Comment on lines
+638
to
+640
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
ast-grep outline crates/tinybox-cli/src/command/mod.rs --items all
rg -n -C 5 'ctrl_c|tokio::signal|signal_hook|SIGINT|SIGTERM|forward\(' crates/tinybox-cli
rg -n -C 8 'forward|abort|ctrl_c|SIGINT|SIGTERM' crates/tinybox-cli/src
fd -a '^Cargo\.toml$' crates/tinybox-cli -x sed -n '1,220p' {}Repository: tinyhumansai/tinybox Length of output: 15447 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- CLI entry points and signal handling ---'
fd -a -t f '.*\.rs$' crates/tinybox-cli | sort
rg -n -C 12 'command::run|run_with_host|ctrl_c|tokio::signal|signal_hook|SIGINT|SIGTERM|signal\(' crates/tinybox-cli crates/tinybox-core
printf '%s\n' '--- Forward implementation and guard drop behavior ---'
rg -n -C 12 'trait Host|struct Forward|enum Forward|impl.*Forward|fn forward\(|is_direct|Drop for' crates/tinybox-core crates/tinybox-host crates/tinybox-ssh crates/tinybox-docker crates/tinybox-linux crates/tinybox-microvm
printf '%s\n' '--- Referenced runtime tests ---'
cat -n crates/tinybox-core/src/runtime/forward_test.rs | sed -n '1,90p'Repository: tinyhumansai/tinybox Length of output: 29653 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '--- CLI main and library entry points ---'
cat -n crates/tinybox-cli/src/main.rs
cat -n crates/tinybox-cli/src/lib.rs | sed -n '1,80p'
cat -n crates/tinybox-cli/src/command/mod.rs | sed -n '1190,1265p'
printf '%s\n' '--- Forward guard implementation ---'
cat -n crates/tinybox-core/src/runtime/forward.rs | sed -n '1,100p'
cat -n crates/tinybox-ssh/src/host/forward.rs | sed -n '1,115p'Repository: tinyhumansai/tinybox Length of output: 14230 Handle shutdown signals before awaiting The CLI has no signal listener. Default 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| Ok(0) | ||
| } | ||
|
|
||
| /// Forward a finished command's output and status to the caller. | ||
| /// | ||
| /// # Errors | ||
|
|
@@ -783,6 +854,94 @@ fn render_sync(outcome: &tinybox_sync::Sync) -> String { | |
| /// Returns [`Error::InvalidIdentifier`] when a Docker namespace is not a valid | ||
| /// identifier. | ||
| /// Destroy one box and print its identifier back. | ||
| /// Run a command in a box, mirroring its output and exit status. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns whatever the backend reports when the command could not be started. | ||
| /// A command that runs and exits non-zero is **not** an error: its status | ||
| /// becomes this process's. | ||
| async fn exec( | ||
| store: &Arc<dyn Store>, | ||
| backends: &Backends<'_>, | ||
| id: String, | ||
| argv: Vec<String>, | ||
| out: &mut dyn Write, | ||
| err: &mut dyn Write, | ||
| ) -> tinybox_core::Result<u8> { | ||
| let id = BoxId::new(id)?; | ||
| let sandbox = backends.get(sandbox_of(store, &id)?)?; | ||
| let output = sandbox.exec(&id, &ExecRequest::new(argv)).await?; | ||
| report(&output, out, err) | ||
| } | ||
|
|
||
| /// Start a command in a box and print the identifier for asking about it. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns [`Error::Unsupported`] when the box's sandbox cannot host a process | ||
| /// between commands, and whatever the backend reports when the command could | ||
| /// not be started. | ||
| async fn spawn( | ||
| store: &Arc<dyn Store>, | ||
| backends: &Backends<'_>, | ||
| id: String, | ||
| argv: Vec<String>, | ||
| out: &mut dyn Write, | ||
| ) -> tinybox_core::Result<u8> { | ||
| let id = BoxId::new(id)?; | ||
| let sandbox = backends.get(sandbox_of(store, &id)?)?; | ||
| let process = sandbox.spawn(&id, &ExecRequest::new(argv)).await?; | ||
| line(out, process.as_ref()) | ||
| } | ||
|
|
||
| /// Report whether a spawned process is still running. | ||
| /// | ||
| /// A process that has exited prints `gone` and exits zero: that it finished is | ||
| /// an answer, and reporting it as a failure would be indistinguishable from an | ||
| /// unreachable box. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns [`Error::Unsupported`] when the box's sandbox does not track | ||
| /// detached processes, and a backend error when the box cannot be reached. | ||
| async fn probe( | ||
| store: &Arc<dyn Store>, | ||
| backends: &Backends<'_>, | ||
| id: String, | ||
| process: &str, | ||
| out: &mut dyn Write, | ||
| ) -> tinybox_core::Result<u8> { | ||
| let id = BoxId::new(id)?; | ||
| let sandbox = backends.get(sandbox_of(store, &id)?)?; | ||
| let running = sandbox | ||
| .is_running(&id, &ProcessId::new(process.to_owned())?) | ||
| .await?; | ||
| line(out, if running { "running" } else { "gone" }) | ||
| } | ||
|
|
||
| /// Stop a spawned process. | ||
| /// | ||
| /// # Errors | ||
| /// | ||
| /// Returns [`Error::Unsupported`] when the box's sandbox does not track | ||
| /// detached processes, and a backend error when the box cannot be reached. A | ||
| /// process that had already exited is not an error. | ||
| async fn kill( | ||
| store: &Arc<dyn Store>, | ||
| backends: &Backends<'_>, | ||
| id: String, | ||
| process: &str, | ||
| out: &mut dyn Write, | ||
| ) -> tinybox_core::Result<u8> { | ||
| let id = BoxId::new(id)?; | ||
| let sandbox = backends.get(sandbox_of(store, &id)?)?; | ||
| sandbox | ||
| .stop(&id, &ProcessId::new(process.to_owned())?) | ||
| .await?; | ||
| line(out, "stopped") | ||
| } | ||
|
|
||
| async fn remove( | ||
| store: &Arc<dyn Store>, | ||
| backends: &Backends<'_>, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Update the Docker
inspectexample.Line 193 says
inspectreports detached-process support and says Docker supports it. The Docker output example at Line 117 omitsdetached processes. Add that capability to the example so the documented output matches the command behavior.🤖 Prompt for AI Agents